Splitting a string in swift using multiple delimiters
Asked Answered
B

8

21

I am trying to split (or explode) a string in Swift (1.2) using multiple delimiters, or seperators as Apple calls them.

My string looks like this:

KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value

I have formatted it for easy reading:

KEY1=subKey1=value&subkey2=value
KEY2=subkey1=value&subkey2=value
KEY3=subKey1=value&subkey3=value

The uppercase "KEY" are predefined names.
I was trying to do this using:

var splittedString = string.componentsSeparatedByString("KEY1")

But as you can see, I can only do this with one KEY as the separator, so I am looking for something like this:

var splittedString = string.componentsSeperatedByStrings(["KEY1", "KEY2", "KEY3"])

So the result would be:

[
  "KEY1" => "subKey1=value&subkey2=value",
  "KEY2" => "subkey1=value&subkey2=value",
  "KEY3" => "subkey1=value&subkey2=value"
]

Is there anything built into Swift 1.2 that I can use? Or is there some kind of extension/library that can do this easily?

Thanks for your time, and have a great day!

Breadnut answered 8/9, 2015 at 18:49 Comment(2)
Can value, KEYN and subKeyN have & or = in their parameters? Or Can also KEYN be in subKeyN (as a subString?)Preconize
Do you have control over the way this string is generated ? How do you know when value ends and the key begins ? Could you add another separator ?Unreasoning
L
5

This isn't very efficient, but it should do the job:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String] {
    let inds = ss.flatMap { s in
      self.rangeOfString(s).map { r in [r.startIndex, r.endIndex] } ?? []
    }
    let ended = [startIndex] + inds + [endIndex]
    let chunks = stride(from: 0, to: ended.count, by: 2)
    let bounds = map(chunks) { i in (ended[i], ended[i+1]) }
    return bounds
      .map { (s, e) in self[s..<e] }
      .filter { sl in !sl.isEmpty }
  }
}



"KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value".componentsSeperatedByStrings(["KEY1", "KEY2", "KEY3"])

// ["=subKey1=value&subkey2=value", "=subkey1=value&subkey2=value", "=subKey1=value&subkey3=value"]

Or, if you wanted it in dictionary form:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String:String] {
    let maybeRanges = ss.map { s in self.rangeOfString(s) }
    let inds   = maybeRanges.flatMap { $0.map { r in [r.startIndex, r.endIndex] } ?? [] }
    let ended  = [startIndex] + inds + [endIndex]
    let chunks = stride(from: 0, to: ended.count, by: 2)
    let bounds = map(chunks) { i in (ended[i], ended[i+1]) }
    let values = bounds
      .map { (s, e) in self[s..<e] }
      .filter { sl in !sl.isEmpty }
    let keys = filter(zip(maybeRanges, ss)) { (r, _) in r != nil }
    var result: [String:String] = [:]
    for ((_, k), v) in zip(keys, values) { result[k] = v }
    return result
  }
}


"KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value".componentsSeperatedByStrings(["KEY1", "KEY2", "KEY3"])

// ["KEY3": "=subKey1=value&subkey3=value", "KEY2": "=subkey1=value&subkey2=value", "KEY1": "=subKey1=value&subkey2=value"]

For Swift 2:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String] {
    let unshifted = ss
      .flatMap { s in rangeOfString(s) }
      .flatMap { r in [r.startIndex, r.endIndex] }
    let inds  = [startIndex] + unshifted + [endIndex]
    return inds.startIndex
      .stride(to: inds.endIndex, by: 2)
      .map { i in (inds[i], inds[i+1]) }
      .flatMap { (s, e) in s == e ? nil : self[s..<e] }
  }
}
Links answered 8/9, 2015 at 19:38 Comment(2)
Thanks man! worked great, until I updated to the new xcode 7GM and swift 2.0 syntax, do you have any idea how I could change the code so it would work? The "stride()" function is giving me an error: let chunks = stride(from: 0, to: ended.count, by: 2) "Stride(from:to:by) is unavailable" Thanks in advanceBreadnut
Thanks a lot! Works like a charm :)Breadnut
K
27

