Compared to a solution using the upper/lowerCased()
string methods,
your approach has some disadvantages:
It does not work for many letters in non-english alphabets:
print(solve("ćœßπĆŒΠ")) // çij¿ΠæIJ
It transforms non-letters as well, to some unexpected output:
print(solve("a b,c")) // A@BLC
Even if the code is only needed for ASCII letters ("A" ... "Z", "a" ... "z"),
it can still be improved.
Defining a global function instead of a closure variable increases the
legibility. You should also use a better function name, solve()
is pretty
non-descriptive:
func toggleCase(_ str: String) -> String {
// ...
}
An alternative would be an extension method:
extension String {
func caseToggled() -> String {
// ...
}
}
which resembles the existing upper/lowerCased()
methods.
A disadvantage is the possibility of name conflicts if some imported
framework happens to define the same extension method.
The use of "magic numbers" – like 32
and 96
in your code – should be
avoided. At least use a constant with an explaining comment:
let delta = 32 // Difference between upper case and lower case Unicode scalar values of ASCII letters.
Better, compute it from the actual values, which makes it self-explaining:
let delta = UnicodeScalar("a")!.value - UnicodeScalar("A")!.value
A switch-case statement allows to transform only the ASCII letters,
with a well-defined behavior for other characters.
Finally, instead of creating an intermediate [Character]
array,
you can append to the unicodeScalar
view of the result string
directly.
Putting it together, the function could look like this:
func toggleCase(_ str: String) -> String {
let delta = UnicodeScalar("a")!.value - UnicodeScalar("A")!.value
var result = ""
for ucs in str.unicodeScalars {
switch ucs {
case "A"..."Z":
result.unicodeScalars.append(UnicodeScalar(ucs.value + delta)!)
case "a"..."z":
result.unicodeScalars.append(UnicodeScalar(ucs.value - delta)!)
default:
result.unicodeScalars.append(ucs) // Leave unchanged
// Alternatively:
// break to ignore other characters
}
}
return result
}
Example:
print(toggleCase("AB c De")) // ab C dE
If you have to toggle the case of arbitrary letters (from any language) then
the upper/lowerCased()
string methods must be used, as there is no
simple "arithmetic operation" which does this transformation.
If performance is the first priority then you should work on the UTF-16 view
of the string, because that is what String
stores internally. (However, that
is an implementation detail and might change in the future.)
func toggleCase1(_ str: String) -> String {
var utf16Result: [UInt16] = []
utf16Result.reserveCapacity(str.utf16.count)
let upperCaseA = UInt16(65)
let upperCaseZ = UInt16(90)
let lowerCaseA = UInt16(97)
let lowerCaseZ = UInt16(122)
let delta = lowerCaseA - upperCaseA
for u in str.utf16 {
switch u {
case upperCaseA...upperCaseZ:
utf16Result.append(u + delta)
case lowerCaseA...lowerCaseZ:
utf16Result.append(u - delta)
default:
utf16Result.append(u)
}
}
return String(utf16CodeUnits: utf16Result, count: utf16Result.count)
}
Note how reserveCapacity()
is used to avoid array reallocations.
Performance comparison: The test was done on a MacBook (Retina, 12-inch, Early 2016, 1.2 GHz Intel Core m5 processor),
with the program compiled in Release configuration:
let str = String(repeating: "abcdefghijklmnopqrstuvwsyzABCDEFGHIJKLMNOPQRSTUVWXYZ", count: 100_000)
do {
let start = Date()
let _ = solve(str)
let end = Date()
print("solve: ", end.timeIntervalSince(start) * 1000)
}
do {
let start = Date()
let _ = toggleCase(str)
let end = Date()
print("toggleCase: ", end.timeIntervalSince(start) * 1000)
}
do {
let start = Date()
let _ = toggleCase1(str)
let end = Date()
print("toggleCase1:", end.timeIntervalSince(start) * 1000)
}
Result:
solve: 496.731996536255
toggleCase: 319.347023963928
toggleCase1: 189.152002334595