Swift: String firstIndex after character

Viewed 887

I'm trying to detect the parentheses on string for example: foo(bar)baz(blim) to and reverse the content inside of the parentheses but I'm getting out of bounce range on my implementation:

func reverseInParentheses(inputString: String) -> String {
    var tmpStr = inputString
    var done = false
    while !done {
        if let lastIndexOfChar = tmpStr.lastIndex(of: "(") {
            let startIndex = tmpStr.index(lastIndexOfChar, offsetBy:1)
            if let index = tmpStr.firstIndex(of: ")") {
                let range = startIndex..<index
                let strToVerse = String(tmpStr[range])
                let reversedStr = reverseStr(str: strToVerse)
                tmpStr = tmpStr.replacingOccurrences(of: "(" + strToVerse + ")", with: reversedStr)
                
            }
        } else {
            done = true
        }
    }
    return tmpStr
}

How can I get the tmpStr.firstIndex(of: ")") after the startIndex any of you knows how can do that?

3 Answers

how can I get the tmpStr.firstIndex(of: ")") after the startIndex?

One way to do this is to "cut" the string at startIndex, and get the second half. Then use firstIndex(of:) on the substring. Since Substrings are just "views" onto the original strings from which they are cut from, firstIndexOf still returns indices of the original string.

let string = "foo(bar)baz(blim)"
if let lastIndexOfChar = string.lastIndex(of: "(") {
    let startIndex = string.index(after: lastIndexOfChar)
    let substring = string[startIndex..<string.endIndex] // cut off the first part of the string.
    // now you have a "Substring" object
    if let indexAfterOpenBracket = substring.firstIndex(of: ")") {
        // prints "blim", showing that the index is indeed from the original string
        print(string[startIndex..<indexAfterOpenBracket])
    }
}

You can write this as an extension:

extension StringProtocol {
    func firstIndex(of char: Character, after index: Index) -> Index? {
        let substring = self[index..<endIndex]
        return substring.firstIndex(of: char)
    }
}

Now if you call tmpStr.firstIndex(of: ")", after: startIndex) in your reverseInParentheses, it should work.

You can iterate your string keeping an index as reference to compare it to the endIndex. So every time you successfully find a range you do a new search starting after the end index. Btw you should not use replacingOccurrences because it might replace words not inside parentheses as well. You can use RangeReplaceableCollection replaceSubrange and pass the reversed substring to that method.

To find the first index after character you can extend collection and return the index after the firstIndex of the element if found:

extension Collection where Element: Equatable {
    func firstIndex(after element: Element) -> Index? {
        guard let index = firstIndex(of: element) else { return nil }
        return self.index(after: index)
    }
}

Your method should look something like this:

func reverseInParentheses(inputString: String) -> String {
    var inputString = inputString
    var startIndex = inputString.startIndex
    while startIndex < inputString.endIndex,
        let start = inputString[startIndex...].firstIndex(after: "("),
        let end = inputString[start...].firstIndex(of: ")") {
        inputString.replaceSubrange(start..<end, with: inputString[start..<end].reversed())
        startIndex = inputString.index(after: end)
    }
    return inputString
}

let str = "foo(bar)baz(blim)"
reverseInParentheses(inputString: str)  // "foo(rab)baz(milb)"


Or extending StringProtocol and constraining Self to RangeReplaceableCollection:

extension StringProtocol where Self: RangeReplaceableCollection {
    var reversingSubstringsBetweenParentheses: Self {
        var startIndex = self.startIndex
        var source = self
        while startIndex < endIndex,
            let start = source[startIndex...].firstIndex(after: "("),
            let end = source[start...].firstIndex(of: ")") {
            source.replaceSubrange(start..<end, with: source[start..<end].reversed())
            startIndex = index(after: end)
        }
        return source
    }
}

let str = "foo(bar)baz(blim)"
str.reversingSubstringsBetweenParentheses  // "foo(rab)baz(milb)"

Updated answer of Leo Dabus in case there is written some more text after the last parentheses.

func reverseInParentheses(_ str: String) -> String {
    var str = str
    var startIndex = str.startIndex
    Var lastIndexOfChar = str.lastIndex(of: ")") ?? startIndex
    while startIndex < lastIndexOfChar {
        let start = str[startIndex...].firstIndex(after: "("),
        let end = str[start...].firstIndex(of: ")") {
        str.replaceSubrange(start..<end, with: str[start..<end].reversed())
        startIndex = str.index(after: end)
    }
    return str
}
Related