はじめに
Rust に限らず、テキスト処理をする際に注意しなければならないのが「絵文字」です。
本記事では、Unicode
と UTF-8
の基礎と、Rust で絵文字を扱う際の注意点を解説します。
Unicode と UTF-8
Rust は文字コードとして Unicode、符号化方式として UTF-8 を採用しています。 一方で、JavaScript は符号化方式として UTF-16 を採用しています。
簡単に説明すると、
- 文字コードは、ひとつひとつの文字に識別番号を振るときのルール
- 符号化方式は、その識別番号をコンピュータのメモリ上に実際に配置するときのルール
です。
プログラム言語によって、文字コードや符号化方式が異なることに注意が必要です。
文字コード
文字コードは、コンピュータ上で文字を扱うために、文字に番号を割り振って定義したものです。ASCII
や Unicode
、Shift_JIS
などの種類があります。近年では Unicode が主流です。
Unicode は、世界中のあらゆる言語で使用されるすべての文字を、固有の識別番号で管理する国際規格です。この識別番号のことをコードポイントと呼びます。
例えば、a
のコードポイントは 16 進数表記で 61
です(10 進数表記: 97
)。
Rust で文字列を記述する際、通常の文字表現だけでなく、コードポイントを用いて記述する方法があります。\u{NNNNNN}
のように Unicode のコードポイントを直接記述する方法です。NNNNNN
には、表現したい文字のコードポイントを 16 進数で記述します。
例えば、"a"
と記述する代わりに "\u{0061}"
と記述することが可能です。
let a = "\u{0061}";
println!("{}", a);
符号化方式
符号化方式は、コードポイントをビット列に変換する方法です。コンピュータ上では、あらゆるデータが 2 種類の電気信号 ( 0
と 1
に対応 )で表現されています。そのため、コンピュータ上で文字を扱う際にはコードポイントをビット列に変換する必要があります。
符号化方式には ASCII
や UTF-8
、UTF-16
などの種類があります。(ASCII
は文字コードと符号化方式の両方を含む仕様です。)
UTF-16 はメモリ上で 1 文字を 16 ビット単位で表現する符号化方式です。UTF-16 はサロゲートペアという独自の仕組みに注意が必要です。Rust には UTF-16 はあまり関係ないので説明しませんが、詳しくは以下の記事で参照してください。
一方、Rust で採用されている UTF-8 はメモリ上で 1 文字を 1 ~ 4 バイトの列で表現する符号化方式です。1 文字あたりのバイト数は、コードポイントによって異なります。
メモリ上(1 ~ 4 バイト) | コードポイント | 範囲 |
---|---|---|
0xxxxxxx | 0bxxxxxxx | 0 ~ 0x7f |
110xxxxx 10yyyyyy | 0bxxxxxyyyyyy | 0x80 ~ 0x7ff |
1110xxxx 10yyyyyy 10zzzzzz | 0bxxxxyyyyyyzzzzzz | 0x800 ~ 0x7ffff |
11110xxx 10yyyyyy 10zzzzzz 10wwwwww | 0bxxxyyyyyyzzzzzzwwwwww | 0x10000 ~ 0x10ffff |
※ UTF-8 では 0xd800
から 0xdfff
までの範囲(サロゲートペア)と 0x10ffff
より大きい値のコードポイントは、ビット列に変換できません。
絵文字
絵文字(emoji)は、テキストメッセージや Web ページなどで広く使用されている小さな図形(例: 😡, 🐻❄️, 🇺🇸)です。これらは感情や動物、天気などを表現するために使われ、コミュニケーションを豊かにします。
絵文字の構造
絵文字は Unicode 標準に基づいて定義されており、単一コードポイントのものもあれば、複数のコードポイントを組み合わせたものも存在します。
-
単一コードポイントの絵文字: 😊 (
U+1F60A
) -
複数コードポイントの組み合わせ: 👩👩👧👦 (
U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
)
👩👩👧👦 は ZWJ (Zero Width Joiner) と呼ばれる特殊な文字 (U+200D) を用いて結合されています。ZWJ によって、独立した絵文字が一つの絵文字として見えるようになります。
基本的な絵文字の扱い
Rust では、絵文字は通常の &str
や char
として扱われます。
let emoji = "😊"; // 絵文字を文字列として定義
println!("Emoji: {}", emoji);
let char_emoji: char = '😊'; // 絵文字を文字として定義
println!("Emoji as char: {}", char_emoji);
// \u{NNNNNN} 形式で記述することも可能
let smile_emoji = "\u{1F60A}"; // 😊
char
型は絵文字のコードポイントも簡単に取得できます。
let emoji = '😊';
println!("Code point: U+{:X}", emoji as u32);
// 出力: Code point: U+1F60A
複数のコードポイントを組み合わせた絵文字(例: "👩❤️💋👩")は、Rust では文字列 &str
として扱われます。絵文字を構成する各コードポイントを確認するには、chars メソッドを使用します。
let emoji = "👩❤️💋👩";
println!("Emoji: {}", emoji);
for c in emoji.chars() {
println!("Code point: U+{:X}", c as u32);
}
// 出力:
// Code point: U+1F469
// Code point: U+200D
// Code point: U+2764
// Code point: U+200D
// Code point: U+1F48B
// Code point: U+200D
// Code point: U+1F469
Rust での絵文字処理
Rust では、絵文字を含む文字列を扱う際に以下の点に注意する必要があります。
文字数のカウント
Rust の len
メソッドはバイト数を返しますが、絵文字のように 1 文字が複数バイトで構成される場合、視覚的に正確な文字数ではありません。
let s = "😊";
println!("{}", s.len()); // 出力: 4 (バイト数)
println!("{}", s.chars().count()); // 出力: 1
let t = "👩❤️💋👩";
println!("{}", t.len()); // 出力: 27(バイト数)
println!("{}", t.chars().count()); // 出力: 8
chars().count()
で文字数を確認すると、単一コードポイントの絵文字では視覚的な文字数と一致します。
ZWJ
を使用した方法以外にも以下のような、複数のコードポイントを組み合わせた絵文字があります。
- 2 つの文字を並べて国旗を表現する
REGIONAL INDICATOR
(例: "🇺🇸" <- 🇺と🇸) - 肌の色を変更する
Emoji Modifier Fitzpatrick
(例: "🧒🏽")
どこまで正確に絵文字をカウントするかは、用途や要件によりますが、これらの絵文字を視覚的な文字数と一致させたい場合、unicode_segmentation
などの外部クレートを利用する必要があります。