1
\$\begingroup\$

I am learning swift and I came across this problem. Converting each start letter to capitalized form if it's lowercase.

func upperCaseFirstCharacter(str:String){
    let myArr = str.components(separatedBy: " ")
    var finalStr :String=""
    for word in myArr {
        let myStr = word.replacingCharacters(in: ...str.startIndex, with: word.first?.uppercased() ?? "")
        finalStr.append(myStr)
        if myArr.last != word{
            finalStr.append(" ")
        }
    }
    print(finalStr)
}
\$\endgroup\$
1
  • \$\begingroup\$ The current question title, which states your concerns about the code, applies to too many questions on this site to be useful. The site standard is for the title to simply state the task accomplished by the code. Please see How do I ask a good question?. \$\endgroup\$
    – BCdotWEB
    Commented Jan 8, 2021 at 14:53

3 Answers 3

3
\$\begingroup\$

There's certainly different ways to do this problem in Swift. Here's an example:

extension Comparable {
    func clamped(to limits: ClosedRange<Self>) -> Self {
        min(max(self, limits.lowerBound), limits.upperBound)
    }
}

extension Int {
    var nonNegative: Int {
        clamped(to: 0...Int.max)
    }
}

extension Collection {
    var tailLength: Int {
        (count - 1).nonNegative
    }

    var head: SubSequence { prefix(1) }
    var tail: SubSequence { suffix(tailLength) }
}

extension String {
    var uppercaseFirst: String {
        head.localizedUppercase + tail
    }
    
    var words: [String] {
        components(separatedBy: " ")
    }
    
    var uppercaseFirstWords: String {
        words
        .map(\.uppercaseFirst)
        .joined(separator: " ")
    }
    
}

let example = "upper case the   first  letters please"
print(example.uppercaseFirstWords)

Now is that shorter than your code? No. Is it better? It's definitely different, so why might we prefer one or the other?

One reason to avoid the for loop and mutation is that it's difficult for people to think about how the dynamic runtime behaviour is, when they just read the code, i.e. the magic vs the spell. Generally it's ok to begin with and then you end up drowning in mutation the more you build it up.

The for-loop imperative style has the focus on all the details about the how to do something, (the reader has to follow all that through when reading the code) where the functional style is about what to do. The detail can get in the way, like temporary variables: "let myArr =". Your brain has to work through how all the mutation will work, compare that with something like

words.map(\.uppercaseFirst).joined(separator: " ")

is more like the description of what is wanted - take the words in the string, uppercase their first letters, then join them up with spaces. Hopefully simple to read.

On the other hand the functional style has a learning curve to it, for loops are easier to get started with. But the state juggling can result in things like:

if myArr.last != word

Which I guess has to do with avoiding a dangling " " - though it's not clear, so time has to be spent to understand that and make sure it's right.

Another advantage would be breaking up the pieces into having more reusable parts, so for example having the 'words' computed property on String means you can reuse it for other things - perhaps it would be useful to have an array of words from a string, let's say to work with the last word, or to count the words:

example.words.count
example.words.suffix(1)
example.words.suffix(1).map(\.uppercaseFirst)
example.words.randomElement().map { $0 + "!" }

Be wary about writing functions that bake in a 'side-effect' like printing its result. That means if you want to use it as part of some other processing, you can't. Consider changing the function signature:

func upperCaseFirstCharacter(_ str: String) -> String

Think carefully about the function names too, does it uppercase the first character of the string passed in?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Note that var tail: SubSequence { dropFirst() } would be simpler and more efficient. \$\endgroup\$
    – Martin R
    Commented Jan 9, 2021 at 11:06
  • \$\begingroup\$ Thanks, and to OP: coding this as a Swift language-learning exercise is different to coding it in real life, for that @MartinR has the right answer about not reinventing the wheel - leveraging Foundation etc \$\endgroup\$
    – Shadowrun
    Commented Jan 9, 2021 at 12:25
4
\$\begingroup\$

A few observations on improving the code snippet, but using the same basic logic:

  1. If you split the string with components, you can use joined to put them back together.
  2. The ...str.startIndex syntax will crash if you supply a zero length string. I would suggest using prefix and dropFirst to simplify the code.
  3. When writing routines like this, I’d generally suggest using StringProtocol so that it works for both string and substrings. In this case, I would make it an extension of StringProtocol.
  4. This method of capitalizing the first character of every word (regardless of part of speech) is sometimes called “start case” or “proper case”. I would suggest renaming the method accordingly.

Thus:

extension StringProtocol {
    func startcased() -> String {
        components(separatedBy: " ")
            .map { $0.prefix(1).uppercased() + $0.dropFirst() }
            .joined(separator: " ")
    }
}

