Search code examples
swiftstringcamelcasing

Separating CamelCase string into space-separated words


I would like to separate a CamelCase string into space-separated words in a new string. Here is what I have so far:

var camelCaps: String {
    guard self.count > 0 else { return self }
    var newString: String = ""

    let uppercase = CharacterSet.uppercaseLetters
    let first = self.unicodeScalars.first!
    newString.append(Character(first))
    for scalar in self.unicodeScalars.dropFirst() {
        if uppercase.contains(scalar) {
            newString.append(" ")
        }
        let character = Character(scalar)
        newString.append(character)
    }

    return newString
}

let aCamelCaps = "aCamelCaps"
let camelCapped = aCamelCaps.camelCaps // Produce: "a Camel Caps"

let anotherCamelCaps = "ÄnotherCamelCaps"
let anotherCamelCapped = anotherCamelCaps.camelCaps // "Änother Camel Caps"

I'm inclined to suspect that this may not be the most efficient way to convert to space-separated words, if I call it in a tight loop, or 1000's of times. Are there more efficient ways to do this in Swift?

[Edit 1:] The solution I require should remain general for Unicode scalars, not specific to Roman ASCII "A..Z".

[Edit 2:] The solution should also skip the first letter, i.e. not prepend a space before the first letter.

[Edit 3:] Updated for Swift 4 syntax, and added caching of uppercaseLetters, which improves performance in very long strings and tight loops.


Solution

  • As far as I tested on my old MacBook, your code seems to be efficient enough for short strings:

    import Foundation
    
    extension String {
    
        var camelCaps: String {
            var newString: String = ""
    
            let upperCase = CharacterSet.uppercaseLetters
            for scalar in self.unicodeScalars {
                if upperCase.contains(scalar) {
                    newString.append(" ")
                }
                let character = Character(scalar)
                newString.append(character)
            }
    
            return newString
        }
    
        var camelCaps2: String {
            var newString: String = ""
    
            let upperCase = CharacterSet.uppercaseLetters
            var range = self.startIndex..<self.endIndex
            while let foundRange = self.rangeOfCharacter(from: upperCase,range: range) {
                newString += self.substring(with: range.lowerBound..<foundRange.lowerBound)
                newString += " "
                newString += self.substring(with: foundRange)
    
                range = foundRange.upperBound..<self.endIndex
            }
            newString += self.substring(with: range)
    
            return newString
        }
    
        var camelCaps3: String {
            struct My {
                static let regex = try! NSRegularExpression(pattern: "[A-Z]")
            }
            return My.regex.stringByReplacingMatches(in: self, range: NSRange(0..<self.utf16.count), withTemplate: " $0")
        }
    }
    let aCamelCaps = "aCamelCaps"
    
    assert(aCamelCaps.camelCaps == aCamelCaps.camelCaps2)
    assert(aCamelCaps.camelCaps == aCamelCaps.camelCaps3)
    
    let t0 = Date().timeIntervalSinceReferenceDate
    
    for _ in 0..<1_000_000 {
        let aCamelCaps = "aCamelCaps"
    
        let camelCapped = aCamelCaps.camelCaps
    }
    
    let t1 = Date().timeIntervalSinceReferenceDate
    print(t1-t0) //->4.78703999519348
    
    for _ in 0..<1_000_000 {
        let aCamelCaps = "aCamelCaps"
    
        let camelCapped = aCamelCaps.camelCaps2
    }
    
    let t2 = Date().timeIntervalSinceReferenceDate
    print(t2-t1) //->10.5831440091133
    
    for _ in 0..<1_000_000 {
        let aCamelCaps = "aCamelCaps"
    
        let camelCapped = aCamelCaps.camelCaps3
    }
    
    let t3 = Date().timeIntervalSinceReferenceDate
    print(t3-t2) //->14.2085000276566
    

    (Do not try to test the code above in the Playground. The numbers are taken from a single trial executed as a CommandLine app.)