☄️

Goで大文字小文字の変換処理を学ぶ

に公開

Goのstrings.ToLowerの処理を参考にどうやって大文字・小文字の変換処理をしているのか整理してみる。

https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/strings/strings.go;l=727

結論

最初に結論から言うと、ASCII範囲内の場合とASCII範囲外の場合で大文字・小文字の変換処理が変わるような実装されている。

  • ASCII範囲内 = ASCIIコード番号を使う
    • 以下番号を用いて処理をする
      • 65 ~ 90 = A ~ Z
      • 97 ~ 122 = a ~ z
  • ASCII範囲外 = Unicodeコードポイントを使う
    • あらかじめ定義されている通りに変換する

ASCII範囲内の場合

ASCII範囲内だけであれば簡単に大文字小文字判定が可能。
シングルクォートで囲ったruneリテラルを使う必要がある点に注意('A'や'Z'のこと)

if 'A' <= c && c <= 'Z' {//大文字判定処理}

上記は以下と同等であるため大文字判定が可能。

if rune(65) <= c && c <= rune(90) {}
  • runeとstring, byteの関係

    // 文字列リテラル -> rune文字列
    r := []rune("A")[0]
    
    // 最初からruneな文字列(シングルクォート)
    if 'A' <= c && c <= 'Z' {}
    // 上記は以下と同等
    // if rune(65) <= c && c <= rune(90) {}
    
    // byte型と比較可能
    // 整数値の比較時はint32の方に勝手に変換されるため比較可能
    if byte(65) == rune(65) {//true}
    // rune = int32
    // byte = uint8
    

ASCII範囲外の場合

大文字の”A”などはASCII範囲外なので、上記のようにして大文字判定ができない。

この場合、unicodeパスに従って変換を行う。このあたりのマッピングは unicode/tables.go などで行われている。

https://go.dev/src/unicode/tables.go

以下のような形式でマッピングされている。

{Lo: 0x0100, Hi: 0x0100, Delta: 1} // Ā(0x0100) → ā(0x0101)
  • Lo,Hi = 変換前の単一文字

  • Delta = 変換するために加算する必要のある整数値

  • つまりāはĀのUnicodeコードポイントに+1した値である。ここを見てコードポイントに加算した結果を返す。

    var lower rune = r + delta
    

実際の小文字変換処理を追ってみる(strings.ToLowerメソッド)

https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/strings/strings.go;l=727

func ToLower(s string) string {
	isASCII, hasUpper := true, false
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c >= utf8.RuneSelf {
			// 1.一文字ずつloopしてASCII範囲である128以下かどうかを確認
			isASCII = false
			break
		}
		// 2.大文字が一つでもあればhasUpperはtrueとなる
		hasUpper = hasUpper || ('A' <= c && c <= 'Z')
	}

	// 3.ASCII範囲であれば以下変換処理を実施
	if isASCII { // optimize for ASCII-only strings.
		if !hasUpper {
			// 4.大文字が一つもなければreturn
			return s
		}
		var (
			b   Builder
			pos int
		)
		// 5.Growを使って事前に変換対象の文字列サイズ分だけあらかじめメモリを確保する
		// ※Goの文字列はImmutableなので結合のたびに新しい領域を確保して結合後の内容でコピーする。そのため以下方法を用いない場合都度メモリが確保されて効率が悪い
		b.Grow(len(s))
		for i := 0; i < len(s); i++ {
			c := s[i]
			if 'A' <= c && c <= 'Z' {
				// 6.小文字変換処理
				// 例:c=Aだったのが+32することでaとなる
				c += 'a' - 'A'
				// 7.元々小文字の範囲はまとめて書き込み
				// pos=小文字範囲のスタート位置となる
				if pos < i {
					b.WriteString(s[pos:i])
				}
				// 8.変換した小文字を書き込み
				b.WriteByte(c)
				// 9.小文字範囲のスタート位置を現在のloop位置にリセット
				pos = i + 1
			}
		}
		// 10."ABCdefg"のような小文字で終了した場合に後ろの部分を書き込み
		if pos < len(s) {
			b.WriteString(s[pos:])
		}
		return b.String()
	}
	// 11.ASCII範囲外の場合は、定義してあるUnicodeコードポイントのマッピング通りに変換
	return Map(unicode.ToLower, s)
}

学びポイント

  • b.Growなどを使うと文字列結合のメモリ確保を最適化できる
  • pos変数にて変換の必要がないものは一気に書き込む工夫がされている

Discussion