1. Вступ
Коли програма тільки вчиться ходити, достатньо println("OK"). Але що далі ви пишете консольний проєкт, то частіше зʼявляється «внутрішній користувач» — ви самі, але вже через 20 хвилин налагодження. І тут раптом зʼясовується: виведення «як завгодно» перетворює будь-яку перевірку на археологію. Ви намагаєтеся зрозуміти, що означає total=1200, чому поруч немає валюти та де взагалі межі записів.
Форматування — це не про красу, а про контракт між вашим кодом і людиною. Що є заголовком, що — рядком даних, де число, а де текст, які поля мають бути «стовпцями», і що робити, якщо рядок довший за очікуваний. До того ж добре форматування часто допомагає виявити баги. Наприклад, якщо стовпець із сумами «поїхав», значить, десь несподівано прийшов текст, а не число.
Щоб усе було практично, продовжимо наш умовний консольний проєкт «облік витрат». Він поки без класів, тож витрати зберігатимемо як Triple(category, amount, note).
2. Інтерполяція рядків: $x і ${expr}
Інтерполяція рядків у Kotlin — це коли ви підставляєте значення прямо всередину рядка: $x для простого імені та ${expr} для виразу. Це базова річ, але саме вона найчастіше робить виведення читабельним — без зайвих + і без «склейок нашвидкуруч».
Важливо відчути різницю: конкатенація через + буквально склеює шматки, а шаблони рядків дають змогу писати майже «людський текст» із підстановками. Для звітів і повідомлень користувачу це майже завжди виграє.
Базовий приклад: друкуємо витрату
Почнімо з простого. Нехай витрата — це Triple<String, Int, String>: категорія, сума, коментар.
fun main() {
val expense = Triple("food", 450, "coffee and croissant")
val category = expense.first
val amount = expense.second
val note = expense.third
println("Витрата: категорія=$category, сума=$amount, примітка=\"$note\"")
// Витрата: категорія=food, сума=450, примітка="coffee and croissant"
}
Зверніть увагу на лапки навколо note: це дрібниця, але інколи корисно візуально бачити, де починається і де закінчується текст.
Коли потрібен ${expr}
Іноді хочеться вбудувати результат обчислення. Наприклад, показати суму зі «знаком» або порахувати податок.
fun main() {
val amount = 450
val tax = (amount * 0.2).toInt()
println("Сума=$amount, податок(20%)=$tax, разом=${amount + tax}")
// Сума=450, податок(20%)=90, разом=540
}
Тут добре видно, навіщо потрібні фігурні дужки: усередині ${...} можна писати вираз.
Про читабельність: не перетворюйте рядок на «дисертацію»
Хороша евристика така: якщо всередині рядка зʼявляється вираз на три рядки, значить, час винести обчислення в окремий val. Тобто форматування не повинно маскувати логіку. Kotlin дає змогу вставити «все на світі», але ви пишете код не лише для компілятора, а й для майбутнього себе.
3. Розділяємо «порахувати» і «показати»
Одна з частих проблем новачків: обчислення, розбір рядків і друк змішуються в одну мішанину. Працює? Іноді. Зрозуміло? Зазвичай ні. Якщо вам коли-небудь доводилося налагоджувати програму за виведенням, ви знаєте: що «чистіше» розділені етапи, то легше помітити помилку.
Дотримуймося дисципліни: функції, які рахують, повертають дані, а функції, які форматують, повертають рядок. А вже main() вирішує, друкувати його чи ні. Так програма стає передбачуванішою і простішою для тестів (навіть якщо ми ще не вивчали тестування — принцип однаково корисний).
Форматуємо один рядок окремою функцією
fun formatExpense(expense: Triple<String, Int, String>): String {
val category = expense.first
val amount = expense.second
val note = expense.third
return "[$category] $amount — $note"
}
fun main() {
val e = Triple("transport", 120, "metro")
println(formatExpense(e)) // [transport] 120 — metro
}
Зверніть увагу: formatExpense нічого не друкує, вона лише повертає рядок. Це маленька, але дуже важлива звичка.
Форматування й логування — різні речі
У консольних навчальних проєктах хочеться друкувати все підряд. На старті це нормально. Але саме тому корисно вже зараз звикати: «форматування» — це про повідомлення для користувача або звіт, а не про хаотичне println("x=$x") посеред алгоритму.
4. Вирівнювання колонок: padEnd() і padStart()
Коли даних стає більше, ніж одне поле, виведення «в один рядок через кому» швидко перестає читатися. Око любить стовпці: текст ліворуч, числа праворуч. У графічному інтерфейсі це робить таблиця, а в консолі — пробіли. І тут у Kotlin є дуже дружній інструмент: padEnd(width) і padStart(width).
Сенс простий: рядок доповнюється пробілами до потрібної ширини. padEnd додає пробіли справа, padStart — зліва. Так, звучить як «просто пробіли». Але це саме ті пробіли, які раптом економлять купу нервів.
Мінітаблиця: заголовок і один рядок
fun main() {
val nameW = 12
val amountW = 8
val header = "КАТЕГОРІЯ".padEnd(nameW) + "СУМА".padStart(amountW)
val row = "food".padEnd(nameW) + "450".padStart(amountW)
println(header)
// КАТЕГОРІЯ СУМА
println(row)
// food 450
}
Тут важливий момент: числа ми поки виводимо як рядки "450".
Чим відрізняються padEnd і padStart
Іноді простіше побачити це наочно:
| Метод | Куди додає пробіли | Типовий випадок |
|---|---|---|
|
праворуч | текстовий стовпець ліворуч |
|
ліворуч | числовий стовпець праворуч |
У консолі числа зазвичай вирівнюють за правим краєм, бо так легше порівнювати величини, особливо коли довжина різна: 5, 50, 5000.
Вирівнюємо числа: спочатку toString(), потім padStart()
Часта мікропомилка — намагатися викликати padStart для числа напряму. padStart — метод рядків, тому число спочатку потрібно перетворити на рядок.
fun main() {
val amount = 450
val amountW = 8
val cell = amount.toString().padStart(amountW)
println("|$cell|") // | 450|
}
Це виглядає кумедно, але |...| справді допомагає побачити пробіли.
Чому ширини краще зберігати у змінних
Якщо ви в одному місці задасте ширину 10, а в іншому — 12, стовпці «поїдуть». Тому краще домовитися: ширини — це маленька конфігурація форматування.
fun main() {
val categoryW = 14
val amountW = 8
val cat = "transport".padEnd(categoryW)
val amt = 120.toString().padStart(amountW)
println(cat + amt) // transport 120
}
Якщо текст довший за ширину
padEnd не обрізає рядок — він лише доповнює його пробілами. Якщо категорія має довжину 50 символів, вона просто посуне все праворуч. У консолі це виглядає так, ніби «форматування зламалося». Тому інколи вводять правило: «якщо довше — обріжемо».
Ми поки не використовуємо нічого «просунутого», лише те, що вже знайоме: length і substring.
fun fitLeft(text: String, width: Int): String {
val cut = if (text.length > width) text.substring(0, width) else text
return cut.padEnd(width)
}
fun main() {
val w = 10
println(fitLeft("very-very-long-category", w) + "|") // very-very-|
}
Так, слово обрізалося. Це нормально: консольна таблиця — не гумова. Головне, що правило тепер явне й однакове всюди.
5. Багаторядкове виведення: joinToString() і «сирі» рядки
Коли ви друкуєте звіт, зазвичай маєте набір рядків: заголовок, рядок-роздільник, рядки даних, підсумок. Найпрямолінійніший шлях — багато println(...). Але є охайніша ідея: зібрати список рядків, а потім обʼєднати їх в один багаторядковий текст.
Для цього чудово підходить joinToString(...): він перетворює колекцію елементів на один рядок із роздільником, префіксом, постфіксом та іншими налаштуваннями. А якщо роздільником зробити "\n", вийде багаторядковий текст.
Збираємо звіт як List<String>
fun main() {
val lines = listOf(
"КАТЕГОРІЯ".padEnd(12) + "СУМА".padStart(8),
"------------" + "--------",
"food".padEnd(12) + "450".padStart(8),
"transport".padEnd(12) + "120".padStart(8)
)
val report = lines.joinToString(separator = "\n")
println(report)
}
Тут ключова думка проста: спочатку ви отримуєте «логічні рядки звіту», а потім склеюєте їх у підсумковий текст. Це простіше підтримувати, ніж десять println, перемішаних з обчисленнями.
prefix і postfix для оформлення
joinToString уміє додавати префікс і постфікс — інколи це зручно для оформлення.
fun main() {
val rows = listOf("A", "B", "C")
val text = rows.joinToString(
separator = "\n",
prefix = "Звіт:\n",
postfix = "\n(кінець)"
)
println(text)
// Звіт:
// A
// B
// C
// (кінець)
}
Префікс і постфікс — це не «магія форматування», а просто ще два шматочки рядка. Але коли їх додає стандартна функція, шансів помилитися менше.
Багаторядкові рядки """...""" для шаблону
Іноді зручніше не збирати звіт із багатьох маленьких шматочків, а мати «шаблон» із кількох рядків — як бланк. У Kotlin для цього є «сирий рядок»: потрійні лапки """...""".
Перевага таких рядків у тому, що всередині можна спокійно писати лапки й переноси рядків. Також зручно використовувати trimIndent(), щоб гарно оформити відступи в коді, але не тягнути їх у виведення.
fun main() {
val title = "Звіт про витрати"
val header = """
=== $title ===
Колонки: категорія | сума | примітка
""".trimIndent()
println(header)
// === Звіт про витрати ===
// Колонки: категорія | сума | примітка
}
Зверніть увагу: ми все ще використовуємо $title. «Сирий рядок» не скасовує шаблони рядків — він лише робить переноси природними.
6. Практичний приклад: звіт про витрати
Зараз зробимо невеликий практичний фрагмент для нашого проєкту обліку витрат: функцію, яка приймає список витрат і повертає охайний текст звіту. Ми використаємо лише те, що вже обговорили: шаблони рядків, padStart/padEnd, збирання рядків у список і joinToString("\n").
Дані витрат — усе ще Triple(category, amount, note). Так, Triple — не найзручніший тип у світі, але він чесно працює доти, доки ми не перейдемо до класів.
Форматування одного рядка таблиці
fun formatExpenseRow(
expense: Triple<String, Int, String>,
categoryW: Int,
amountW: Int
): String {
val category = expense.first
val amount = expense.second
val note = expense.third
val safeCategory = if (category.length > categoryW) category.substring(0, categoryW) else category
val catCell = safeCategory.padEnd(categoryW)
val amountCell = amount.toString().padStart(amountW)
return "$catCell$amountCell $note"
}
Тут ми задали два «контракти»: ширину категорії та ширину суми. Також помітили «небезпечне місце» — довгу категорію — і заздалегідь ухвалили рішення обрізати її.
Повний звіт як один багаторядковий рядок
fun buildExpensesReport(expenses: List<Triple<String, Int, String>>): String {
val categoryW = 14
val amountW = 8
val title = "ВИТРАТИ"
val header = "КАТЕГОРІЯ".padEnd(categoryW) + "СУМА".padStart(amountW) + " ПРИМІТКА"
val separator = "-".repeat(categoryW) + "-".repeat(amountW) + " ----"
val rows = expenses.map { e ->
formatExpenseRow(e, categoryW, amountW)
}
val total = expenses.sumOf { it.second }
val footer = "РАЗОМ".padEnd(categoryW) + total.toString().padStart(amountW)
val lines = listOf(
"== $title ==",
header,
separator
) + rows + listOf(separator, footer)
return lines.joinToString(separator = "\n")
}
Тут є кілька важливих спостережень.
По-перше, ми нічого не друкуємо всередині buildExpensesReport. Ми повертаємо рядок — це зручніше, ніж «друк дорогою». По-друге, ми явно фіксуємо ширини стовпців і використовуємо їх усюди однаково. По-третє, звіт будується як список рядків, а наприкінці перетворюється на багаторядковий текст через joinToString("\n").
Використання в main()
fun main() {
val expenses = listOf(
Triple("food", 450, "coffee and croissant"),
Triple("transport", 120, "metro"),
Triple("books", 900, "kotlin book")
)
val report = buildExpensesReport(expenses)
println(report)
// == ВИТРАТИ ==
// КАТЕГОРІЯ СУМА ПРИМІТКА
// ------------------------ ----
// food 450 coffee and croissant
// transport 120 metro
// books 900 kotlin book
// ------------------------ ----
// РАЗОМ 1470
}
Якщо ви зараз подумали: «це схоже на маленький термінальний UI», то так — саме так і народжуються консольні звіти: із пробілів, дисципліни та легкої параної щодо ширин.
7. Типові помилки
Помилка № 1: змішувати обчислення і друк в одному місці, доки код не стане «локшиною».
Новачки часто роблять так: порахували суму — одразу println, потім посеред циклу ще println, потім десь у if — теж. У підсумку ви не можете повторно використати виведення, не можете зібрати звіт у рядок, і складно перевірити коректність. Лікується звичкою: форматувальна функція повертає String, а друк живе в одному-двох місцях.
Помилка № 2: очікувати, що padStart/padEnd «самі зрозуміють», як вирівнювати число.
padStart працює з рядком, а не з числом. Тому «правильний ритуал» такий: amount.toString().padStart(width). Якщо забути toString(), ви або отримаєте помилку компіляції, або почнете вигадувати дивні обхідні шляхи.
Помилка № 3: не фіксувати ширини стовпців і розмазувати числа 10, 12, 8 по коду.
Спочатку здається, що «і так зрозуміло». Потім ви змінюєте ширину заголовка в одному місці, а рядки даних — в іншому, і таблиця розвалюється. Набагато надійніше мати val categoryW = ..., val amountW = ... і користуватися ними як міні-налаштуванням звіту.
Помилка № 4: не думати про довгі рядки, через що «їде» вся таблиця.
padEnd не обрізає рядок. Якщо в категорії раптом опинився текст на 40 символів, він зсуне інші стовпці, і звіт стане нечитабельним. Потрібно заздалегідь вибрати політику: обрізати (substring), скорочувати з трьома крапками або переносити на наступний рядок. У навчальному варіанті достатньо простого «обрізати до ширини».
Помилка № 5: збирати багаторядковий звіт десятками println, а потім дивуватися, що формат складно змінювати.
Коли звіт створюється як єдиний рядок через joinToString("\n"), ви можете легко його зберегти, повернути з функції, показати частково або дописати зверху/знизу. Коли звіт друкується «на ходу», ви втрачаєте контроль.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