34

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.

Brian Arnold
  • 353
  • 1
  • 4
  • 10
  • 1
    Single line `return unicodeScalars.reduce("") { CharacterSet.uppercaseLetters.contains($1) ? $0 + " " + String($1) : $0 + String($1)}` – Leo Dabus Dec 23 '16 at 07:49
  • How to deal with string with consecutive capital characters? for eg: with above code "upperCased LETTERS" is returned as "upper Cased L E T T E R S". While the expected output is "upper Cased Letters". – Frankenxtein Sep 01 '19 at 17:14
  • @Frankenxtein Simply check the string being created, `$0` in our case, and see if the last letter is uppercase also. If yes, you just add the character, `$1`, with no space. – Natanel Sep 13 '19 at 02:13

13 Answers13

28
extension String {
    func camelCaseToWords() -> String {
        return unicodeScalars.dropFirst().reduce(String(prefix(1))) {
            return CharacterSet.uppercaseLetters.contains($1)
                ? $0 + " " + String($1)
                : $0 + String($1)
        }
    }
}
print("ÄnotherCamelCaps".camelCaseToWords()) // Änother Camel Caps

May be helpful for someone :)

Jano
  • 62,815
  • 21
  • 164
  • 192
Augustine P A
  • 5,008
  • 3
  • 35
  • 39
21

One Line Solution

I concur with @aircraft, regular expressions can solve this problem in one LOC!

// Swift 5 (and probably 4?)
extension String {
    func titleCase() -> String {
        return self
            .replacingOccurrences(of: "([A-Z])",
                                  with: " $1",
                                  options: .regularExpression,
                                  range: range(of: self))
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .capitalized // If input is in llamaCase
    }
}

Props to this JS answer.

P.S. I have a gist for snake_case → CamelCase here.

P.P.S. I updated this for New Swift (currently 5.1), then saw @busta's answer, and swapped out my startIndex..<endIndex for his range(of: self). Credit where it's due y'all!

AmitaiB
  • 1,656
  • 1
  • 19
  • 19
  • 5
    Thank you, I appreciate this answer, and perhaps it will be useful to some. However, my goal wasn't to implement one line of code, but to find a more efficient, maintainable implementation. The regex solutions run slower than the other solutions provided above, and the fastest solution requires more lines of code. – Brian Arnold May 15 '18 at 13:56
12

I might be late but I want to share a little improvement to Augustine P A answer or Leo Dabus comment.
Basically, that code won't work properly if we are using upper camel case notation (like "DuckDuckGo") because it will add a space at the beginning of the string.
To address this issue, this is a slightly modified version of the code, using Swift 3.x, and it's compatible with both upper and lower came case:

extension String {

    func camelCaseToWords() -> String {
        return unicodeScalars.reduce("") {
            if CharacterSet.uppercaseLetters.contains($1) {
                if $0.count > 0 {
                    return ($0 + " " + String($1))
                }
            }
            return $0 + String($1)
        }
    }
}
David Villegas
  • 458
  • 3
  • 18
matteodv
  • 3,992
  • 5
  • 39
  • 73
12

a better full swifty solution... based on AmitaiB answer

extension String {
    func titlecased() -> String {
        return self.replacingOccurrences(of: "([A-Z])", with: " $1", options: .regularExpression, range: self.range(of: self))
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .capitalized
    }
}
busta117
  • 526
  • 4
  • 8
10

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.)

OOPer
  • 47,149
  • 6
  • 107
  • 142
  • The last method, camelCaps3, appears to be the fastest because it caches the regular expression in a static. However, it does not handle non-ASCII capital letters (e.g., Ä). – Brian Arnold Jan 02 '17 at 20:54
  • @BrianArnold, this article's main intension is to show that using regex is not so fast as expected. If it was faster than your code, it could be improved to include non-ASCII capital letters. For example, you can use `"\\p{Lu}"` instead of `"[A-Z]"`. Please try by yourself. – OOPer Jan 02 '17 at 21:13
  • Thanks, using `"\\p{Lu}"`for the regex is more general, and when I compile this as a command line tool for optimal performance, regex is indeed not so fast as expected. Oddly, the optimized single line suggestion by Leo Daubus is also not so fast as expected when compiled as a command line tool. So, I am left with keeping the code I suggested, as it is fast enough and readable. – Brian Arnold Jan 04 '17 at 20:13