One can also use the following approach to split a string with multiple delimiters in case keys are single characters:

//swift 4+
let stringData = "K01L02M03"
let res = stringData.components(separatedBy: CharacterSet(charactersIn: "KLM"))

//older swift syntax
let res = stringData.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "KLM"));

res will contain ["01", "02", "03"]

If anyone knows any kind of special syntax to extend the approach to multiple characters per key you are welcome to suggest and to improve this answer

Kersten answered 6/5, 2016 at 19:8 Comment(1)
Some extra things to be aware of here: https://mcmap.net/q/561697/-component-separatedby-versus-split-separatorAngola
C
17

Swift 4.2 update to @vir us's answer:

let string = "dots.and-hyphens"
let array = string.components(separatedBy: CharacterSet(charactersIn: ".-"))
Cherise answered 29/11, 2018 at 16:34 Comment(0)
B
7

Swift provides a new function split here:

let line = "BLANCHE:   I don't want realism. I want magic!"
print(line.split(whereSeparator: { $0 == " " || $0 == "."}))
Basipetal answered 17/4, 2023 at 8:48 Comment(2)
This looks like a great updated approach. Documentation says it's been available since Swift 8.0+Angola
@Angola You mean iOS 8.0+ (ain't no Swift 8.0 yet)Hibachi
L
5

This isn't very efficient, but it should do the job:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String] {
    let inds = ss.flatMap { s in
      self.rangeOfString(s).map { r in [r.startIndex, r.endIndex] } ?? []
    }
    let ended = [startIndex] + inds + [endIndex]
    let chunks = stride(from: 0, to: ended.count, by: 2)
    let bounds = map(chunks) { i in (ended[i], ended[i+1]) }
    return bounds
      .map { (s, e) in self[s..<e] }
      .filter { sl in !sl.isEmpty }
  }
}



"KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value".componentsSeperatedByStrings(["KEY1", "KEY2", "KEY3"])

// ["=subKey1=value&subkey2=value", "=subkey1=value&subkey2=value", "=subKey1=value&subkey3=value"]

Or, if you wanted it in dictionary form:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String:String] {
    let maybeRanges = ss.map { s in self.rangeOfString(s) }
    let inds   = maybeRanges.flatMap { $0.map { r in [r.startIndex, r.endIndex] } ?? [] }
    let ended  = [startIndex] + inds + [endIndex]
    let chunks = stride(from: 0, to: ended.count, by: 2)
    let bounds = map(chunks) { i in (ended[i], ended[i+1]) }
    let values = bounds
      .map { (s, e) in self[s..<e] }
      .filter { sl in !sl.isEmpty }
    let keys = filter(zip(maybeRanges, ss)) { (r, _) in r != nil }
    var result: [String:String] = [:]
    for ((_, k), v) in zip(keys, values) { result[k] = v }
    return result
  }
}


"KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value".componentsSeperatedByStrings(["KEY1", "KEY2", "KEY3"])

// ["KEY3": "=subKey1=value&subkey3=value", "KEY2": "=subkey1=value&subkey2=value", "KEY1": "=subKey1=value&subkey2=value"]

For Swift 2:

import Foundation

extension String {
  func componentsSeperatedByStrings(ss: [String]) -> [String] {
    let unshifted = ss
      .flatMap { s in rangeOfString(s) }
      .flatMap { r in [r.startIndex, r.endIndex] }
    let inds  = [startIndex] + unshifted + [endIndex]
    return inds.startIndex
      .stride(to: inds.endIndex, by: 2)
      .map { i in (inds[i], inds[i+1]) }
      .flatMap { (s, e) in s == e ? nil : self[s..<e] }
  }
}
Links answered 8/9, 2015 at 19:38 Comment(2)
Thanks man! worked great, until I updated to the new xcode 7GM and swift 2.0 syntax, do you have any idea how I could change the code so it would work? The "stride()" function is giving me an error: let chunks = stride(from: 0, to: ended.count, by: 2) "Stride(from:to:by) is unavailable" Thanks in advanceBreadnut
Thanks a lot! Works like a charm :)Breadnut
L
3

