Swift: Extract a Substring from a String

Viewed 171

I have a String that can be either of the following:

tempString = "What? The ID: 54673 Over there"
tempString = "Jump... ID: 4561E how high"

Basically, I want to retrieve the five character ID: from the string. I can do this with most languages by finding ID:, adding 4 to it to get to the beginning of the ID:, and then taking the next five characters.

For example, in Excel:

=MID("Jump... ID: 4561E how high",FIND("ID:","Jump... ID: 4561E how high")+4, 5)

Would = 4561E.

How do we do this with Swift?

4 Answers

You can use range(of:) on the string to find the range of the substring, and then use the upperBound of that range to index the original string to get the substring starting from that point. Finally prefix(5) allows you to grab the first 5 characters of that substring and String() turns the Substring.Subsequence back into a normal String:

if let range = tempString.range(of: "ID: ") {
    let ident = String(tempString[range.upperBound...].prefix(5))
    print(ident)
}

Note: range(of:) comes from the Foundation framework, so you need to import Foundation or import another framework that uses Foundation such as UIKit or Cocoa.

The regex approach to find the first 5 characters from [0-9] or [A-F] after "ID: "(used hex letters only if this is not the case you can use A-Z).

if let range = string.range(of: "(?<=ID: )[0-9A-F]{5}", options: .regularExpression) {
    let id = string[range]  // 
    // if you need a String instead of a substring
    let stringID = String(string[range])
}

edit/update:

Looking at your answer it looks like the requirements are totally different from your original post anyway to find an IF compound of 8 hexa characters followed by hyphen then 4 hexa characters followed by hyphen (3 times) than 12 hexa characters you can use the following regex "([0-9a-f]{8}-)([0-9a-f]{4}-){3}([0-9a-f]{12})":


let dataString = """
{
  "@odata.context": "$metadata#GeoFences(Points())/$entity",
  "ID": "2b2a2abc-5962-4290-92b4-773025ffd50b",
  "Points": {
    "POINT_TYPE": "F",
    "POINT_NUM": 0,
    "LATITUDE": 32.92197686725423,
    "LONGITUDE": -117.04306932263532,
    "parent_ID": "2b2a2abc-5962-4290-92b4-773025ffd50b"
  },
  "GEOFENCE_NAME": "New Fence",
  "GEOFENCE_TYPE": "O",
  "PRIVACY": "X",
  "CENTER_LAT": 32.92043316309709,
  "CENTER_LONG": -117.04286922250975,
  "ZOOM_LAT": 0.006238797350533787,
  "ZOOM_LONG": 0.005345531926053582,
  "PATH_TOLERANCE": 5,
  "ENTRANCE_TOLERANCE": 5
}
"""

if let range = dataString.range(of: "([0-9a-f]{8}-)([0-9a-f]{4}-){3}([0-9a-f]{12})", options: .regularExpression) {
    let id = dataString[range]  // 4561E
    print("ID:", id)
    // if you need a String instead of a substring
    let stringID = String(dataString[range])
    print("stringID:", stringID)
}

This will print

ID: 2b2a2abc-5962-4290-92b4-773025ffd50b

stringID: 2b2a2abc-5962-4290-92b4-773025ffd50b


Note that your code would result in "2b2a2abc-5962-4290-92b4-773025ffd50


edit/update2:

Considering that your string is JSON you can simply decode your string id:

struct Root: Codable {
    let id: String
    enum CodingKeys: String, CodingKey {
        case id = "ID"
    }
}
do {
    let id = try JSONDecoder().decode(Root.self, from: Data(dataString.utf8)).id
    print(id)  // "2b2a2abc-5962-4290-92b4-773025ffd50b"
} catch {
    print(error)
}

If you need to decode all your data:

struct Root: Codable {
    let odataContext, id: String
    let points: Points
    let geofenceName, geofenceType, privacy: String
    let centerLat, centerLong, zoomLat, zoomLong: Double
    let pathTolerance, entranceTolerance: Int

