Chars and Substrings
Буквы (char)
Перевод букв в числа методом to_digit(RADIX) (RADIX=10 в десятичной системе) и их сумма:
fn main() {
const RADIX: u32 = 10;
let x = "134";
println!("{}", x.chars().map(|c| c.to_digit(RADIX).unwrap()).sum::<u32>()); }
Перевод букв в коды ASCII и обратно:
println!("{}", 'a' as u8); // перевод символа в код ASCII
println!("{}", 97 as char); // число как символ
// перевод кода UTF-8 в символ (не все символы можно перевести):
println!("{:?}", std::char::from_u32(98).unwrap());
Первая и последняя буква в строке
Чтобы проверить или изменить 1-ую букву в строке (в том числе иероглиф или иной вариант алфавита), нужно строку переделать в вектор из букв:
let char_vec: Vec<char> = text.chars().collect();
if char_vec[0].is_lowercase() { .. }
Для последней буквы есть метод last(), вешается на итератор chars(), возвращает Option<char>, так как последней буквы может и не быть:
println!("{}", "foobar".chars().last().unwrap()); // 'r'
Гласные / согласные буквы
Проверку нужно написать в виде функции:
fn is_vowel(c: char) -> bool {
c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' }
let text = String::from("Aria");
Сортировка букв в словах
Есть функции sort() и sort_unstable() (работает быстрее, но равные элементы может перемешивать) для вектора символов Vec<char>. Обе функции делают сортировку на месте, возвращают ().
let input = "zxyabc"; // сортировка Unicode тоже работает
let mut char2vec_iter = input.chars()
.collect::<Vec<char>>();
char2vec_iter.sort_unstable(); // сортировка на месте
// char2vec_iter.sort_by(|a,b| b.cmp(a)); // реверс-сортировка
// собираем назад String из Vec<char>
let sorted_string: String = char2vec_iter.into_iter().collect();
println!("{}", sorted_string); // "abcxyz"
chunks() и chunks_exact() разбиение строк на части
Аналогично массивам, строки можно разбить на части:
let text = "ПриветМир";
let chars = text.chars().collect::<Vec<char>>();
println!("Разбиваем строку на части по 3 символа:");
for chunk in chars.chunks(3) {
let s: String = chunk.iter().collect();
println!("{}", s);
// При
// вет
// Мир
Строковые срезы
Срезы позволяют взять часть строки, указав диапазон байтовых индексов.
- Синтаксис:
&string[start..end] (где start — начало, end — конец, не включительно).
- Особенность: нужно вручную следить за границами байтов, иначе будет паника при попытке разрезать строку посреди многобайтового символа.
let text = "Hello, world!"; // тип &str
let text = String::from("Hello, world!"); // String
// Если у вас String, а не &str, нужно взять срез с помощью &:
let first_three = &text[0..3]; // первые 3 символа
let last_five = &text[text.len()-5..]; // последние 5 символов
println!("{}", first_three); // "Hel"
println!("{}", last_five); // "orld!"
Отрицательные индексы в Rust не поддерживаются, поэтому нужно вычислять вручную. Если указать только начало ([start..]), берётся всё до конца:
let world_and_more = &text[7..];
println!("{}", world_and_more); // "world!"
Если строка содержит многобайтовые символы (например, кириллицу или эмодзи), простые байтовые срезы могут вызвать панику. Для работы с символами (Unicode scalar values) используйте метод .chars():
let text = "Привет, мир!";
let chars: Vec = text.chars().collect(); // преобразуем в вектор символов
let privet: String = chars[0..6].iter().collect(); // собираем первые 6 символов
println!("{}", privet); // "Привет"
Метод .chars() возвращает итератор по символам, а .collect() собирает их в нужный тип (например, String).
.get(start..end) для безопасного извлечения
Если нет уверенности в границах, и надо избежать паники, подойдёт метод .get() вместо прямого среза. Он возвращает Option<&str>:
let text = "Hello, world!";
if let Some(substr) = text.get(0..5) {
println!("{}", substr); // "Hello"
} else {
println!("Ошибка: неверные границы"); }
Библиотека substring
Для более прямого аналога substr можно использовать стороннюю библиотеку substring из crates.io. Добавьте в Cargo.toml:
[dependencies]
substring = "1.4.5"
Пример использования:
use substring::Substring;
let text = "Hello, world!";
let hello = text.substring(0, 5);
println!("{}", hello); // "Hello"
let world = text.substring(7, 12);
println!("{}", world); // "world"
- Преимущество: проще для новичков, не нужно беспокоиться о байтовых границах.
- Недостаток: добавляет внешнюю зависимость.
Примеры
Разворот букв в словах
Дана строка с пробелами между словами. Необходимо развернуть слова в строке наоборот, при этом сохранить пробелы.
fn reverse_words_split(str: &str) -> String {
str.to_string()
.split(" ") // при разделении split() множественные пробелы сохраняются
.map(|x| x.chars().rev().collect::<String>()) // разворот слов+сбор в строку
.collect::<Vec<String>>(). // сбор всего в вектор
.join(" ") // превращение вектора в строку
}
fn main() {
let word: &str = "The quick brown fox jumps over the lazy dog.";
println!("{}",reverse_words_split(&word));
}
// ehT kciuq nworb xof spmuj revo eht yzal .god
String Iterators
Отображение части строки
Передавать владельца не нужно, передаём в &str:
let s = String::from("Hello World!");
let word = first_word(&s);
println!("The first word is: {}", word);
}
fn first_word(s: &String) -> &str { // передача строки по ссылке
let word_count = s.as_bytes();
for (i, &item) in word_count.iter().enumerate() {
if item == b' ' {
return &s[..i]; // возврат части строки как &str
}
}
&s[..] // обязательно указать возвращаемое значение, если условие в цикле выше ничего не вернёт (например, строка не содержит пробелов = вернуть всю строку)
‘Проход’ по строке итератором
Можно пройти по строке итератором chars() и его методами взятия N-го символа nth() спереди или nth_back() сзади:
let person_name = String::from("Alice");
println!("The last character of string is: {}", match person_name.chars().nth_back(0) { // ищем 1-ый символ с конца строки
Some(i) => i.to_string(), // если находим - превращаем в строку
None => "Nothing found!".to_string(), // не находим = сообщаем
});
matches() и rmatches(),
Возвращают итератор с теми частями строки, которые совпадают с заданным шаблоном:
let v: Vec<&str> = "abcXXXabcYYYabc".matches("abc").collect();
assert_eq!(v, ["abc", "abc", "abc"]); // вывод слева-направо
let v: Vec<&str> = "1abc2abc3".rmatches(char::is_numeric).collect();
assert_eq!(v, ["3", "2", "1"]); // вывод справа-налево
find() и rfind()
Возвращает Option<байт индекс 1го символа в строке слева-направо>, совпадающий с шаблоном. Либо возвращает None, если символ отсутствует в строке. rfind
fn duplicate_encode2(word: &str) -> String {
let s = String::from(word).to_lowercase();
s.chars()
.map(|c| if s.find(c) == s.rfind(c) { '(' } else { ')' })
.collect() } // если у символа есть дубли => замена на '(',
// иначе на ')'
fn main() {
println!("{}", duplicate_encode("rEcede"));
}
Примеры
Повтор части строки n раз
Новый подход использует std::repeat
fn main() {
let repeated = "Repeat".repeat(4);
println!("{}", repeated); // RepeatRepeatRepeatRepeat
}
Старый вариант через итератор - позволяет бесконечно отдавать любое значение (как generic):
use std::iter;
fn main() {
let repeated: String = iter::repeat("Repeat").take(4).collect();
println!("{}", repeated);
}
Удаление пробелов в строке String
Use split(' '), filter out empty entries then re-join by space:
s.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
// Using itertools:
use itertools::Itertools;
s.trim().split(' ').filter(|s| !s.is_empty()).join(" ")
// Using split_whitespace, allocating a vector & string
pub fn trim_whitespace_v1(s: &str) -> String {
let words: Vec<_> = s.split_whitespace().collect();
words.join(" ")
}
Озаглавить каждое слово в предложении
В заданной фразе озаглавить каждое слово. Если результат больше 140 символов или пустой, вернуть None:
fn capitalize_first_letter(s: &str) -> Option<String> {
let res = s
.split_whitespace()
.map(capital) // каждое слово передать в функцию capital()
.collect::<Vec<String>>() // собрать в вектор
.join(" "); // потому что вектор можно собрать в string с join()
if res.len() < 141 || !res.is_empty() { // проверка длины
Some(res)
} else { None } }
fn capital(word: &str) -> String {
let mut lword = word.to_ascii_lowercase();
// изменить НА МЕСТЕ - прямо в этой строке (быстрее всего):
lword[0..1].make_ascii_uppercase();
lword // вернуть итоговую строку
}
String Methods
Вывод строк
- Макрос
println! позволяет вывести строку в поток stdout;
// println!("Hello there!\n");
// раскрывается в такой код:
use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();
- Макрос
dbg!() позволяет вывести переменные и структуры в поток stdout;
split()
Метод split: разбивает строку на части по указанному разделителю и возвращает итератор. Разделитель может быть символом, строкой или даже пробелом. В том числе можно делить по нескольким символам разом:
let text = String::from("the_stealth-warrior");
let parts = text.split(['-', '_']).collect::<Vec<&str>>();
// collect собирает в коллекцию типа вектор
for part in parts {
println!("{}", part);
Другие методы разбивки
split_whitespace()
Разбивает по любым пробельным символам (пробелы, табы, переносы строк).
let text = "apple banana\ncherry";
let words: Vec<&str> = text.split_whitespace().collect();
println!("{:?}", words); // ["apple", "banana", "cherry"]
splitn(n, delimiter)
Разбивает только на первые n частей:
let text = "a,b,c,d";
let parts: Vec<&str> = text.splitn(3, ",").collect();
println!("{:?}", parts); // ["a", "b", "c,d"]
Склеивание строк (конкатенация)
В Rust надо учитывать, что String владеет памятью, а &str — нет.
Оператор +
String + &str работает, но забирает владение у первой строки.
let s1 = String::from("hello");
let s2 = " world";
let res = s1 + s2; // res == "hello world", s1 больше нельзя использовать
push_str()
Метод push_str() добавляет &str к существующей String, не забирая владение (строка должна быть mut).
let mut s = String::from("hello");
s.push_str(" world"); // s == "hello world"
push()
Добавляет один символ:
let mut s = String::from("hello");
s.push('!'); // s == "hello!"
join()
Склеивает коллекцию (вектор) строк с разделителем:
let words = vec!["apple", "banana", "cherry"];
let res = words.join(", "); // res == "apple, banana, cherry"
Мощный способ объединять строки. Макрос format! позволяет сформировать строку и вернуть из функции;
fn output_string(t: &String) -> String {
format!("Hello, {}",t) // возврат сформированной строки
}
При этом format!() позволяет конвертировать формат чисел.
Decimal -> HEX:
fn rgb(r: i32, g: i32, b: i32) -> String {
format!(
"{:02X}{:02X}{:02X}", // конвертация dec -> 2 символа UPPER hex
// {:02x} => конвертация в lower hex.
r.clamp(0, 255), // clamp задаёт валидный диапазон чисел
g.clamp(0, 255), // аналог == g.min(255).max(0)
b.clamp(0, 255))}
fn main() {
println!("{}", rgb(1, 2, 3)); // 010203
println!("{}", rgb(255, 255, 255)); // FFFFFF
println!("{}", rgb(-20, 275, 125)); // 00FF7D
}
Decimal -> Binary:
let b = format!("{:b}", 42);
println!("{}", b); // 101010
Другие популярные методы работы со строками
- Метод
len() выдаёт длину строки в байтах;
- Метод
is_empty() проверят, что строка непустая;
- Метод
contains() ищет одну строку в другой строке;
- Метод
replace(from,to) заменяет часть строки на другую и выдаёт результат;
fn main() {
let mut a = String::from("Wonderful RUST World");
println!("Hello{}!", output_string(&a)); // вывод строки
println!("String is empty? {}", a.is_empty());
println!("String length: {}", a.len());
println!("Does string contain 'Hello'? {}", a.contains("Hello"));
}
Пример задачи
Нахождение закономерностей в структурах со строками
В примере мы передаём вектор из строк. Далее, анализируем его по частям:
fn likes(names: &[&str]) -> String {
match names {
[] => "no one likes this".to_string(),
[a] => format!("{} likes this", a),
[a, b] => format!("{} and {} like this", a, b),
[a, b, c] => format!("{}, {} and {} like this", a, b, c),
[a, b, other @ ..] => format!("{}, {} and {} others like this", a, b, other.len()),
}
}