rs.nkmk.me

Rustの文字列からn文字目の文字charを取得

Posted: | Tags: Rust, 文字列

Rustで文字列(&str, String)からn文字目の文字charを取得する方法について説明する。

文字ではなく部分文字列を取得したい場合は以下の記事を参照。

以下のサンプルコードでは、説明のため、型推論によって省略できるときも明示的に型注釈を記述している場合がある。また、簡単のためunwrapを使っている。必要であればその他のメソッドやmatchなどで処理すればよい。

文字列からn文字目の文字を取得: chars().nth()

&strStringは、ベクタVecや配列のように[n]でインデックス指定できない。

let s: &str = "😀👍abc";

// let c = s[1];
// error[E0277]: the type `str` cannot be indexed by `{integer}`
// note: you can use `.chars().nth()` or `.bytes().nth()`

コンパイラが教えてくれるように、.chars().nth()または.bytes().nth()を使う。

charsは文字charのイテレータ、bytesはバイトu8のイテレータを返す。

nthはイテレータのメソッドで、n番目の要素をOptionで返す。要素数を超える位置を指定するとNoneを返す。

したがって、n番目の文字charは以下のように取得できる。

let c: char = s.chars().nth(1).unwrap();
assert_eq!(c, '👍');

assert_eq!(s.chars().nth(100), None);

例は省略するが、文字列Stringでも同様にcharsメソッドが使用可能。

書記素クラスタを考慮: unicode-segmentation

charは書記素クラスタを考慮しないため、charsでは国旗の絵文字などが2文字に分割されてしまう。

It’s important to remember that char represents a Unicode Scalar Value, and might not match your idea of what a ‘character’ is. Iteration over grapheme clusters may be what you actually want. str::chars - Rust

let s: &str = "JAPAN🇯🇵";

assert_eq!(
    s.chars().collect::<Vec<char>>(),
    ['J', 'A', 'P', 'A', 'N', '🇯', '🇵']
);

let c: char = s.chars().nth(5).unwrap();
assert_eq!(c, '🇯');

書記素クラスタを考慮するにはunicode-segmentationを使う。

UnicodeSegmentationトレイトをインポートすると、&strStringから書記素のイテレータを返すgraphemesメソッドが使えるようになる。

use unicode_segmentation::UnicodeSegmentation;
let s: &str = "JAPAN🇯🇵";

assert_eq!(
    s.graphemes(true).collect::<Vec<&str>>(),
    ["J", "A", "P", "A", "N", "🇯🇵"]
);

let c_str: &str = s.graphemes(true).nth(5).unwrap();
assert_eq!(c_str, "🇯🇵");

let c_str: &str = s.graphemes(true).nth(2).unwrap();
assert_eq!(c_str, "P");

文字charではなく文字列スライス&strが返されるので注意。

なお、graphemesメソッドのis_extended引数はtrueにすることが推奨されている。

If is_extended is true, the iterator is over the extended grapheme clusters; otherwise, the iterator is over the legacy grapheme clusters. UAX#29 recommends extended grapheme cluster boundaries for general processing. unicode_segmentation::UnicodeSegmentation::graphemes - Rust

例は省略するが、文字列Stringからも同様にgraphemesメソッドが使える。

1バイト文字(ASCII文字)だけの場合: as_bytes().get()

文字列に含まれるのが1バイト文字(ASCII文字)だけの場合、そのままバイトu8として処理することもできる。

上述のように、コンパイラのメッセージでは.bytes().nth()が推奨されているが、Clippyでは.as_bytes().get()が推奨されている。

bytesはバイトのイテレータを返すのに対し、as_bytesはバイトのスライス&[u8]を返す。

さらに、[]だと範囲外の位置を指定した場合、実行時にパニックになるが、getOptionを返し、範囲外だとNoneとなる。

let s: &str = "xyz<123>";

let b: &u8 = s.as_bytes().get(1).unwrap();
assert_eq!(*b, 121);
assert_eq!(*b as char, 'y');

assert_eq!(s.as_bytes().get(100), None);

上の例における121(= 0x79)はyのASCIIコード。getが返すのは&u8で、charに変換するときなどは*を付ける必要があるので注意。

例は省略するが、文字列Stringからも同様にas_bytesメソッドが使える。

同じ文字列から繰り返し文字を取得する場合

同じ文字列から複数回に渡って文字を取得する場合を考える。

上述のように1バイト文字だけの場合はas_bytesでバイトのスライス&[u8]に変換できるので、それを繰り返し利用できる。

let s: &str = "xyz<123>";

let bytes: &[u8] = s.as_bytes();
assert_eq!(*bytes.get(2).unwrap() as char, 'z');
assert_eq!(*bytes.get(5).unwrap() as char, '2');

chars().nth()graphemes().nth()の場合、繰り返すと毎回イテレータを生成して最初から反復することになる。特に気にならない程度であれば問題ないが、より効率的には以下の2通りの方法も考えられる。

イテレータを再利用

charsgraphemesが返すイテレータを保持して再利用する。ただし、nthはイテレータを進めていくので、先頭から順に文字を取得していく必要がある。前に戻ることはできない

let s: &str = "😀👍abc";
let mut chars = s.chars();

let pos1 = 1;
let pos2 = 3;

assert_eq!(chars.nth(pos1).unwrap(), '👍');
assert_eq!(chars.nth(pos2 - pos1 - 1).unwrap(), 'b');

例はcharsだがgraphemesでも同様。graphemesの場合、charではなく&strが返されるので注意。

collectでベクタに変換

collectでベクタVecに変換する方法もある。

ベクタに変換してしまえば、[]getで要素を取得できる。

let s: &str = "😀👍abc";
let v: Vec<char> = s.chars().collect();

assert_eq!(v[1], '👍');
assert_eq!(v[3], 'b');

assert_eq!(*v.get(1).unwrap(), '👍');
assert_eq!(*v.get(3).unwrap(), 'b');
assert_eq!(v.get(100), None);

イテレータのように、前から順に文字を取得しなければならないという制約はない。ただし、新たにベクタを生成するので、その分のメモリが余計に確保されるというデメリットがある。

例はcharsだがgraphemesでも同様。graphemesの場合はVec<&str>に変換する。

関連カテゴリー

関連記事