85

Kotlin deprecated the capitalize function on String class, and their suggested replacement is obnoxiously long. This is an example of a situation where they made the right call on deprecating it, but the wrong call on the user experience.

For example, this code:

val x = listOf("foo", "bar", "baz").map { it.capitalize() }

is "cleaned up" by the IDE to become:

val x = listOf("foo", "bar", "baz").map { it.replaceFirstChar {
                    if (it.isLowerCase()) it.titlecase(
                        Locale.getDefault()
                    ) else it.toString()
                } }

This is preeeeetty ugly. What can we do about it?

Stevey
  • 2,822
  • 1
  • 23
  • 30

8 Answers8

74

The suggested replacement is ugly because it needs to be equivalent to what capitalize() used to do:

  1. dependent on the default locale
  2. NOT converting an uppercase first char into titlecase (e.g. capitalize does NOT transform a leading 'DŽ' into 'Dž' - both are single characters here, try to select them)

If you didn't care too much about this behaviour, you can use a simpler expression using an invariant locale and unconditionally titlecasing the first character even if uppercase:

val x = listOf("foo", "bar", "baz").map { it.replaceFirstChar(Char::titlecase) }

This means that if the first character is uppercase like 'DŽ', it will be transformed into the titlecase variant 'Dž' anyway, while the original code wouldn't touch it. This might actually be desirable.

