65

I am getting a string from html parse that is;

string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"

my code is something like

var startIndex = text.rangeOfString("'")
var endIndex = text.rangeOfString("',")
var range2 = startIndex2...endIndex
substr= string.substringWithRange(range)

i am not sure if my second splitting string should be "'" or "',"

i want my outcome as

substr = "Info/99/something"
Alp
  • 1,863
  • 1
  • 20
  • 38
  • Is there always the same length (for example the 1) - or is that different? Is the 'Info/..." always the same? Pls share some more Strings, to find out the best way to get the String. – derdida Jul 30 '15 at 13:48
  • javascript:getInfo(1,'Info/123/somethingelse', 'City2 hall3',456,789); – Alp Jul 30 '15 at 13:59

12 Answers12

130
extension String {
    
    func slice(from: String, to: String) -> String? {
        return (range(of: from)?.upperBound).flatMap { substringFrom in
            (range(of: to, range: substringFrom..<endIndex)?.lowerBound).map { substringTo in
                String(self[substringFrom..<substringTo])
            }
        }
    }
}

"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
  .sliceFrom("'", to: "',")
pkamb
  • 33,281
  • 23
  • 160
  • 191
oisdk
  • 9,763
  • 4
  • 18
  • 36
  • 1
    @oisdk does it work if we need to slice from a string to the end of the string ? Ex : "blabla popo titi toto" -> slice(from "popo", to : endOfString) ? – Makaille Jan 20 '17 at 14:34
  • 1
    In Swift 4 you can also compute the second range as `self[substringFrom...].range(of: to)`, that saves some keystrokes and the mention of `endIndex`. – Martin R May 15 '18 at 07:27
  • I found this `return flatmap.().map()` code pretty hard to grok for the function; so I tried [rewriting a version using `guard`](https://stackoverflow.com/a/57865482/1265393). – pkamb Oct 17 '19 at 18:32
  • @Makaille see this [answer](https://stackoverflow.com/a/69242560/7486818) – Mendy Sep 19 '21 at 10:55
30

I'd use a regular expression to extract substrings from complex input like this.

Swift 3.1:

let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"

if let match = test.range(of: "(?<=')[^']+", options: .regularExpression) {
    print(test.substring(with: match))
}

// Prints: Info/99/something

Swift 2.0:

let test = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"

if let match = test.rangeOfString("(?<=')[^']+", options: .RegularExpressionSearch) {
    print(test.substringWithRange(match))
}

// Prints: Info/99/something
seb
  • 2,350
  • 24
  • 30
  • For more detail on the regex, see https://stackoverflow.com/questions/6109882/regex-match-all-characters-between-two-strings – bshirley Nov 18 '19 at 17:45
23

I rewrote one of the top Swift answers to understand what it was doing with map. I prefer a version using guard, IMO.

extension String {
    
    func slice(from: String, to: String) -> String? {
        guard let rangeFrom = range(of: from)?.upperBound else { return nil }
        guard let rangeTo = self[rangeFrom...].range(of: to)?.lowerBound else { return nil }
        return String(self[rangeFrom..<rangeTo])
    }
    
}

behavior:

let test1 =   "a[b]c".slice(from: "[", to: "]") // "b"
let test2 =     "abc".slice(from: "[", to: "]") // nil
let test3 =   "a]b[c".slice(from: "[", to: "]") // nil
let test4 = "[a[b]c]".slice(from: "[", to: "]") // "a[b"
pkamb
  • 33,281
  • 23
  • 160
  • 191
15

To find all substrings that are between a starting string and an ending string:

extension String {
    func sliceMultipleTimes(from: String, to: String) -> [String] {
        components(separatedBy: from).dropFirst().compactMap { sub in
            (sub.range(of: to)?.lowerBound).flatMap { endRange in
                String(sub[sub.startIndex ..< endRange])
            }
        }
    }
}

let str = "start A end ... start B end"
str.sliceMultipleTimes(from: "start", to: "end")    // ["A", "B"]
Shaked Sayag
  • 5,712
  • 2
  • 31
  • 38
  • Hi, I love this answer, how can we slightly change the returned results to be the string between the 2 strings but also include the from and to not just return what's in between? Thanks. – Wael Jun 23 '23 at 09:54
  • @Wael Just change the last line to: from + String(sub[sub.startIndex ..< endRange]) + to – Shaked Sayag Jun 25 '23 at 15:24
  • Thank you. I did work it out Thanks for responding. – Wael Jun 27 '23 at 20:40
6

This works if it is always the second split:

let subString = split(string, isSeparator: "'")[1]
Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • let subString = split(text, "'") ; return subString[1]; in this code it gives error "Missing argument for parameter "isSeparator" in call" – Alp Jul 30 '15 at 14:18
5

You can use var arr = str.componentsSeparatedByString(",") as your second split which will return you array

iAnurag
  • 9,286
  • 3
  • 31
  • 48
  • im doing this substring process in a a function, when i call return subString[1] gives error "cannot subscript a value of type '[String]' with an index of type ()." if i call subString[0] it works as expected – Alp Jul 30 '15 at 14:15
5

In Swift 4 or later you can create an extension method on StringProtocol to support substrings as well. You can just return a Substring instead of a new String:

edit/update: Swift 5 or later

extension StringProtocol  {
    func substring<S: StringProtocol>(from start: S, options: String.CompareOptions = []) -> SubSequence? {
        guard let lower = range(of: start, options: options)?.upperBound
        else { return nil }
        return self[lower...]
    }
    func substring<S: StringProtocol>(through end: S, options: String.CompareOptions = []) -> SubSequence? {
        guard let upper = range(of: end, options: options)?.upperBound
        else { return nil }
        return self[..<upper]
    }
    func substring<S: StringProtocol>(upTo end: S, options: String.CompareOptions = []) -> SubSequence? {
        guard let upper = range(of: end, options: options)?.lowerBound
        else { return nil }
        return self[..<upper]
    }
    func substring<S: StringProtocol, T: StringProtocol>(from start: S, upTo end: T, options: String.CompareOptions = []) -> SubSequence? {
        guard let lower = range(of: start, options: options)?.upperBound,
            let upper = self[lower...].range(of: end, options: options)?.lowerBound
        else { return nil }
        return self[lower..<upper]
    }
    func substring<S: StringProtocol, T: StringProtocol>(from start: S, through end: T, options: String.CompareOptions = []) -> SubSequence? {
        guard let lower = range(of: start, options: options)?.upperBound,
            let upper = self[lower...].range(of: end, options: options)?.upperBound
        else { return nil }
        return self[lower..<upper]
    }
}

Usage:

let string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
let substr = string.substring(from: "'")                   // "Info/99/something', 'City Hall',1, 99);"
let through = string.substring(through: "Info")  // "javascript:getInfo"
let upTo = string.substring(upTo: "Info")  // "javascript:get"
let fromUpTo = string.substring(from: "'", upTo: "',")  // "Info/99/something"
let fromThrough = string.substring(from: "'", through: "',")  // "Info/99/something',"

let fromUpToCaseInsensitive = string.substring(from: "'info/", upTo: "/something", options: .caseInsensitive)  // "99"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
5

Swift 4.2:

extension String {

    //right is the first encountered string after left
    func between(_ left: String, _ right: String) -> String? {
        guard let leftRange = range(of: left), let rightRange = range(of: right, options: .backwards)
            ,leftRange.upperBound <= rightRange.lowerBound else { return nil }

        let sub = self[leftRange.upperBound...]
        let closestToLeftRange = sub.range(of: right)!
        return String(sub[..<closestToLeftRange.lowerBound])
    }

}
Devansh Vyas
  • 447
  • 1
  • 5
  • 8
5

Swift 5

extension String {
    /// Returns an array of substrings between the specified left and right strings.
    /// Returns an empty array when there are no matches.
    func substring(from left: String, to right: String) -> [String] {
        // Escape special characters in the left and right strings for use in a regular expression
        let leftEscaped = NSRegularExpression.escapedPattern(for: left)
        let rightEscaped = NSRegularExpression.escapedPattern(for: right)

        // Create a regular expression pattern to match content between the last occurrence of the left string
        // and the right string
        let pattern = "\(leftEscaped).*(?<=\(leftEscaped))(.*?)(?=\(rightEscaped))"

        // Create a regular expression object with the pattern
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
            return []
        }

        // Find matches in the current string
        let matches = regex.matches(in: self, options: [], range: NSRange(startIndex..., in: self))

        // Extract the substrings from the matches and return them in an array
        return matches.compactMap { match in
            guard let range = Range(match.range(at: 1), in: self) else { return nil }
            return String(self[range])
        }
    }
}

// Example usage
let result = "cat cat dog rat".substring(from: "cat", to: "rat")
print(result) // Output: [" dog "]
ScottyBlades
  • 12,189
  • 5
  • 77
  • 85
4

Consider using a regular expression to match everything between single quotes.

let string = "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"

let pattern = "'(.+?)'"
let regex = NSRegularExpression(pattern: pattern, options: nil, error: nil)
let results = regex!.matchesInString(string, options: nil, range: NSMakeRange(0, count(string)))  as! [NSTextCheckingResult]

let nsstring = string as NSString
let matches = results.map { result in return nsstring.substringWithRange(result.range)}

// First match
println(matches[0])
litso
  • 322
  • 1
  • 6
2

If you want to support also from the start or end of the string

extension String {
    func slice(from: String, to: String) -> String? {
        return (from.isEmpty ? startIndex..<startIndex : range(of: from)).flatMap { fromRange in
            (to.isEmpty ? endIndex..<endIndex : range(of: to, range: fromRange.upperBound..<endIndex)).map({ toRange in
                String(self[fromRange.upperBound..<toRange.lowerBound])
            })
        }
    }
}

"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
  .slice(from: "'", to: "',") // "Info/99/something"

"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
  .slice(from: "", to: ":") // "javascript"

"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
  .slice(from: ":", to: "") // "getInfo(1,'Info/99/something', 'City Hall',1, 99);"

"javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"
  .slice(from: "", to: "") // "javascript:getInfo(1,'Info/99/something', 'City Hall',1, 99);"

if you want another syntax, maybe more readable

extension String {
    func slice(from: String, to: String) -> String? {
        guard let fromRange = from.isEmpty ? startIndex..<startIndex : range(of: from) else { return nil }
        guard let toRange = to.isEmpty ? endIndex..<endIndex : range(of: to, range: fromRange.upperBound..<endIndex) else { return nil }
        
        return String(self[fromRange.upperBound..<toRange.lowerBound])
    } 
}
Mendy
  • 176
  • 2
  • 8
0

Swift 4 version of @litso. To find all values in text

func find(inText text: String, pattern: String) -> [String]? {
    do {
        let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
        let result = regex.matches(in: text, options: .init(rawValue: 0), range: NSRange(location: 0, length: text.count))

        let matches = result.map { result in
            return (text as NSString).substring(with: result.range)
        }

        return matches
    } catch {
        print(error)
    }
    return nil
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Arek
  • 375
  • 5
  • 5