できない.dev

文字列の長さを取得するには

文字列の長さを数える基本形を各言語で示す。
len や length が返すのが文字数ではなく、バイト数・UTF-16 の単位数・コードポイントの数のどれかであるという言語ごとの違いと、絵文字や国旗で値が食い違う点までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

s = "こんにちは"
print(len(s))                  # 5(コードポイントの数)
print(len(s.encode("utf-8")))  # 15(UTF-8 のバイト数)
print(len("😀"))               # 1(絵文字 1 つは 1 コードポイント)
print(len("🇯🇵"))              # 2(国旗は 2 つのコードポイントの組)
print(len("e\u0301"))          # 2(é を e と結合文字で表した場合)

len は文字列に含まれるコードポイントの数を返す。
バイト数が必要なときは encode してから len を取る。
国旗や結合文字は見た目が 1 文字でも複数のコードポイントでできているので、len の値と見た目の文字数は一致しない。

うまくいかない時: MySQL で「Incorrect string value」が出て絵文字を保存できない

JavaScript 実行確認済み

const s = "こんにちは";
console.log(s.length);                           // 5
console.log(new TextEncoder().encode(s).length); // 15(UTF-8 のバイト数)
console.log("😀".length);                        // 2(UTF-16 のサロゲートペア)
console.log([..."😀"].length);                   // 1(コードポイントの数)
 
const seg = new Intl.Segmenter("ja", { granularity: "grapheme" });
console.log([..."🇯🇵"].length);                  // 2(国旗は 2 つのコードポイント)
console.log([...seg.segment("🇯🇵")].length);     // 1(見た目の 1 文字)

String.length は UTF-16 の単位数を返すため、絵文字のようにサロゲートペアになる文字は 2 と数えられる。
コードポイントで数えるなら展開してから length を見て、見た目の 1 文字で数えるなら Intl.Segmenter を使う。
バイト数は TextEncoder で UTF-8 に変換して取る。

TypeScript 実行確認済み

const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
 
function countGraphemes(text: string): number {
  return [...segmenter.segment(text)].length;
}
 
const flag: string = "🇯🇵";
console.log(flag.length);          // 4(UTF-16 の単位)
console.log([...flag].length);     // 2(コードポイントの数)
console.log(countGraphemes(flag)); // 1(見た目の 1 文字)
console.log(countGraphemes("こんにちは")); // 5

実行時の振る舞いは JavaScript と同じで、差分は戻り値を number と型付けして関数に切り出している点だけである。
Intl.Segmenter は lib に es2022 以降を含めないと型が見つからないので、tsconfig の target か lib を確認する。

Go 実行確認済み

package main
 
import (
	"fmt"
	"unicode/utf8"
)
 
func main() {
	s := "こんにちは"
	fmt.Println(len(s))                       // 15(バイト数)
	fmt.Println(utf8.RuneCountInString(s))    // 5(文字数。rune の数)
	fmt.Println(len([]rune(s)))               // 5(rune に変換して数える)
	fmt.Println(len("😀"))                     // 4(絵文字は 4 バイト)
	fmt.Println(utf8.RuneCountInString("😀")) // 1
}

len(s) は文字数ではなくバイト数を返すため、日本語は 1 文字 3 バイトで数えられる。
文字数が必要なときは utf8.RuneCountInString を使う。
[]rune に変換しても数えられるが、変換のためにメモリを確保する分だけ無駄が出る。

うまくいかない時: MySQL で「Incorrect string value」が出て絵文字を保存できない

Rust 実行確認済み

fn main() {
    let s = "こんにちは";
    println!("{}", s.len());               // 15(UTF-8 のバイト数)
    println!("{}", s.chars().count());     // 5(Unicode スカラー値の数)
    println!("{}", "😀".len());             // 4(絵文字は 4 バイト)
    println!("{}", "😀".chars().count());   // 1
}

str::len は UTF-8 のバイト数を返し、文字数は chars().count() で求める。
chars().count() は先頭から走査するので文字列の長さに比例した時間がかかる。
見た目の 1 文字(書記素クラスタ)で数える機能は標準ライブラリに無く、unicode-segmentation クレートなどを使う。

うまくいかない時: MySQL で「Incorrect string value」が出て絵文字を保存できない

Java 実行確認済み

import java.nio.charset.StandardCharsets;
 
public class Main {
    public static void main(String[] args) {
        String s = "こんにちは";
        String emoji = "😀";
        System.out.println(s.length());                                // 5
        System.out.println(s.getBytes(StandardCharsets.UTF_8).length); // 15(UTF-8 のバイト数)
        System.out.println(emoji.length());                            // 2(UTF-16 の char 単位)
        System.out.println(emoji.codePointCount(0, emoji.length()));   // 1(コードポイントの数)
    }
}

String.length は UTF-16 の char 単位数を返すので、絵文字などの補助文字は 2 と数えられる。
コードポイントの数は codePointCount で求める。
バイト数は文字コードを明示して getBytes から取り、引数なしの getBytes は実行環境の既定の文字コードに依存するため避ける。

C# 実行確認済み

using System;
using System.Globalization;
using System.Linq;
using System.Text;
 
class Program {
    static void Main() {
        var s = "こんにちは";
        Console.WriteLine(s.Length);                             // 5
        Console.WriteLine(Encoding.UTF8.GetByteCount(s));        // 15(UTF-8 のバイト数)
        Console.WriteLine("😀".Length);                           // 2(UTF-16 の char 単位)
        Console.WriteLine("😀".EnumerateRunes().Count());         // 1(コードポイントの数)
        Console.WriteLine(new StringInfo("🇯🇵").LengthInTextElements); // 1(見た目の 1 文字)
    }
}

string.Length は UTF-16 の char 単位数を返すため、絵文字は 2 と数えられる。
コードポイントの数は EnumerateRunes で、見た目の 1 文字は StringInfo.LengthInTextElements で数える。
バイト数は Encoding.UTF8.GetByteCount で求める。

つまずき

「長さ」が何を数えるかは言語ごとに違う。
Python の len はコードポイントの数、Go と Rust の len はバイト数、JavaScript・Java・C# の length は UTF-16 の単位数を返す。
実行して比べると、「こんにちは」は Python・JavaScript・Java・C# では 5、Go と Rust の len では UTF-8 のバイト数である 15 になった。
「😀」はさらに分かれ、Python が 1、JavaScript・Java・C# が 2、Go・Rust が 4 を返した。
日本語の文字数を数えたいだけなら Go では utf8.RuneCountInString、Rust では chars().count() を選ぶ。

見た目の1文字と数値が一致しない

国旗の絵文字「🇯🇵」は見た目が 1 文字だが、2 つのコードポイントの組でできている。
実行すると Python の len と JavaScript のコードポイント展開はどちらも 2 を返し、Intl.Segmenter と C# の StringInfo は 1 を返した。
é を e と結合文字で表した場合も Python の len は 2 になる。
入力欄の「最大 N 文字」のような上限を数えるときは、バイト・コードポイント・見た目の 1 文字のどれで数えるかを先に決め、保存先の列長の単位と揃えておく。

この記事は役立ちましたか?