Как сравнить в Rust две строки, игнорируя регистр символов? Строго говоря, используя лишь стандартную библиотеку — никак, поскольку перевод из одного регистра в другой зависит от локали, но давайте пока проигнорируем эту деталь и притворимся, что среди пользователей нашего приложения нет кого-то, кто живёт в Турции или Азербайджане (а также что у нас нет проблемы нормализования строк).
Итак, как же нам сравнить две строки, игнорируя регистр символов? Большинство Rust-программистов (особенно новичков) напишут что-то вроде этого:
fn equal_ignoring_case(a: &str, b: &str) -> bool {
a.to_lowercase() == b.to_lowercase()
}
Правильное ли это решение? Можно ли сделать лучше? Разумеется: мы можем преобразовывать символы в нижний регистр не сразу все, а на лету, по требованию. Используя стандартную библиотеку, легко написать подобную функцию, которая ещё и не выделяет память в куче:
fn equal_ignoring_case(a: &str, b: &str) -> bool {
a.chars().flat_map(char::to_lowercase).eq(b.chars().flat_map(char::to_lowercase))
}
(тут используется почему-то малоизвестный метод Iterator::eq, который проверяет, что два итератора выдают равные последовательности элементов)А что делать, если одна из строк заведомо содержит лишь ASCII символы (например, это литерал для какого-нибудь формата разметки)? В таком случае мы можем игнорировать юникодные правила преобразования и проверять лишь равенство ASCII-символов без учёта регистра, что значительно более простая задача. Мы можем эксплуатировать тот факт, что строка — это набор байт, проверять строки побайтово, перед этим ещё и проверить, что у них одинаковая длина... Или же не переизобретать велосипед и воспользоваться готовым методом str::eq_ignore_ascii_case.
Немного усложним задачу: теперь нужно определить, что одна строка начинается с другой, игнорируя регистр. Решение в лоб:
fn starts_with_ignoring_case(s: &str, prefix: &str) -> bool {
s.to_lowercase().starts_with(&prefix.to_lowercase())
}
, но оно не оптимально по тем же причинам. К сожалению, трюк с Iterator::eq здесь не подойдёт, потому что этот метод может вернуть true в том случае, если s короче prefix и потому не может его содержать. По аналогичным причинам не подойдёт Iterator::zip. К сожалению, придется написать немного кода самому, с ручными вызовами next:fn starts_with_ignoring_case(s: &str, prefix: &str) -> bool {
let mut s = s.chars().flat_map(char::to_lowercase);
let mut prefix = prefix.chars().flat_map(char::to_lowercase);
while let Some(s_ch) = s.next() {
match prefix.next() {
Some(p_ch) => if s_ch != p_ch {
return false
},
None => return true, //префикс закончился, а все символы до это совпадали
//значит, строка содержит префикс
}
}
//закончились символы в строке, и они все совпали с символами в префиксе
true
}
Разумеется, и тут применима оптимизация, если одна из строк состоит только из ASCII-символов:fn starts_with_ignoring_ascii_case(s: &str, prefix: &str) -> bool {
s.get(..prefix.len()).map_or(false, |start| start.eq_ignore_ascii_case(prefix))
}
Почему я решил об этом написать? Да потому что меня бесит, когда я вижу подобный настолько неоптимальный код. Так что можете считать, что это #бомбёжкипост.