できない.dev

文字列の大文字と小文字を変換するには

文字列を大文字・小文字に変換する基本形を各言語で示す。
ß の扱い、トルコ語の i、大文字小文字を無視した比較など、言語とロケールによって結果が変わる点まで扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

s = "Hello, World"
print(s.upper())       # HELLO, WORLD
print(s.lower())       # hello, world
print(s.capitalize())  # Hello, world(先頭だけ大文字、残りは小文字)
print(s.title())       # Hello, World(単語の先頭を大文字)
 
print("straße".upper())                        # STRASSE(ß は 2 文字になる)
print("STRASSE".lower() == "straße".lower())   # False
print("STRASSE".casefold() == "straße".casefold())  # True(比較には casefold)

upper() と lower() で全体を、capitalize() で先頭だけを大文字にできる。
大文字小文字を無視して比較するときは lower() ではなく casefold() を使う。
ドイツ語の ß のような文字は lower() では変わらないが、casefold() なら ss に畳まれる。

JavaScript

const s = "Hello, World";
console.log(s.toUpperCase()); // HELLO, WORLD
console.log(s.toLowerCase()); // hello, world
console.log(s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()); // Hello, world
 
console.log("straße".toUpperCase()); // STRASSE(ß は 2 文字になる)
console.log("I".toLocaleLowerCase("tr")); // ı(トルコ語では点なしの i になる)
console.log("I".toLowerCase());           // i

toUpperCase() と toLowerCase() は文字列を返す非破壊のメソッドで、元の文字列は変わらない。
先頭だけ大文字にする専用メソッドは無いので charAt(0) と slice(1) で組み立てる。
トルコ語のように規則が違う言語向けには toLocaleLowerCase("tr") のようにロケールを渡す。

TypeScript

function capitalize(text: string): string {
  return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}
 
const s: string = "Hello, World";
console.log(s.toUpperCase()); // HELLO, WORLD
console.log(s.toLowerCase()); // hello, world
console.log(capitalize(s));   // Hello, world
 
console.log("straße".toUpperCase());      // STRASSE(ß は 2 文字になる)
console.log("I".toLocaleLowerCase("tr")); // ı(ロケール指定で結果が変わる)

実行時の振る舞いは JavaScript と同じで、先頭だけ大文字にする処理を string を受けて string を返す関数に切り出している。
toLocaleLowerCase にロケールを渡すと結果が変わるので、画面表示ではなくデータの正規化には引数なしの toLowerCase() を使う。

Go

package main
 
import (
	"fmt"
	"strings"
)
 
func main() {
	s := "Hello, World"
	fmt.Println(strings.ToUpper(s)) // HELLO, WORLD
	fmt.Println(strings.ToLower(s)) // hello, world
 
	// 先頭だけ大文字にする(先頭が ASCII の 1 バイトの文字である前提)
	fmt.Println(strings.ToUpper(s[:1]) + strings.ToLower(s[1:])) // Hello, world
 
	fmt.Println(strings.ToUpper("straße"))              // STRAßE(ß は変わらない)
	fmt.Println(strings.EqualFold("STRASSE", "straße")) // false
	fmt.Println(strings.EqualFold("Go", "GO"))          // true(大文字小文字を無視した比較)
}

strings.ToUpper と strings.ToLower で全体を変換する。
先頭だけを変えるには s[:1] と s[1:] に分けるが、これはバイト位置の切り出しなので先頭が ASCII の場合に限る。
strings.EqualFold は大文字小文字を無視して比較するが、ß と SS のような複数文字にまたがる対応までは見ない。

Rust

fn main() {
    let s = "Hello, World";
    println!("{}", s.to_uppercase()); // HELLO, WORLD
    println!("{}", s.to_lowercase()); // hello, world
 
    let mut chars = s.chars();
    let capitalized = match chars.next() {
        Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
        None => String::new(),
    };
    println!("{}", capitalized); // Hello, world
 
    println!("{}", "straße".to_uppercase()); // STRASSE(ß は 2 文字になる)
    println!("{}", "ABC".eq_ignore_ascii_case("abc")); // true(ASCII だけの比較)
}