    enum CodingKeys: String, CodingKey {
        case odataContext = "@odata.context", id = "ID", points = "Points", geofenceName = "GEOFENCE_NAME", geofenceType = "GEOFENCE_TYPE", privacy = "PRIVACY", centerLat = "CENTER_LAT", centerLong = "CENTER_LONG", zoomLat = "ZOOM_LAT", zoomLong = "ZOOM_LONG", pathTolerance = "PATH_TOLERANCE", entranceTolerance = "ENTRANCE_TOLERANCE"
    }
}

struct Points: Codable {
    let pointType: String
    let pointNum: Int
    let latitude, longitude: Double
    let parentID: String

    enum CodingKeys: String, CodingKey {
        case pointType = "POINT_TYPE", pointNum = "POINT_NUM", latitude = "LATITUDE", longitude = "LONGITUDE", parentID = "parent_ID"
    }
}

do {
    let root = try JSONDecoder().decode(Root.self, from: Data(dataString.utf8))
    print("ID:", root.id)  // ID: 2b2a2abc-5962-4290-92b4-773025ffd50b
    print("Root:", root)   // Root: Root(odataContext: "$metadata#GeoFences(Points())/$entity", id: "2b2a2abc-5962-4290-92b4-773025ffd50b", points: __lldb_expr_111.Points(pointType: "F", pointNum: 0, latitude: 32.92197686725423, longitude: -117.04306932263532, parentID: "2b2a2abc-5962-4290-92b4-773025ffd50b"), geofenceName: "New Fence", geofenceType: "O", privacy: "X", centerLat: 32.92043316309709, centerLong: -117.04286922250975, zoomLat: 0.0062387973505337885, zoomLong: 0.005345531926053582, pathTolerance: 5, entranceTolerance: 5)
} catch {
    print(error)
}

For your case can use NSRegularExpression:

let tempString1 = "What? The ID: 54673 Over there"
let tempString2 = "Jump... ID: 4561E how high"

func matches(for regex: String, in text: String) -> [String] {
    do {
        let regex = try NSRegularExpression(pattern: regex)
        let results = regex.matches(in: text,
                                    range: NSRange(text.startIndex..., in: text))
        return results.map {
            String(text[Range($0.range, in: text)!])
        }
    } catch let error {
        print("invalid regex: \(error.localizedDescription)")
        return []
    }
}

var res1 = matches(for: "[0-9]+[A-Z]*", in: tempString1) // 54673
var res2 = matches(for: "[0-9]+[A-Z]*", in: tempString2) // 4561E

Okay, I started this exercise because I was too lazy to decode the JSON, because I had to build the structures and get them right. So, I thought that I would quickly do a substring. Well, decoding would have been better and faster and more accurate, but I will show my code below. Basically, I wanted to get the ID from a JSON string:

{
  "@odata.context": "$metadata#GeoFences(Points())/$entity",
  "ID": "2b2a2abc-5962-4290-92b4-773025ffd50b",
  "Points": {
    "POINT_TYPE": "F",
    "POINT_NUM": 0,
    "LATITUDE": 32.92197686725423,
    "LONGITUDE": -117.04306932263532,
    "parent_ID": "2b2a2abc-5962-4290-92b4-773025ffd50b"
  },
  "GEOFENCE_NAME": "New Fence",
  "GEOFENCE_TYPE": "O",
  "PRIVACY": "X",
  "CENTER_LAT": 32.92043316309709,
  "CENTER_LONG": -117.04286922250975,
  "ZOOM_LAT": 0.006238797350533787,
  "ZOOM_LONG": 0.005345531926053582,
  "PATH_TOLERANCE": 5,
  "ENTRANCE_TOLERANCE": 5
}

My code is as follows. I will replace it with decode soon. the variable id does contain the id.

var foundI :Bool = false
var iIndex = 0
for (index, char) in dataString.enumerated() {
    if char == "I" {
        foundI = true
        iIndex = index
    } else if char == "D" {
        if foundI == true && index == iIndex+1 {
           var suffixStr =  String(dataString.dropFirst(iIndex+5))
           var id = String(suffixStr.prefix(36))
        
            print("New String \(id)")
        }
    }
}
Related