Rustで文字列の位置を指定して部分文字列を取得
Rustで文字列(&str
, String
)の位置を指定して部分文字列を取得する方法について説明する。
部分文字列ではなくn文字目の文字char
を取得したい場合は以下の記事を参照。
- 関連記事: Rustの文字列からn文字目の文字charを取得
以下のサンプルコードでは、説明のため、型推論によって省略できるときも明示的に型注釈を記述している場合がある。また、簡単のためunwrap
を使っている。必要であればその他のメソッドやmatch
などで処理すればよい。
1バイト文字(ASCII文字)だけの文字列: スライス
1バイト文字(ASCII文字)だけの文字列の部分文字列はスライスで取得できる。
文字列スライス&str
の場合。
let s: &str = "abcde";
let s_sub: &str = &s[1..4];
assert_eq!(s_sub, "bcd");
文字列String
の場合も同様。結果をString
に変換したい場合はString::from
などを使う。
- 関連記事: Rustで&strとStringを相互に変換
let s: String = String::from("abcde");
let s_sub: &str = &s[1..4];
assert_eq!(s_sub, "bcd");
let s_sub: String = String::from(&s[1..4]);
assert_eq!(s_sub, "bcd");
なお、[]
でスライスを取得する場合、範囲外を指定すると実行時にパニックになる。Option
を返すget
を使うと、範囲外が指定された場合にNone
として処理できる。
let s: &str = "abcde";
// let s_sub: &str = &s[1..100];
// thread 'main' panicked at 'byte index 100 is out of bounds of `abcde`'
assert_eq!(s.get(1..4).unwrap(), "bcd");
assert_eq!(s.get(1..100), None);
スライスの範囲指定については以下の記事を参照。..
ではなく..=
も使える。start
, end
を省略することも可能。
マルチバイト文字を含む文字列: char_indicesを利用
スライスで指定するのはバイトの位置なので、マルチバイト文字を含む文字列では文字の位置と異なる。文字の境目ではないバイト位置を指定すると実行時にパニックが発生する。
let s: &str = "😀👉💯👈👍";
// let s_sub: &str = &s[1..4];
// thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '😀' (bytes 0..4) of `😀👉💯👈👍`
文字の境目のバイト位置を取得するにはchar_indices
を使う。
char_indices
は、文字char
とその文字が始まるバイト位置のタプルのイテレータを返す。順番は(位置、文字)
。
assert_eq!(
s.char_indices().collect::<Vec<(usize, char)>>(),
[(0, '😀'), (4, '👉'), (8, '💯'), (12, '👈'), (16, '👍')]
);
イテレータのn
番目の要素はnth
で取得できるので、n
文字目のバイト位置は以下のように取得できる。
let byte_position = s.char_indices().nth(3).unwrap().0;
assert_eq!(byte_position, 12);
バイト位置が分かれば、スライスで部分文字列を取得できる。
let byte_start = s.char_indices().nth(1).unwrap().0;
let byte_end = s.char_indices().nth(4).unwrap().0;
let s_sub: &str = &s[byte_start..byte_end];
assert_eq!(s_sub, "👉💯👈");
関数にした例は以下の通り。
start
とend
を引数にするとstart > end
などの条件が面倒なので、start
と部分文字列の長さ(文字数)length
を引数にしている。nth
はイテレータを消費していくことに注意。
fn substring(s: &str, start: usize, length: usize) -> &str {
if length == 0 {
return "";
}
let mut ci = s.char_indices();
let byte_start = match ci.nth(start) {
Some(x) => x.0,
None => return "",
};
match ci.nth(length - 1) {
Some(x) => &s[byte_start..x.0],
None => &s[byte_start..],
}
}
以下のように使える。String
の場合は参照を渡す。
let s: &str = "😀👉💯👈👍";
let s_sub: &str = substring(s, 1, 3);
assert_eq!(s_sub, "👉💯👈");
let s: String = String::from("😀👉💯👈👍");
let s_sub: &str = substring(&s, 1, 3);
assert_eq!(s_sub, "👉💯👈");
この例では、start
が範囲外の場合は空文字列""
、部分文字列の終わりが範囲外になる場合は元の文字列の最後までを返すようにしている。
let s: &str = "😀👉💯👈👍";
assert_eq!(substring(s, 1, 0), "");
assert_eq!(substring(s, 100, 3), "");
assert_eq!(substring(s, 1, 100), "👉💯👈👍");
範囲外が指定されたときはNone
を返すようにするには返り値の型をOption
にすればよい。
fn substring_option(s: &str, start: usize, length: usize) -> Option<&str> {
if length == 0 {
return Some("");
}
let mut ci = s.char_indices();
let byte_start = match ci.nth(start) {
Some(x) => x.0,
None => return None,
};
match ci.nth(length - 1) {
Some(x) => Some(&s[byte_start..x.0]),
None => None,
}
}
let s: &str = "😀👉💯👈👍";
assert_eq!(substring_option(s, 1, 0).unwrap(), "");
assert_eq!(substring_option(s, 100, 3), None);
assert_eq!(substring_option(s, 1, 100), None);
書記素クラスタを考慮: unicode-segmentation
char
は書記素クラスタを考慮しないため、char_indices
では国旗の絵文字などが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 = "🇯🇵JP😀";
assert_eq!(
s.char_indices().collect::<Vec<(usize, char)>>(),
[(0, '🇯'), (4, '🇵'), (8, 'J'), (9, 'P'), (10, '😀')]
);
書記素クラスタを考慮するにはunicode-segmentationを使う。
UnicodeSegmentation
トレイトをインポートすると、&str
やString
からchar_indices
の書記素版であるgrapheme_indices
メソッドが使えるようになる。
use unicode_segmentation::UnicodeSegmentation;
let s: &str = "🇯🇵JP😀";
assert_eq!(
s.grapheme_indices(true).collect::<Vec<(usize, &str)>>(),
[(0, "🇯🇵"), (8, "J"), (9, "P"), (10, "😀")]
);
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
上で定義した部分文字列を返す関数のchar_indices
をgrapheme_indices
に変更すると、書記素クラスタを考慮した関数になる。
fn substring_grapheme(s: &str, start: usize, length: usize, is_extended: bool) -> &str {
if length == 0 {
return "";
}
let mut gi = s.grapheme_indices(is_extended);
let byte_start = match gi.nth(start) {
Some(x) => x.0,
None => return "",
};
match gi.nth(length - 1) {
Some(x) => &s[byte_start..x.0],
None => &s[byte_start..],
}
}
let s: &str = "🇯🇵JP😀";
assert_eq!(substring_grapheme(s, 0, 2, true), "🇯🇵J");
let s: String = String::from("🇯🇵JP😀");
assert_eq!(substring_grapheme(&s, 0, 2, true), "🇯🇵J");
同じ文字列から繰り返し部分文字列を取得する場合
同じ文字列から複数回に渡って部分文字列を取得する場合を考える。
char_indices
やgrapheme_indices
とnth
を使う場合、繰り返すと毎回イテレータを生成して最初から反復することになる。特に気にならない程度であれば問題ないが、より効率的には以下の2通りの方法も考えられる。
イテレータを再利用
char_indices
やgrapheme_indices
が返すイテレータを保持して再利用する。ただし、nth
はイテレータを進めていくので、先頭から順にバイト位置を取得していく必要がある。前に戻ることはできない。
let s: &str = "abcde😀👉💯👈👍";
let mut ci = s.char_indices();
let pos1 = 1;
let len1 = 2;
let pos2 = 5;
let len2 = 3;
let byte_start1 = ci.nth(pos1).unwrap().0;
let byte_end1 = ci.nth(len1 - 1).unwrap().0;
let byte_start2 = ci.nth(pos2 - pos1 - len1 - 1).unwrap().0;
let byte_end2 = ci.nth(len2 - 1).unwrap().0;
let s_sub1: &str = &s[byte_start1..byte_end1];
let s_sub2: &str = &s[byte_start2..byte_end2];
assert_eq!(s_sub1, "bc");
assert_eq!(s_sub2, "😀👉💯");
例はchar_indices
だがgrapheme_indices
でも同様。
collectでベクタに変換
collect
でベクタVec
に変換する方法もある。
バイト位置は必要ないので、char_indices
とgrapheme_indices
ではなくchars
とgraphemes
を使う。
chars
をVec<char>
に変換し、そのスライスをiter().collect()
でString
に変換する。
let s: &str = "abcde😀👉💯👈👍";
let v_char: Vec<char> = s.chars().collect();
let s_sub1: String = v_char[1..3].iter().collect();
let s_sub2: String = v_char[5..8].iter().collect();
let s_sub3: String = v_char[2..6].iter().collect();
assert_eq!(s_sub1, "bc");
assert_eq!(s_sub2, "😀👉💯");
assert_eq!(s_sub3, "cde😀");
graphemes
はVec<&str>
への変換となる。そのスライスをconcat
でString
に変換する。
let s: &str = "abcde🇯🇵👉💯👈👍";
let v_str: Vec<&str> = s.graphemes(true).collect();
let s_sub1: String = v_str[1..3].concat();
let s_sub2: String = v_str[5..8].concat();
let s_sub3: String = v_str[2..6].concat();
assert_eq!(s_sub1, "bc");
assert_eq!(s_sub2, "🇯🇵👉💯");
assert_eq!(s_sub3, "cde🇯🇵");
イテレータのように、前から順にバイト位置を取得しなければならないという制約はない。ただし、新たにベクタを生成し、さらに部分文字列として新たなString
を生成するので、その分のメモリが余計に確保されるというデメリットがある。
なお、上述のように、スライスの取得には[]
ではなくget
を使うことも可能。範囲外を指定した場合にも対応できる。
assert_eq!(v_char.get(1..3).unwrap().iter().collect::<String>(), "bc");
assert_eq!(v_char.get(1..100), None);
assert_eq!(v_str.get(1..3).unwrap().concat(), "bc");
assert_eq!(v_str.get(1..100), None);