6
extension String {
    func titlecased() -> String {
        return self
            .replacingOccurrences(of: "([a-z])([A-Z](?=[A-Z])[a-z]*)", with: "$1 $2", options: .regularExpression)
            .replacingOccurrences(of: "([A-Z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression)
            .replacingOccurrences(of: "([a-z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression)
            .replacingOccurrences(of: "([a-z])([A-Z][a-z])", with: "$1 $2", options: .regularExpression)
    }
}

In

 "ThisStringHasNoSpacesButItDoesHaveCapitals"
 "IAmNotAGoat"
 "LOLThatsHilarious!"
 "ThisIsASMSMessage"

Out

"This String Has No Spaces But It Does Have Capitals" 
"I Am Not A Goat" 
"LOL Thats Hilarious!" 
"This Is ASMS Message" // (Difficult tohandle single letter words when they are next to acronyms.)

enter link description here

小洋粉
  • 71
  • 1
  • 2
  • This should be the accepted answer. This worked perfectly for what I was looking for as it handles capitalized letters that should stay together, such as "LOL". – szady Nov 26 '21 at 04:31
5

I can do this extension in less lines of code (and without a CharacterSet), but yes, you basically have to enumerate each String if you want to insert spaces in front of capital letters.

extension String {
    var differentCamelCaps: String {
        var newString: String = ""
        for eachCharacter in self {
            if "A"..."Z" ~= eachCharacter {
                newString.append(" ")
            }
            newString.append(eachCharacter)
        }
        return newString
    }
}

print("ÄnotherCamelCaps".differentCamelCaps) // Änother Camel Caps
Jano
  • 62,815
  • 21
  • 164
  • 192
Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
3

Swift 5 solution

extension String {

    func camelCaseToWords() -> String {
        return unicodeScalars.reduce("") {
            if CharacterSet.uppercaseLetters.contains($1) {
                if $0.count > 0 {
                    return ($0 + " " + String($1))
                }
            }
            return $0 + String($1)
        }
    }
}
Deepak
  • 724
  • 4
  • 13
2

If you want to make it more efficient, you can use Regular Expressions.

 extension String {
    func replace(regex: NSRegularExpression, with replacer: (_ match:String)->String) -> String {
    let str = self as NSString
    let ret = str.mutableCopy() as! NSMutableString

    let matches = regex.matches(in: str as String, options: [], range: NSMakeRange(0, str.length))
    for match in matches.reversed() {
        let original = str.substring(with: match.range)
        let replacement = replacer(original)
        ret.replaceCharacters(in: match.range, with: replacement)
    }
        return ret as String
    }
}

let camelCaps = "aCamelCaps"  // there are 3 Capital character
let pattern = "[A-Z]"
let regular = try!NSRegularExpression(pattern: pattern)
let camelCapped:String = camelCaps.replace(regex: regular) { " \($0)" }
print("Uppercase characters replaced: \(camelCapped)")
aircraft
  • 25,146
  • 28
  • 91
  • 166
2

Swift 5+

Small style improvements on previous answers

import Foundation

extension String {
    func camelCaseToWords() -> String {
        unicodeScalars.reduce("") {
            guard CharacterSet.uppercaseLetters.contains($1),
                  $0.count > 0
            else { return $0 + String($1) }
            return ($0 + " " + String($1))
        }
    }
}

Using guard let statements is usually recommended, as they provide an "early exit" for non matching cases and decrease the overall nesting levels of your code (which usually improves readability quite a lot... and remember, readability counts!)

1

Here's what I came up with using Unicode character classes: (Swift 5)

extension String {
    var titleCased: String {
        self
            .replacingOccurrences(of: "(\\p{UppercaseLetter}\\p{LowercaseLetter}|\\p{UppercaseLetter}+(?=\\p{UppercaseLetter}))",
                                  with: " $1",
                                  options: .regularExpression,
                                  range: range(of: self)
            )
            .capitalized
    }
}

Output:

fillPath                ➝ Fill Path
ThisStringHasNoSpaces   ➝ This String Has No Spaces
IAmNotAGoat             ➝ I Am Not A Goat
LOLThatsHilarious!      ➝ Lol Thats Hilarious!
ThisIsASMSMessage       ➝ This Is Asms Message
nielsbot
  • 15,922
  • 4
  • 48
  • 73
0

Swift way:

extension String {
    var titlecased: String {
        map { ($0.isUppercase ? " " : "") + String($0) }
            .joined(separator: "")
            .trimmingCharacters(in: .whitespaces)
    }
}
Artem Sydorenko
  • 353
  • 4
  • 8
0

Solution with REGEX

let camelCase = "SomeATMInTheShop"
let regexPattern = "[A-Z-_&](?=[a-z0-9]+)|[A-Z-_&]+(?![a-z0-9])"
let newValue = camelCase.replacingOccurrences(of: regexPattern, with: " $0", options: .regularExpression, range: nil)

Otuput ==> Some ATM In The Shop

Mixorok
  • 125
  • 9