グラフェムという単位について
Zero To Production In Rust本で Graphemeという言葉が出てきて知らんかったのでメモ
辞書的な意味Cambridgeより
the smallest unit in a system of writing a language that can express a difference in sound or meaning
音や意味の違いを表現できる、言語の表記体系における最小単位のこと
要は人間が認識できる1文字のことだと思われる.
コンピュータ上でのunicodeに依存した機械が認識する1文字と人間の認識する1文字の違いを明確にするために利用されるのだと理解した.
Graphemeは人間の認識する1文字を表現するために利用される
例: 👨👩👧👦: 人間にとっては1つ(1 grapheme), unicode point上では7 個
Unicode point, エンコーディング, グラフェムというレイヤーについて.
ChatGPTさんに質問
Unicodeとエンコーディング形式、人間が認識する文字、それと人間が認識しなくてもUnicode pointで表現されるもの(ZWJなど)の複数のレイヤーがあって混乱してきました. 整理して教えてください。
ChatGPT
おっしゃる通り、これらの概念は一見すると混乱することがあります。以下にそれぞれのレイヤーを詳しく説明します。
Unicodeコードポイント(Unicode Code Point):これは基本的にすべての文字や記号(及び一部の特殊な制御コード)に与えられた一意の数値のことです。これらの数値は全世界の文字をカバーし、ギリシャ文字、アラビア文字、絵文字など、どのような文字でも一意のコードポイントを持ちます。
エンコーディング:コードポイントは抽象的な概念であり、これをコンピューターで実際に表現・保存・伝送するためには、それをバイト列に変換する方法が必要です。これをエンコーディングと呼びます。UTF-8、UTF-16、UTF-32などは、Unicodeコードポイントをどのようにバイト列に変換するかを定義したものです。
グラフェム(Grapheme):これはユーザーが認識する最小の文字単位のことで、視覚的に一つの文字として認識されるものです。しかし、その内部的な表現(コードポイントやエンコーディング)は複数の要素から成ることがあります。例えば、「👨👩👧👦」(家族)は一つのグラフェムとして認識されますが、内部的には複数のコードポイント(👨、👩、👧、👦、そしてゼロ幅結合子)から成り立っています。
特殊な制御コード:Unicodeには、ZWJ(ゼロ幅結合子)のように、直接的には視覚的な表現を持たないが、周りの文字の表示に影響を与える特殊なコードポイントもあります。これらもまた、Unicodeの一部であり、エンコーディングの対象となります。
このようなレイヤーが存在し、それぞれが異なる役割を果たしながら、我々が見ている文字列の表現を実現しています。
Rustのコードでどのように文字列がカウントされているのか確かめてみる.
graphemeとして文字を扱うためのクレート.
cargo add unicode-segmentation # 文字列をUnicode準拠の方法で正確に分割するためのcrate
前提: RustではString型と&str型は、その内容がUTF-8でエンコードされているというルールがある.
UTF-8は1 ~ 4バイトなのでデータサイズが一定ではない.
#[test]
fn grapheme_test() {
// Add [dependencies] unicode-segmentation = "1" to Cargo.toml.
use unicode_segmentation::UnicodeSegmentation;
assert_eq!("あ".chars().count(), 1); // Unicodeスカラー値の数を数える
assert_eq!("あ".len(), 3); // byte長を測る.
assert_eq!("👨".chars().count(), 1);
assert_eq!("👨".len(), 4);
// ここまではUnicodeのスカラー値と人間が認識する字面上の数は一致している.
// 複数のコードポイントを組み合わせて表現される文字の例, 人間にとっては1文字でもUnicode point上は7
assert_eq!("👨👩👧👦".chars().count(), 7);
// (👨: 4byte + ZWJ: 3 byte + 👩: 4byte + ZWJ: 3 byte + 👧: 4 byte + ZWJ: 3 byte + 👦: 4 byte = 25 byte)
assert_eq!("👨👩👧👦".len(), 25);
assert_eq!("👨👩👧👦".graphemes(true).count(), 1);
}
字面上は同じでもユニコードスカラー値が異なる例
#[test]
fn test_zero_width_joiner() {
use unicode_segmentation::UnicodeSegmentation;
assert_eq!("👨".chars().count(), 1);
assert_eq!("👨".len(), 4);
// 以下字面上では同じに見えるが、実際には異なる例, zero width joiner(U+200D)を含む場合.
assert_eq!("👨".chars().count(), 2);
assert_eq!("👨".len(), 7);
// この字面上では分からない場合もgraphemes(true)を使うと文字数は人間の認識と同じ1とカウントする.
assert_eq!("👨".graphemes(true).count(), 1);
// \u{...}でUnicodeスカラー値を表現できる. 200D: Zero width joiner(ゼロ幅結合子)
assert_eq!("👨", "👨\u{200D}");
assert_eq!("\u{200D}".len(), 3);
assert_eq!("👨\u{200D}".graphemes(true).count(), 1);
}
コードポイント、グラフェム、UTF-8をいつ使うかというアドバイスの参考となるもの見つけた
When to use code points, graphemes, and UTF-8
Deciding when to use code points, graphemes, and UTF-8 can be nonobvious to say the least!
The way Roc organizes the Str module and supporting packages is designed to help answer this question. Every situation is different, but the following rules of thumb are typical:
Most often, using Str values along with helper functions like split, joinWith, and so on, is the best option.
If you are specifically implementing a parser, working in UTF-8 bytes is usually the best option. So functions like walkUtf8, toUtf8, and so on. (Note that single-quote literals produce number literals, so ASCII-range literals like 'a' gives an integer literal that works with a UTF-8 U8.)
If you are implementing a Unicode library like roc-lang/unicode, working in terms of code points will be unavoidable. Aside from basic readability considerations like \u(...) in string literals, if you have the option to avoid working in terms of code points, it is almost always correct to avoid them.
If it seems like a good idea to split a string into "characters" (graphemes), you should definitely stop and reconsider whether this is really the best design. Almost always, doing this is some combination of more error-prone or slower (usually both) than doing something else that does not require taking graphemes into consideration.
For this reason (among others), grapheme functions live in roc-lang/unicode rather than in Str. They are more niche than they seem, so they should not be reached for all the time!
DeepLでの訳
コードポイント、グラフェム、UTF-8をいつ使うか
コード・ポイント、graphemes、UTF-8をいつ使うかを決めるのは、控えめに言っても容易ではない!
RocがStrモジュールとサポート・パッケージを整理する方法は、この質問に答えるのを助けるように設計されている。すべての状況は異なるが、以下の経験則は典型的なものである:
ほとんどの場合、splitやjoinWithなどのヘルパー関数と一緒にStr値を使用するのが最適です。
パーサーを特別に実装する場合は、UTF-8バイトで作業するのが通常は最適です。そのため、walkUtf8やtoUtf8などの関数が必要です。(シングルクオートリテラルは数値リテラルを生成するので、'a'のようなASCII範囲のリテラルはUTF-8のU8で動作する整数リテラルを生成することに注意してください)。
roc-lang/unicodeのようなUnicodeライブラリを実装する場合、コードポイントでの作業は避けられません。文字列リテラルにおける \u(...) のような基本的な可読性の考慮は別として、コード・ポイントでの作業を避ける選択肢があるのなら、避けるのがほとんど常に正しいです。
文字列を「文字」(graphemes)に分割するのが良いアイデアだと思われる場合は、絶対に立ち止まって、これが本当に最良の設計であるかどうかを考え直すべきです。ほとんどの場合、このようなことをすると、字句を考慮する必要のない他のことをするよりも、エラーが起こりやすいか、遅いか(たいていはその両方)のどちらかの組み合わせになる。
このような理由から(他の理由もあるが)、grapheme関数はStrではなくroc-lang/unicodeにある。これらは見た目以上にニッチなものなので、常に手を伸ばすべきものではない!