Я пытаюсь разделить String
, сохраняя каждое совпадение с шаблоном, но сохраняя совпавшую часть как начало следующей подстроки, а не как терминатор последней.
let s = String::from("AaaBbCc");
for block in s.split_inclusive(|c: char| c.is_uppercase()) {
println!("{block}")
}
A
aaB
bC
c
Aaa
Bb
Cc
Как этого можно достичь?
Я считал, что этого невозможно добиться каким-то одним методом на String
. Но есть обходной путь:
let s = String::from("AaaBbCc");
let s_reversed = s.chars().rev().collect::<String>();
let blocks = s_reversed
.split_inclusive(|c: char| c.is_uppercase())
// reverse each block in the iterator...
.map(|block| block.chars().rev().collect::<String>())
// and then reverse the whole iterator
.rev();
for block in blocks {
println!("{}", block)
}
Это работает, но очень неэффективно. Итераторы разделения в Rust ленивы и не требуют выделения памяти, поэтому они чрезвычайно эффективны. Напротив, ваш код обрабатывает строку дважды и дважды выделяет для нее память (и немного больше маленьких String's
менее эффективны, чем один большой String
из-за необходимости хранить их размер и указатель каждый), это работает, но это легко сделать лучше . например:
let s = String::from("AaaBbCc"); let blocks = { let mut blocks = Vec::new(); let mut last_split_at = 0; for (index, c) in s.chars().enumerate() { if c.is_uppercase() { blocks.push(&s[last_split_at..index]); last_split_at = index; } } if last_split_at != s.len() - 1 { blocks.push(&s[last_split_at..s.len()]) } blocks }; for block in blocks { println!("{block}") }
который создает Vec
из &str
, он по-прежнему гораздо менее эффективен, чем split
, поскольку он не ленив и ему нужно выделить место для Vec
, но он немного быстрее и эффективнее использует память, чем реверсирование, сбор, разделение, реверсирование, собирать, а затем снова разделять. Возможно, вы правы в том, что это невозможно сделать с помощью split
, но я пока оставлю вопрос открытым на случай, если нам чего-то не хватает (или есть более эффективный обходной путь)
«Очень эффективно» и «очень неэффективно» без конкретных критериев бессмысленно. В зависимости от варианта использования это также может быть просто «достаточно хорошо».
@RichardNeumann это очень неэффективно по сравнению с оптимальной реализацией, поскольку требует большого количества ненужного выделения памяти. Это не обязательно означает, что это плохая реализация. Например, его преимущество заключается в том, что он немного проще, чем собственное решение итератора isaactfa, и, как вы говорите, в зависимости от варианта использования это может быть хорошо (здесь я имею дело с большими строками и множеством разделений, поэтому это не отлично), но тот факт, что эффективность может не потребоваться для некоторых проблем или вы предпочитаете простоту эффективности, не меняет того факта, что это неэффективно.
Я не думаю, что стандартная библиотека сможет вам в этом помочь (хотя на String
есть миллион методов, я могу ошибаться). Но если вам действительно нужна лень, это достаточно простой итератор, который можно написать самому:
struct CamelCaseSplit<'a> {
char_indices: std::str::CharIndices<'a>,
chunk_start: usize,
s: &'a str
}
impl<'a> CamelCaseSplit<'a> {
pub fn new(s: &'a str) -> Self {
let mut char_indices = s.char_indices();
// We'll never want to split before the first char, so skip it.
char_indices.next();
Self {
char_indices,
chunk_start: 0,
s,
}
}
}
impl<'a> Iterator for CamelCaseSplit<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
// The input is exhausted
if self.chunk_start == self.s.len() {
return None;
}
// Find the next uppercase letter position OR the end of the string
let chunk_end = if let Some((chunk_end, _)) = self.char_indices.by_ref().skip_while(|(_, c)| !c.is_uppercase()).next() {
chunk_end
} else {
self.s.len()
};
let chunk = &self.s[self.chunk_start..chunk_end];
self.chunk_start = chunk_end;
return Some(chunk);
}
}
fn main() {
let split = CamelCaseSplit::new("AaaBbCc");
for c in split {
println!("{c}");
// Aaa
// Bb
// Cc
}
}
Спасибо, лень не критична на 100% (хотя это здорово, посмотрите, как это сделать). Проблема в том, что я работаю с большими строками, которые будут разбиты на множество частей, поэтому избегаю необходимости выделять новую память (особенно целая новая строка) для каждого разделения является существенным преимуществом. Спасибо за отличный ответ.
Конечно. Я упомянул лень именно потому, что вы, конечно, можете просто сделать это в цикле for. Но если вам нужно часто использовать его повторно или разделить итерацию, итератор может пригодиться. Обратите внимание, что в исходном коде была ошибка. Вначале он всегда будет создавать пустой кусок. Исправление — это просто новые изменения в первых двух строках конструктора.
@DanielA.White, пожалуйста, хотя бы прочитайте вопрос, прежде чем отправлять запрос на закрытие. Если бы вы прочитали любой вопрос, было бы ясно, что я знаю, как сохранить разделители. Текущий код делает это, и текущие выходные данные показывают, что он делает это, он также использует
split_inclusive
, что является решением, данным в вопросе, на который вы ссылаетесь. Вопрос в том, как изменить местоположение разделения (до и после), а не в том, как сохранить совпадающий символ.