to_uppercase() と to_lowercase() は新しい String を返し、ß は SS の 2 文字に展開される。
char の to_uppercase() はイテレータを返すので、先頭だけ大文字にするときは collect で文字列に戻す。
eq_ignore_ascii_case は ASCII の範囲だけを比べるため、日本語や欧文のアクセント付き文字には向かない。

Java

import java.util.Locale;
 
public class Main {
    public static void main(String[] args) {
        String s = "Hello, World";
        System.out.println(s.toUpperCase(Locale.ROOT)); // HELLO, WORLD
        System.out.println(s.toLowerCase(Locale.ROOT)); // hello, world
        System.out.println(s.substring(0, 1).toUpperCase(Locale.ROOT)
                + s.substring(1).toLowerCase(Locale.ROOT)); // Hello, world
 
        System.out.println("straße".toUpperCase(Locale.ROOT)); // STRASSE(ß は 2 文字になる)
        System.out.println("I".toLowerCase(Locale.ROOT));      // i
        System.out.println("I".toLowerCase(Locale.forLanguageTag("tr"))); // ı(引数なしだと実行環境のロケールで変わる)
    }
}

toUpperCase(Locale.ROOT) のように Locale を必ず渡す。
引数なしの toUpperCase() は実行環境の既定ロケールに従うので、トルコ語環境では "I".toLowerCase() が ı になり、ID やキーの比較が環境によって食い違う。

C#

using System;
using System.Globalization;
 
class Program {
    static void Main() {
        var s = "Hello, World";
        Console.WriteLine(s.ToUpperInvariant()); // HELLO, WORLD
        Console.WriteLine(s.ToLowerInvariant()); // hello, world
        Console.WriteLine(char.ToUpperInvariant(s[0]) + s.Substring(1).ToLowerInvariant()); // Hello, world
 
        Console.WriteLine("straße".ToUpperInvariant()); // STRAßE(ß は変わらない)
        Console.WriteLine("I".ToLower(new CultureInfo("tr-TR"))); // ı(カルチャで結果が変わる)
        Console.WriteLine(string.Equals("STRASSE", "straße", StringComparison.OrdinalIgnoreCase)); // False
        Console.WriteLine(string.Equals("Go", "GO", StringComparison.OrdinalIgnoreCase)); // True
    }
}

ToUpperInvariant() と ToLowerInvariant() はカルチャに依存しない変換で、内部キーや比較用の正規化に向く。
ToLower(new CultureInfo("tr-TR")) のようにカルチャを渡すと結果が変わる。
大文字小文字を無視した比較は StringComparison.OrdinalIgnoreCase を明示して書く。

つまずき

ASCII の英字だけなら全言語で同じ結果になるが、それ以外の文字で食い違う。
「straße」を大文字にすると、Python・JavaScript・Rust・Java は STRASSE と 2 文字に展開し、Go と C# は STRAßE のまま ß を残した。
実行して比べると、"STRASSE" と "straße" を大文字小文字無視で比べた結果も、Go の strings.EqualFold と C# の OrdinalIgnoreCase では false、Python の casefold() 同士では true になった。

文字数が変わりうるので、変換後の文字列を固定幅の列やインデックスに入れる処理では長さを前提にしない。

ロケールで結果が変わる

トルコ語では I を小文字にすると i ではなく点なしの ı になる。
JavaScript の toLocaleLowerCase("tr")、Java の Locale.forLanguageTag("tr") を渡した toLowerCase、C# の tr-TR カルチャで、いずれも ı を返した。

Java の引数なしの toLowerCase() と C# の ToLower() は実行環境のロケールに従うので、サーバーの設定によってキーの比較結果が変わる。
データの正規化や比較には、Java なら Locale.ROOT、C# なら Invariant 系のように、ロケールに依存しない形を明示する。
JavaScript の引数なしの toLowerCase() はロケールに依存しない。