🧊

RustでUTF-16単位の文字を扱う

あらすじとしては、

  • RustでJavaScriptのRegExpパーサーを書いてた
  • RegExpは、uvフラグがつくと、正規表現をUnicode単位で処理できるようになる
  • そうでない場合、UTF-16のコードユニット単位で処理する
/^.{1}$/.test('𠮷');  // false
/^.{1}$/u.test('𠮷'); // true
/^.{1}$/v.test('𠮷'); // true

これをRustでやるには・・・?という話。

RustのcharはUnicode scalar value

char - Rust https://doc.rust-lang.org/std/primitive.char.html

またの名を、Unicodeコードポイントとのこと。

Rustで文字といえばchars()ってことで、これをそのまま使った場合こうなる。

let s = "Hello, 👈🏻𠮷野家あっち/xyz";

let chars = s.chars().collect::<Vec<_>>();
println!("{chars:?}");

出力されるのは、['H', 'e', 'l', 'l', 'o', ',', ' ', '👈', '🏻', '𠮷', '野', '家', 'あ', 'っ', 'ち', '/', 'x', 'y', 'z']という文字たち。

EmojiのSkin toneとか国旗とかはバラけるけど、吉野家の”よし”はちゃんと土かんむりで1つになってる。

というわけで、Unicodeモードであるなら、charをこのまま使えばよさそう。 (複雑な絵文字を見たまま分割するのは、JSでもIntl.Segmenterとかそういう実装がないとできないし。)

そしてcharは内部的にu32そのままらしいので、場合によってはas u32して取り回すほうが便利かもしれない。

https://doc.rust-lang.org/std/primitive.char.html#method.from_u32

UTF-16単位

Unicodeモードじゃない場合は、encode_utf16()というAPIで変換してから扱う。

let s = "Hello, 👈🏻𠮷野家あっち/xyz";
let s = s.encode_utf16();

これでu16のイテレータが取得できる。

ただこっちの場合、ループしてる1つはサロゲートペアの一部であるかもしれず、もはやcharとして復元できる保証はなくなってしまう。

u16as u32に変換することはできるので、画一的に扱いたいならばそうする。

まとめ

fn main() {
    let s = "Hello, 👈🏻𠮷野家あっち/xyz";
    let count = 20;

    println!("--- char(= unicode scalar value) ---");
    // `chars()` returns a `char`, which is a Unicode scalar value, not a Unicode code point.
    // It can be converted to a `u32`(internally `u32`) to see the code point.
    for i in 0..=count {
        let cp = s.chars().nth(i).map(|c| c as u32);
        let ch = cp.and_then(char::from_u32); // Always `Some`
        println!("[{i}]: {:?} = {:?}", cp, ch);
    }

    println!("--- utf16 code unit ---");
    // `encode_utf16()` returns a `u16`, which is a UTF-16 code unit.
    // It can be converted to a `u32`(just extend) to see the code point.
    for i in 0..=count {
        let cp = s.encode_utf16().nth(i).map(|c| c as u32);
        let ch = cp.and_then(char::from_u32); // May be `None`
        println!("[{i}]: {:?} = {:?}", cp, ch);
    }
}

これを実行すると表示されるのはこれ。

--- char(= unicode scalar value) ---
[0]: Some(72) = Some('H')
[1]: Some(101) = Some('e')
[2]: Some(108) = Some('l')
[3]: Some(108) = Some('l')
[4]: Some(111) = Some('o')
[5]: Some(44) = Some(',')
[6]: Some(32) = Some(' ')
[7]: Some(128072) = Some('👈')
[8]: Some(127995) = Some('🏻')
[9]: Some(134071) = Some('𠮷')
[10]: Some(37326) = Some('野')
[11]: Some(23478) = Some('家')
[12]: Some(12354) = Some('あ')
[13]: Some(12387) = Some('っ')
[14]: Some(12385) = Some('ち')
[15]: Some(47) = Some('/')
[16]: Some(120) = Some('x')
[17]: Some(121) = Some('y')
[18]: Some(122) = Some('z')
[19]: None = None
[20]: None = None

--- utf16 code unit ---
[0]: Some(72) = Some('H')
[1]: Some(101) = Some('e')
[2]: Some(108) = Some('l')
[3]: Some(108) = Some('l')
[4]: Some(111) = Some('o')
[5]: Some(44) = Some(',')
[6]: Some(32) = Some(' ')
[7]: Some(55357) = None
[8]: Some(56392) = None
[9]: Some(55356) = None
[10]: Some(57339) = None
[11]: Some(55362) = None
[12]: Some(57271) = None
[13]: Some(37326) = Some('野')
[14]: Some(23478) = Some('家')
[15]: Some(12354) = Some('あ')
[16]: Some(12387) = Some('っ')
[17]: Some(12385) = Some('ち')
[18]: Some(47) = Some('/')
[19]: Some(120) = Some('x')
[20]: Some(121) = Some('y')

UTF-16のほうは、charに変換できてない場合があることがわかる。