let string = "this is a test"
print(string.startcased())                                  // This Is A Test

If the in the intent was to achieve “title case”, we often wouldn't capitalize certain parts of speech, e.g., articles and conjunctions. The NaturalLanguage library can be used for more refined capitalization logic:

import NaturalLanguage

extension StringProtocol {
    func titlecased() -> String {
        var capitalizeWordRanges: [Range<String.Index>] = []

        var result = String(self)

        // find words other than conjunctions and determiners

        let tagger = NLTagger(tagSchemes: [.lexicalClass])
        tagger.string = result
        let options: NLTagger.Options = [.omitWhitespace, .joinContractions]
        var isStartOfSentence = true

        tagger.enumerateTags(in: startIndex..<endIndex, unit: .word, scheme: .lexicalClass, options: options) { tag, range in
            guard let tag = tag else { return true }

            print(tag.rawValue, self[range])

            switch tag {
            case .sentenceTerminator:               // see if we're potentially starting a new sentence
                isStartOfSentence = true
                return true

            case .punctuation, .openQuote, .closeQuote, .openParenthesis, .closeParenthesis:
                return true

            case .conjunction where self[range].count < 4,
                 .determiner where self[range].count < 4,
                 .preposition where self[range].count < 4:         // only capitalize conjunctions or articles at the start of a sentence
                if isStartOfSentence { fallthrough }

            default:
                capitalizeWordRanges.append(range)            // otherwise, add this to the array of ranges to t
            }

            isStartOfSentence = false

            return true
        }

        // loop through, doing replacements; do it in reverse order because sometimes the capitalized string isn't the same length, e.g. German ß -> SS

        for range in capitalizeWordRanges.reversed() {
            result.replaceSubrange(range.lowerBound...range.lowerBound, with: result[range.lowerBound].uppercased())
        }

        return result
    }
}

let string2 = "this is a test"
print(string2.titlecased())                                 // This Is a Test

let string3 = #"The sign said, "don't feed the animals.""#
print(string3.titlecased())                                 // The Sign Said, "Don't Feed the Animals."

let string4 = "¿donde está la biblioteca?"
print(string4.titlecased())                                 // ¿Donde Está la Biblioteca?

Note, enumerating through the words solves another problem, namely situations where words may not preceded immediately by a space, but rather, perhaps punctuation, such as the quotation marks before “don't” in string3, above. Just splitting and joining by spaces will only work in the simplest of scenarios. Also, some languages have punctuation at the start of a sentence, e.g. the Spanish “¡Ay, caramba!” or “¿Donde está la biblioteca?” If you iterate through the actual words, rather than relying on spaces, you solve that problem.

This admittedly is not perfect “title case” logic. E.g. it is taking care of the capitalizing of the first character, but not shifting the rest of the word to lowercase (e.g., if the input string was entirely in uppercase). But if you are only worried about the first letter, this is probably better than the naive start/proper capitalization routine. And if you want to implement a richer title case rendition, this routine might be a good starting point.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ There is also capitalized which apparently handles quotation marks and punctuation correctly, but has no special treatment of articles and conjunctions. \$\endgroup\$
    – Martin R
    Commented Jan 9, 2021 at 3:28
  • \$\begingroup\$ Well, now that is the right answer. I was sitting here all smug and happy with my simple solution that eliminated a lot of the kruft, and you politely posted the overlooked one-liner! You should post that as an answer! I’d upvote it. Sure, my “title case” solution is a fine over-engineered answer to a question that wasn’t asked, but capitalized is the correct answer to the original question, IMHO. \$\endgroup\$
    – Rob
    Commented Jan 9, 2021 at 4:41
2
\$\begingroup\$

Another bug

The detection whether to insert a separator or not

if myArr.last != word{
    finalStr.append(" ")
}

does not work if the string contains multiple instances of the last word:

let s = "foo bar foo"
upperCaseFirstCharacter(str: s) // FooBar Foo

You would have to check for the last index instead, or better, use map and joined as already suggested in the other answers.

Why reinvent the wheel?

If the intention is to change the initial character of each word to uppercase, and all remaining characters of a word to lowercase, then you can simply use capitalized from the Foundation framework:

import Foundation

let s = "foo bar FOO"
print(s.capitalized) // Foo Bar Foo

This also handles words surrounded by quotation marks or other punctuation correctly (examples taken from Rob's answer):

let s2 = #"The sign said, "don't feed the animals.""#
print(s2.capitalized) // The Sign Said, "Don't Feed The Animals."

let s3 = "¿donde está la biblioteca?"
print(s3.capitalized) // ¿Donde Está La Biblioteca?
\$\endgroup\$

Your Answer

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

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