2
\$\begingroup\$

What is benefit of using uppercased() / lowercased() over the following approach ? (Other than making it more readable)

Any comments on perf ?

Can this be further improved ?

Example : for input : ABcDe, Expected Output is : abCdE

var solve = {(str: String) -> String in
    var arr = [Character]()
    for char in str.unicodeScalars {
        char.value > 96 ? arr.append(Character(UnicodeScalar(char.value - 32)!)) : arr.append(Character(UnicodeScalar(char.value + 32)!))
    }
    return String(arr)
}

solve("Nipun")
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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
\$\endgroup\$
8
  • \$\begingroup\$ BTW, 1.2GHz is only the sticker speed. Most of the time when you're actually doing anything, it will turbo much higher. The main exception would be if you actually tried to do some heavy-duty number crunching or video encoding that would heat your CPU up if it stayed at high turbo. (See this SO Q&A for more about Intel's very-low-power CPUs ). \$\endgroup\$ Oct 14, 2017 at 8:17
  • \$\begingroup\$ Anyway, unless you disabled turbo, it would make more sense to just give the model number than to say "1.2GHz core m5" because that could be Haswell or Skylake or whatever. \$\endgroup\$ Oct 14, 2017 at 8:17
  • \$\begingroup\$ And BTW, for the ASCII range, you can efficiently flip the case of upper or lowercase by XORing with 0x20. (ASCII is nicely designed so that the alphabet doesn't cross a 32-code-point alignment boundary). You can check for a byte being an alphabetic ASCII character by checking if x|0x20 is a between 'a' and 'z'. You can use that to SIMD vectorize upper-casing with SSE2 \$\endgroup\$ Oct 14, 2017 at 8:24
  • 1
    \$\begingroup\$ @PeterCordes: I am not a processor architecture expert. Would "MacBook (Retina, 12-inch, Early 2016), 1.2 GHz Intel Core m5" be more informative? Or do you mean the processor model number? I'll have to find out how to determine that :) – Anyway, my main point is the comparison of the various methods, I just wanted to give a rough idea how I tested. \$\endgroup\$
    – Martin R
    Oct 14, 2017 at 8:29
  • \$\begingroup\$ Related: case-flipping an ASCII string in x86 asm and vectorized C with intrinsics. For unicode, you could detect non-ASCII characters and handle them specially. (Sorry, I got kind of carried away when you mentioned performance and case-flipping, since those were fun answers. Not particularly relevant for Swift I'd guess :P I'm only here because this made the Hot Network Questions list) \$\endgroup\$ Oct 14, 2017 at 8:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.