Swift 5:

extension String {
    func components<T>(separatedBy separators: [T]) -> [String] where T : StringProtocol {
        var result = [self]
        for separator in separators {
            result = result
                .map { $0.components(separatedBy: separator)}
                .flatMap { $0 }
        }
        return result
    }
}

It's for the sake of nice and neat code, don't use it if you need something efficiently

Lazar answered 23/5, 2019 at 6:17 Comment(0)
A
0

You could do it with regular expressions. The below snippet is a bit clumsy and not really fail-safe but it should give you an idea.

let string = "KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value"
let re = NSRegularExpression(pattern: "(KEY1|KEY2|KEY3)=", options: nil, error: nil)!
let matches = re.matchesInString(string, options: nil,
    range: NSMakeRange(0, count(string)))

var dict = [String: String]()

for (index, match) in enumerate(matches) {
    let key = (string as NSString).substringWithRange(
        NSMakeRange(match.range.location, match.range.length - 1))

    let valueStart = match.range.location + match.range.length
    let valueEnd = index < matches.count - 1 ? matches[index + 1].range.location
                                             : count(string)
    let value = (string as NSString).substringWithRange(
        NSMakeRange(valueStart, valueEnd - valueStart))

    dict[key] = value
}

The final value of dict is

[KEY3: subKey1=value&subkey3=value, 
 KEY2: subkey1=value&subkey2=value,
 KEY1: subKey1=value&subkey2=value]
Armor answered 8/9, 2015 at 19:7 Comment(3)
Thanks for the answer, I see how it works. But the actual string im using does not have keys that are formatted like "KEYX" where X is the Int. I have keys that are completely different from each other, more like "NS", "AAAA" and "MX". How could I implement an array with these keys in your code? Thank you very much for your time!Breadnut
@Breadnut Sorry for the delay. For the record, I updated my answer to support arbitrary key names. The idea is to use an either-or-like expression (KEY1|KEY2|KEY3).Armor
Thanks man, I am currently using oisdk's method of the string extension, which works great, but I will give your method a try and see what works best.Breadnut
W
0

Swift 2 for forward compatibility

Using a regular expression:

let string  = "KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value"
let nsString :NSString = string
let stringRange = NSMakeRange(0, string.utf16.count)
let pattern = "(KEY\\d)=([^=]+=[^&]+[^=]+?=[^K]+)"
var results = [String:String]()
do {
    var regEx = try NSRegularExpression(pattern:pattern, options:[])
    regEx.enumerateMatchesInString(string, options: [], range: stringRange) {
        (result : NSTextCheckingResult?, _, _) in
        if let result = result {
            if result.numberOfRanges == 3 {
                let key   = nsString.substringWithRange(result.rangeAtIndex(1))
                let value = nsString.substringWithRange(result.rangeAtIndex(2))
                results[key] = value
            }
        }
    }
}
catch {
    print("Bad Pattern")
}

results: ["KEY3": "subKey1=value&subkey3=value", "KEY2": "subkey1=value&subkey2=value", "KEY1": "subKey1=value&subkey2=value"]

Wehrle answered 8/9, 2015 at 20:0 Comment(0)
U
0

This works:

extension StringProtocol {
    func split(by breaks: [String]) -> [any StringProtocol] {
        for split in breaks {
            guard self.contains(split) else { continue }
            return self.components(separatedBy: split).flatMap({ $0.split(by: breaks) }).filter({!$0.isEmpty})
        }
        return [self]
    }
}

Then you can use it like this

let str = "KEY1=subKey1=value&subkey2=valueKEY2=subkey1=value&subkey2=valueKEY3=subKey1=value&subkey3=value"
str.split(by: ["KEY1=", "KEY2=", "KEY3="])
// Output: ["subKey1=value&subkey2=value", "subkey1=value&subkey2=value", "subKey1=value&subkey3=value"]

Urolith answered 10/7 at 5:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.