One of the reasons capitalize() has been deprecated is because the behaviour of the method was unclear. For instance:

  • behaviour #2 is pretty weird
  • not capitalizing words in a sentence might be unexpected (C# would titlecase every space-separated word)
  • not lowercasing other characters of the words might be unexpected as well

If you want to keep the exact current behaviour on purpose, but make it more convenient to use, you can always roll your own extension function with a name that suits you ("capitalize(d)" might not give enough info to the unaware reader):

fun String.titlecaseFirstCharIfItIsLowercase() = replaceFirstChar { 
    if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() 
}

Or for the version with invariant locale that titlecases the uppercase chars:

fun String.titlecaseFirstChar() = replaceFirstChar(Char::titlecase)
Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • 1
    Maybe slightly less verbose? `map { it.replaceFirstChar(Char::uppercase) }` – Adam Millerchip Jun 05 '21 at 03:19
  • @AdamMillerchip Good point, that's a neater way of avoiding the shadowing of `it`. I added that to my answer. – Joffrey Jun 05 '21 at 07:27
  • 1
    Note: the title case part is there in the suggested replacement for a reason. For example: the character dž in title case is Dž instead of the upper case DŽ. – Cristan Sep 02 '21 at 10:12
  • Yes, I clarified my answer to explain what exactly you're giving up. – Joffrey Sep 02 '21 at 10:44
  • This `if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()` is actually suggested in deprecation note of `capitalize()` and still not the right solution. Imagine this simple case `BAku` Kotlin's suggestion will leave it as is. However, capitalizing means making the word's "first character" uppercase and the rest lowercase. `capitalize()` was an ugly solution and their new suggestion is yet another ugly suggestion. TBH, would have been much better if they didn't suggest anything at all instead of a misleading solution – Farid Apr 13 '22 at 18:23
  • @Farid I'm not entirely sure which point you're making here. *`capitalize()` was an ugly solution* - a solution to what problem? `capitalize` was a stdlib function, and is now deprecated because its behaviour was unclear to many users (including you apparently). Yes the replacement I'm mentioning is the one suggested by the deprecation message, that's kinda the point of this answer: either change the behaviour to something simpler because it didn't make sense in the first place, or keep the exact same weird behaviour but wrap it in an extension – Joffrey Apr 14 '22 at 08:32
  • An ugly solution for cases like `BAku`, `appLE`. With `capitalize()` (from Kotlin), one would expect these words to be transformed to `Baku`, `Apple`. Instead, they would end up being `BAku`, `AppLE`, with the replacement they suggest the same thing will happen. This is what I was calling an ugly solution – Farid Apr 14 '22 at 09:28
  • @Farid ok. What you mention is exactly the reason why the Kotlin team deprecated this method. With such name, people may expect the behaviour you are describing, or even something else. The current behaviour of `capitalize()` could not be changed for compatibility reasons, so the best way forward is to deprecate and remove the method, and maybe later re-introduce something that match user expectations better. The suggested replacement with the deprecation message is meant to provide the exact same behaviour so people migrating don't have breaking changes, so that's totally expected. – Joffrey Apr 14 '22 at 09:33
34

A neat solution is to define a new extension function on String, which hides the gory details with a cleaner name:

/**
 * Replacement for Kotlin's deprecated `capitalize()` function.
 */
fun String.capitalized(): String {
    return this.replaceFirstChar { 
        if (it.isLowerCase())
            it.titlecase(Locale.getDefault())
        else it.toString() 
    }
}

Now your old code can look like this:

val x = listOf("foo", "bar", "baz").map { it.capitalized() }

You'll need to define the extension function at the top level in some package that you can import easily. For example, if you have a kotlin file called my.package.KotlinUtils (KotlinUtils.kt), and you put the definition inside it like so:

package my.package

fun String.capitalized(): String {...}

Then you can import it in your other packages with:

import my.package.capitalized
Stevey
  • 2,822
  • 1
  • 23
  • 30
  • I can see a big difference between the correct answer and this one. This must be the recommend answer for anyone whose looking for a right and appropriate usage of Kotlin pure functions. map function is an overkill for few strings to transform. it would increase the overhead performance. – Jack Aug 05 '21 at 04:15
  • 1
    Or just "fun String.capitalized(): String = this.replaceFirstChar { it.titlecase() }". Calling "titlecase" on a single char is... uhm... and interesting concept. Same calling "toString" on a char without a reason. – Lutosław Aug 24 '21 at 05:40
  • @Lutosław This is how the `titlecase()` function is designed (it's defined on `Char`): a single char can be capitalized with multiple characters, so `Char.titlecase()` returns a `String`. If you use this `if` to check for lowercase before performing any string transformation, you need an else branch that returns a `String`, so... you need `toString()`. It's not without a reason. – Joffrey Sep 02 '21 at 10:48
7
val fruits = listOf("baNana", "avocAdo", "apPle", "kiwifRuit")
fruits
    .filter { it.startsWith("a") }
    .sortedBy { it }
    .map { it.lowercase().replaceFirstChar(Char::uppercase) }
    .forEach { println(it) }

Output:

Apple
Avocado
TarekB
  • 757
  • 14
  • 20
5

You can call the replaceFirstChar function on the original string and pass the transform function as input. The transform function takes the first character and converts it to an uppercase character using the uppercase() function.

val list = listOf("foo", "bar", "baz") .map {
     it.replaceFirstChar { firstChar ->
          firstChar.uppercase()
     }
 }
println("List - > $list")

Output

List - > [Foo, Bar, Baz]
Vinod Kamble
  • 201
  • 3
  • 4
1

How about this?

fun main() {
    val x = listOf("foo", "bar", "baz").map { it[0].uppercase() + it.drop(1) }
    println(x)
}

Output:

[Foo, Bar, Baz]
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
1

I found a method trying to capitalize a string that came from the API and it apparently worked, found it in the Kotlin docs:

println("kotlin".replaceFirstChar { it.uppercase() }) // Kotlin

and use it like this in my code:

 binding.textDescriptions.text = "${it.Year} - ${it.Type.replaceFirstChar {it.uppercase()}}"
0

If you are not sure (maybe you receive Strings from an API) if the first letter is upper or lower case , you can use the below method;

var title = "myTitle"
title.replaceFirstChar {
        if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else 
        it.toString()
    }

New title will be "MyTitle"

Ahmet B.
  • 1,290
  • 10
  • 20
0

You can use this extension function to capitalize first characture of String

fun String.capitalize(): String {
        return this.replaceFirstChar {
            if (it.isLowerCase()) it.titlecase(Locale.getDefault())
            else it.toString()
        }
}

And call this method like

"abcd".capitalize()
Aminul Haque Aome
  • 2,261
  • 21
  • 34