rs.nkmk.me

Rustで文字列の位置を指定して部分文字列を取得

Posted: | Tags: Rust, 文字列

Rustで文字列(&str, String)の位置を指定して部分文字列を取得する方法について説明する。

部分文字列ではなく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などを使う。

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, "👉💯👈");

関数にした例は以下の通り。

startendを引数にすると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トレイトをインポートすると、&strStringから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_indicesgrapheme_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_indicesgrapheme_indicesnthを使う場合、繰り返すと毎回イテレータを生成して最初から反復することになる。特に気にならない程度であれば問題ないが、より効率的には以下の2通りの方法も考えられる。

イテレータを再利用

char_indicesgrapheme_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_indicesgrapheme_indicesではなくcharsgraphemesを使う。

charsVec<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😀");

graphemesVec<&str>への変換となる。そのスライスをconcatStringに変換する。

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);

関連カテゴリー

関連記事