1. Пользователи такие пользователи
Если бы пользователи всегда вводили текст идеально, программисты лишились бы половины работы, а вторая половина ушла бы на споры о форматировании. В реальности человек вводит «как получилось»: с лишними пробелами, с разным регистром, иногда с переносами строк (особенно если копирует из заметок), а иногда ещё и с «красивыми» пробелами, которые выглядят как обычные, но ведут себя иначе. В результате простой код вида if input == "help" внезапно перестаёт работать, потому что пользователь ввёл " Help ".
Нормализация — это превращение «сырого» текста во «вход, удобный для программы». Обычно это конвейер из шагов: обрезали пробелы по краям, привели регистр, схлопнули повторяющиеся пробелы и только потом сравнили/разобрали.
Небольшая схема, чтобы зафиксировать идею:
flowchart TD
A["readLine() (сырой ввод)"] --> B["trim: убрать пробелы и переводы строк по краям"]
B --> C["lowercased(): привести регистр"]
C --> D["схлопнуть пробелы внутри"]
D --> E["сравнение / условия / дальнейший разбор"]
2. Базовый конвейер нормализации
Foundation и почему иногда нужен import
В Swift часть полезных строковых возможностей живёт не в «голой» стандартной библиотеке, а в библиотеке Foundation. Это нормально: Foundation — большой набор практичных инструментов, который исторически пришёл из мира Cocoa, но в современном Swift используется очень широко.
Когда вы видите методы вроде trimmingCharacters(in:) и replacingOccurrences(of:with:), не удивляйтесь, что иногда без import Foundation компилятор скажет «не знаю такого». Эти методы — привычная часть Swift-экосистемы, но формально относятся к API уровня Foundation (и исторически происходят из мира NSCharacterSet / CharacterSet). Имена этих методов (в современном «свифтовом» виде) закреплены в стандартных гайдах по API, включая trimmingCharacters(in:) и replacingOccurrences(of:with:).
Практическое правило простое: если вы используете CharacterSet.whitespacesAndNewlines или trimmingCharacters(in:), просто поставьте import Foundation в начале файла. И живите спокойно.
trimmingCharacters(in:): чистим края строки
trimmingCharacters(in:) — это как аккуратно подровнять края листа ножницами: мы отрезаем всё лишнее только в начале и конце, но середину не трогаем. Это очень важная мысль. Многие новички ожидают, что «trim уберёт пробелы вообще везде», а потом удивляются, почему "a b" остаётся с внутренними пробелами. Он и должен: иначе метод был бы опасным, потому что мог бы менять смысл текста (например, в имени “Van Helsing” или в строке с кодом).
Чаще всего для ввода человека нам нужен набор символов «пробелы и переводы строк по краям», то есть CharacterSet.whitespacesAndNewlines. В связке это выглядит так:
import Foundation
let raw = " Hello, Swift! \n"
let cleaned = raw.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
print("|\(raw)|") // | Hello, Swift!
// |
print("|\(cleaned)|") // |Hello, Swift!|
Обратите внимание на приём с вертикальными палками |...|: он помогает глазами увидеть, что именно осталось в строке. Это маленький трюк, который экономит время отладки, особенно когда в конце строки прячется \n.
Ещё один пример, более приближенный к «вводу из консоли»:
import Foundation
let line = readLine() ?? ""
let trimmed = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
print("Вы ввели: |\(trimmed)|")
Здесь важна идея: мы сначала делаем ?? "", чтобы гарантированно получить строку, а потом уже применяем нормализацию. Так код остаётся простым, без «сложных» конструкций, которые вы будете разбирать позже.
lowercased(): сравниваем команды без войны регистров
С регистром у людей всё плохо — и это комплимент. Кто-то пишет "Help", кто-то "HELP", кто-то "hElP", а кто-то вообще считает, что Caps Lock — это стиль жизни. Чтобы программа не зависела от этих художественных решений, мы приводим строку к одному регистру (чаще всего к нижнему) и сравниваем уже нормализованную версию.
Метод lowercased() возвращает новую строку. Он не «исправляет строку на месте», потому что String — тип-значение, и в целом в Swift принято возвращать новое значение (это и безопаснее, и понятнее). В документах по строковым и символьным свойствам часто упоминаются операции приведения регистра вроде lowercased()/uppercased().
Пример с командой:
import Foundation
let raw = readLine() ?? ""
let normalized = raw
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
.lowercased()
if normalized == "help" {
print("Доступные команды: help, exit")
} else {
print("Неизвестная команда: \(normalized)")
}
Да, тут появилась «цепочка» вызовов через точку. Важно воспринимать её спокойно: мы буквально читаем слева направо — «берём raw, делаем trim, делаем lowercased». Пока цепочка короткая, она отлично читается. Если цепочка станет длинной, лучше будет разбить на несколько let — не потому что «так правильнее», а потому что так проще мозгу.
Покажу тот же пример, но «лесенкой», чтобы было совсем прозрачно:
import Foundation
let raw = readLine() ?? ""
let trimmed = raw.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let lowered = trimmed.lowercased()
print("raw=|\(raw)|")
print("trimmed=|\(trimmed)|")
print("lowered=|\(lowered)|")
Простые замены: replacingOccurrences(of:with:)
После trim и регистра часто хочется «подправить» текст: заменить запятые на пробелы, заменить несколько пробелов на один, убрать двойные тире и т.д. Для таких задач есть метод replacingOccurrences(of:with:), который возвращает новую строку. Это тоже часть привычного строкового API, закреплённого в современных соглашениях именования Swift, включая replacingOccurrences(of:with:).
Самый простой пример — добавить пробел после запятой:
import Foundation
let s = "яблоко,банан,груша"
let spaced = s.replacingOccurrences(of: ",", with: ", ")
print(spaced) // яблоко, банан, груша
Здесь важно не ожидать «магии»: метод делает буквальную замену всех вхождений. Если у вас уже где-то стоял пробел, получится двойной пробел. И это как раз приводит нас к следующей теме: пробелы внутри строки нужно иногда стабилизировать.
Схлопывание пробелов: один пробел вместо многих
Схлопывание пробелов — типичная задача нормализации: пользователь может случайно поставить несколько пробелов подряд, а вам для логики нужно, чтобы слова разделялись одним пробелом. Например, вы хотите распознать команду "add book", а пользователь ввёл "add book". Для человека это «то же самое», а для программы — разные строки.
Есть много способов решать это (и некоторые мы сознательно не берём сегодня, чтобы не забегать вперёд). Самый понятный на текущем этапе — повторять замену " " (двойной пробел) на " " (одинарный), пока в строке не останется двойных пробелов.
Вот компактный вариант:
import Foundation
var s = "add book"
while s.contains(" ") {
s = s.replacingOccurrences(of: " ", with: " ")
}
print(s) // add book
Почему это работает? Если в строке пять пробелов подряд, то после одной замены станет, например, три, потом два, потом один. Мы просто «дожимаем» строку до стабильного состояния.
Здесь есть тонкость, которую полезно понимать интуитивно: это решение не самое быстрое на огромных текстах, потому что каждый проход создаёт новую строку, а проходов может быть несколько. Но для консольного ввода человека это почти всегда нормально: строки короткие, а читаемость решения важнее микросекунд.
Чуть более «жизненный» пример, где мы нормализуем ввод команды:
import Foundation
let raw = readLine() ?? ""
var cmd = raw.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
cmd = cmd.lowercased()
while cmd.contains(" ") {
cmd = cmd.replacingOccurrences(of: " ", with: " ")
}
print("Нормализовано: |\(cmd)|")
Если пользователь введёт " HeLP ", вы получите "help". Если введёт "ADD BOOK", вы получите "add book". То есть вы сделали ввод предсказуемым.
Памятка: что делает каждый шаг нормализации
Когда вы только начинаете, мозг любит путать «trim», «lowercase» и «replace». Чтобы быстрее ориентироваться, держите в голове простую таблицу. Она не заменяет практику, но хорошо помогает не перепутать смысл методов.
| Шаг | Пример кода | Что меняется | Что не меняется | Частая цель |
|---|---|---|---|---|
| 1 | |
Края строки (начало и конец) | Середина строки | Убрать лишние пробелы и переводы строк вокруг ввода |
| 2 | |
Регистр букв | Пробелы, знаки, цифры | Сравнивать команды «без Caps Lock» |
| 3 | |
Все вхождения подстроки | Всё остальное | Простые «микро-правки» текста |
| 4 | |
Повторяющиеся пробелы внутри | Не трогает одинарные пробелы | Стабилизировать разделение слов |
3. Пример: консольный мини-помощник help и exit
Давайте аккуратно соединим всё в один небольшой сценарий, который ощущается как «приложение», а не набор несвязанных строк. Мы сделаем примитивный цикл: спрашиваем команду, нормализуем ввод, реагируем на "help" и "exit", всё остальное считаем неизвестной командой.
Этот пример одновременно тренирует строки, условия и циклы — и показывает, почему нормализация ставится до сравнений.
import Foundation
var isRunning = true
while isRunning {
print("Введите команду (help/exit):", terminator: " ")
let raw = readLine() ?? ""
var cmd = raw.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
cmd = cmd.lowercased()
while cmd.contains(" ") {
cmd = cmd.replacingOccurrences(of: " ", with: " ")
}
if cmd == "help" {
print("Команды: help, exit")
} else if cmd == "exit" {
print("Пока!")
isRunning = false
} else if cmd.isEmpty {
print("Пустой ввод — я не обижаюсь, но команду не понял.")
} else {
print("Неизвестная команда: \(cmd)")
}
}
Обратите внимание на важную деталь: мы проверяем cmd.isEmpty после trim. Это логично: пользователь мог ввести «пустоту», которая состоит из пробелов и Enter. До trim это не пустая строка, а после — пустая. И именно это поведение нам и нужно.
4. Типичные ошибки
Ошибка №1: ожидать, что trimmingCharacters(in:) удалит пробелы внутри строки.
Это один из самых частых сюрпризов: человек видит слово «trimming» и думает «ну значит подчистит всё». На самом деле метод предназначен именно для краёв. Это хорошо: иначе вы могли бы случайно изменить смысл текста. Если вам нужно управлять пробелами внутри, делайте это отдельным шагом, явно (например, схлопыванием двойных пробелов).
Ошибка №2: забыть import Foundation и ловить “Value of type 'String' has no member ...”.
Когда компилятор ругается на trimmingCharacters или на CharacterSet, это почти всегда означает, что вы забыли импорт. Не надо пытаться «чинить» это странными способами — просто добавьте import Foundation в начало файла и продолжайте жить. И да, это нормально: часть строковых инструментов формально находится в Foundation.
Ошибка №3: думать, что lowercased() меняет строку «на месте».
lowercased() возвращает новую строку. Если написать raw.lowercased() и никуда результат не сохранить, вы ничего не измените. Это типичный момент «я вызвал метод, почему не сработало?». В Swift многие преобразования возвращают новое значение, поэтому либо сохраняйте в let, либо используйте var и присваивание.
Ошибка №4: сделать одну замену " " → " " и считать, что этого достаточно.
Одна замена уберёт только часть проблемы. Если пробелов было много подряд, после одной замены останутся двойные пробелы, и строка всё ещё не нормализована. Поэтому и используется цикл while, который повторяет замену до стабильного результата. Это чуть дольше по времени, но гораздо надёжнее по смыслу.
Ошибка №5: нормализовать ввод после проверок.
Иногда код выглядит так: сначала if raw == "help", а потом где-то ниже trim и lowercase. В итоге проверки всё равно не работают на «кривом» вводе. Правило простое: сначала нормализуем, потом сравниваем. Нормализация — это входной фильтр, а не косметика в конце программы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