How can I split a String and keep the delimiters in Swift?

Viewed 246

I am new to Swift, I come from Java background. I want to make a struct for parsing input for a calculator. Here is my code

struct Parse {
    var runningCalc: String
    
    init(runningCalc: String) {
        self.runningCalc = runningCalc
    }
    
    mutating func toArray(String: runningCalc){
        runningCalc = runningCalc.split()
    }
}

I want to have input of a string, and have it return a string Array with delimiters of { +-/*()%}.

In java you could do a string tokenizer to keep the delimiters in the returned string array.

How can I use the split or components method to separate my string input to a string array?

2 Answers

I personally wouldn't use a struct. It's more common to extend an existing type with additional functionality. So, you want the components of a String that includes delimiters? Sounds like you want to extend String. Since String conforms to StringProtocol I'd extend StringProtocol so that any other types that conform to StringProtocol get this new functionality for free:

StringProtocol already has a method called components(separatedBy:) which takes a CharacterSet (a set of characters) to delimit the receiver.

Let's implement a method similar to the existing StringProtocol method.

extension StringProtocol {
    func components(separatedByIncluding delims: CharacterSet) -> [String] {
        var components: [String] = []
        var component = ""

        for character in self {
            if String(character).rangeOfCharacter(from: delims) != nil {
                if !component.isEmpty {
                    components.append(component)
                }
                components.append(String(character))
                component = ""
            } else {
                component += [character]
            }
        }
        if !component.isEmpty {
            components.append(component)
        }

        return components
    }
}

The method loops over each Character and checks if it's a delimiter. If so, it adds a new String to the array consisting of the delimiter Character. If not, it constructs a substring consisting of non-delimiter Character values. The resulting array of delimiters and substrings are then returned.

Usage would be:

func testExample() throws {
    let delims = CharacterSet(charactersIn: "{ +-/*()%}")
    let text = "{abc def+ghi-jkl/mno"

    let components = text.components(separatedByIncluding: delims)

    print("================")
    print(components)
    print("================")
}

The result is:

================
["{", "abc", " ", "def", "+", "ghi", "-", "jkl", "/", "mno"]
================

AFAIK there is no native Swift split method that allows you to keep the delimiters. You will need to implement your own split method extending Collection. Special thanks for @MartinR and @Rob for the code review:

extension Collection {
    func splitAndKeep(
        maxSplits: Int = .max,
        omittingEmptySubsequences: Bool = true,
        whereSeparator isSeparator: (Element) throws -> Bool
    ) rethrows -> [SubSequence] {
        precondition(maxSplits >= 0, "maxSplits can not be negative")
        if isEmpty { return [] }
        var subsequences: [SubSequence] = []
        var lowerBound = startIndex
        func appendAndAdvance(with upperBound: Index) {
            let range = lowerBound..<upperBound
            if !omittingEmptySubsequences || !range.isEmpty {
                subsequences.append(self[range])
                lowerBound = upperBound
            }
        }
        while
            var upperBound = try self[lowerBound...].firstIndex(where: isSeparator),
            subsequences.count < maxSplits {
            appendAndAdvance(with: upperBound)
            if subsequences.count == maxSplits {
                break
            }
            formIndex(after: &upperBound)
            appendAndAdvance(with: upperBound)
        }
        appendAndAdvance(with: endIndex)
        return subsequences
    }
}

let string = "*(22+13)/"
let delimiters = Set<Character>("{+-/*()%}.")

let ss = string.splitAndKeep(whereSeparator: delimiters.contains)  // ["*", "(", "22", "+", "13", ")", "/"]
let ss1 = string.splitAndKeep(maxSplits: 0, whereSeparator: delimiters.contains)  // ["*(22+13)/"]
let ss2 = string.splitAndKeep(maxSplits: 1, whereSeparator: delimiters.contains)  // ["*", "(22+13)/"]
let ss3 = string.splitAndKeep(maxSplits: 2, whereSeparator: delimiters.contains)  // ["*", "(", "22+13)/"]

let ss4 = string.splitAndKeep(omittingEmptySubsequences: false, whereSeparator: delimiters.contains)  // ["", "*", "", "(", "22", "+", "13", ")", "", "/", ""]["", "*", "", "(", "22", "+", "13", ")", "", "/", ""]
let ss5 = string.splitAndKeep(maxSplits: 0, omittingEmptySubsequences: false, whereSeparator: delimiters.contains)  // ["*(22+13)/"]
let ss6 = string.splitAndKeep(maxSplits: 1, omittingEmptySubsequences: false, whereSeparator: delimiters.contains)  // ["", "*(22+13)/"]
let ss7 = string.splitAndKeep(maxSplits: 2, omittingEmptySubsequences: false, whereSeparator: delimiters.contains)  // ["", "*", "(22+13)/"]
Related