1. Wprowadzenie
Kiedy program dopiero uczy sie chodzic, wystarczy println("OK"). Ale im dalej piszesz swoj projekt konsolowy, tym czesciej pojawia sie „wewnetrzny uzytkownik” — ty sam po 20 minutach debugowania. I wtedy nagle wychodzi na jaw, ze wyjscie „jak popadnie” zamienia kazde sprawdzenie w archeologie: probujesz zrozumiec, co oznacza total=1200, dlaczego obok nie ma waluty i gdzie w ogole sa granice wpisow.
Formatowanie nie jest o pieknie, tylko o kontrakcie miedzy twoim kodem a czlowiekiem: co jest naglowkiem, co wierszem danych, gdzie jest liczba, gdzie tekst, ktore pola sa „kolumnami” i co zrobic, jesli napis jest dluzszy niz oczekiwano. Dodatkowo dobre formatowanie czesto pomaga wykryc bledy: na przyklad, jesli kolumna sum „rozjechala sie”, to znaczy, ze gdzies niespodziewanie przyszedl tekst, a nie liczba.
Zeby bylo praktycznie, bedziemy kontynuowac nasz umowny projekt konsolowy „ewidencja wydatkow”. Na razie bez klas, wiec wydatki bedziemy przechowywac jako Triple(category, amount, note).
2. Interpolacja napisow: $x i ${expr}
Interpolacja napisow w Kotlin to sytuacja, gdy wstawiasz wartosc bezposrednio do napisu: $x dla prostej nazwy oraz ${expr} dla wyrazenia. To podstawowa rzecz, ale wlasnie ona najczesciej sprawia, ze wyjscie jest czytelne — bez zbednych + i bez „klejenia na slowo honoru”.
Wazne jest, aby poczuc roznice: konkatenacja przez + doslownie skleja kawalki, a string templates pozwalaja pisac prawie „ludzki tekst” z podstawieniami. Dla raportow i komunikatow dla uzytkownika prawie zawsze jest to lepsze.
Podstawowy przyklad: drukujemy wydatek
Zacznijmy od prostego. Niech wydatek to Triple<String, Int, String>: kategoria, kwota, komentarz.
fun main() {
val expense = Triple("food", 450, "coffee and croissant")
val category = expense.first
val amount = expense.second
val note = expense.third
println("Expense: category=$category, amount=$amount, note=\"$note\"")
// Expense: category=food, amount=450, note="coffee and croissant"
}
Zwracaj uwage na cudzyslow wokol note: to drobiazg, ale czasem przydaje sie wizualnie widziec, gdzie zaczyna sie i gdzie konczy tekst.
Kiedy potrzebne jest ${expr}
Czasem chcesz wbudowac wynik obliczen. Na przyklad, pokazac kwote ze „znakiem” albo policzyc podatek.
fun main() {
val amount = 450
val tax = (amount * 0.2).toInt()
println("Amount=$amount, tax(20%)=$tax, total=${amount + tax}")
// Amount=450, tax(20%)=90, total=540
}
Tutaj dobrze widac, po co sa nawiasy klamrowe: wewnatrz ${...} mozna pisac wyrazenie.
O czytelnosci: nie rob z napisu „dysertacji”
Dobra heurystyka jest taka: jesli wewnatrz napisu pojawia sie wyrazenie na trzy linie, to znak, ze pora wyniesc obliczenia do osobnego val. To znaczy, formatowanie nie powinno ukrywac logiki. Kotlin pozwala wstawic „wszystko, co sie da”, ale piszesz kod nie tylko dla kompilatora, lecz takze dla przyszlego siebie.
3. Rozdzielamy „policzyc” i „pokazac”
Jeden z czestych problemow poczatkujacych: obliczenia, parsowanie napisow i drukowanie mieszaja sie w jedna salatke. Dziala? Czasem. Jest zrozumiale? Zwykle nie. Jesli kiedykolwiek zdarzalo ci sie debugowac program po samym wyjsciu, wiesz, ze im „czystszy” podzial etapow, tym latwiej zauwazyc blad.
Trzymajmy sie dyscypliny: funkcje, ktore licza, zwracaja dane, a funkcje, ktore formatuja, zwracaja napis. A dopiero main() decyduje, czy go wypisac. To czyni program bardziej przewidywalnym i prostszym do testow (nawet jesli jeszcze nie omawialismy testowania — zasada i tak jest przydatna).
Formatujemy jeden wiersz w osobnej funkcji
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
}
Zauwaz: formatExpense niczego nie wypisuje, ona tylko zwraca napis. To maly, ale bardzo wazny styl.
Formatowanie i logowanie to rozne rzeczy
W edukacyjnych projektach konsolowych kusi, by wypisywac wszystko po kolei. To normalne na starcie. Ale wlasnie dlatego warto juz teraz przyzwyczajac sie: „formatowanie” dotyczy komunikatu dla uzytkownika albo raportu, a nie chaotycznego println("x=$x") w srodku algorytmu.
4. Wyrownanie kolumn: padEnd() i padStart()
Gdy danych jest wiecej niz jedno pole, wyjscie „w jednej linii przez przecinek” szybko przestaje byc czytelne. Ludzkie oko lubi kolumny: tekst po lewej, liczby po prawej. W interfejsie graficznym robi to tabela, a w konsoli — spacje. I wlasnie tu w Kotlin pojawia sie bardzo przyjazne narzedzie: padEnd(width) i padStart(width).
Sens jest prosty: napis jest dopelniany spacjami do wymaganej szerokosci. padEnd dodaje spacje po prawej, padStart — po lewej. I tak, brzmi jak „po prostu spacje”, ale to te spacje, ktore nagle oszczedzaja mase nerwow.
Mini-tabela: naglowek i jeden wiersz
fun main() {
val nameW = 12
val amountW = 8
val header = "CATEGORY".padEnd(nameW) + "AMOUNT".padStart(amountW)
val row = "food".padEnd(nameW) + "450".padStart(amountW)
println(header)
// CATEGORY AMOUNT
println(row)
// food 450
}
Wazny szczegol: liczby na razie wypisujemy jako napisy "450".
Czym sie roznia padEnd i padStart
Czasem najlatwiej zobaczyc to oczami:
| Metoda | Gdzie dodaje spacje | Typowy przypadek |
|---|---|---|
|
w prawo | kolumna tekstowa po lewej |
|
w lewo | kolumna liczbowa po prawej |
W konsoli liczby zazwyczaj wyrownuje sie do prawej krawedzi, bo tak latwiej porownywac wartosci, zwlaszcza gdy maja rozna dlugosc: 5, 50, 5000.
Wyrownujemy liczby: najpierw toString(), potem padStart()
Czesty mikro-blad: proba uzycia padStart bezposrednio na liczbie. padStart to metoda napisow, wiec liczbe trzeba zamienic na napis.
fun main() {
val amount = 450
val amountW = 8
val cell = amount.toString().padStart(amountW)
println("|$cell|") // | 450|
}
Wyglada smiesznie, ale |...| naprawde pomaga zobaczyc spacje.
Dlaczego lepiej trzymac szerokosci w zmiennych
Jesli w jednym miejscu ustawisz szerokosc na 10, a w innym na 12, kolumny sie „rozjada”. Dlatego lepiej sie umowic: szerokosci to mala konfiguracja formatowania.
fun main() {
val categoryW = 14
val amountW = 8
val cat = "transport".padEnd(categoryW)
val amt = 120.toString().padStart(amountW)
println(cat + amt) // transport 120
}
Jesli tekst jest dluzszy niz szerokosc
padEnd nie ucina napisu, tylko dopelnia. Jesli kategoria ma dlugosc 50 znakow, po prostu przesunie wszystko w prawo. W konsoli wyglada to tak, jakby „formatowanie sie zepsulo”. Dlatego czasem wprowadza sie zasade: „jesli dluzsze — ucinamy”.
Nie uzywamy jeszcze nic „zaawansowanego”, tylko to, co juz znane: length i 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-|
}
Tak, slowo zostalo uciete. To normalne: konsolowa tabela nie jest z gumy. Najwazniejsze, ze zasada jest teraz jawna i taka sama wszedzie.
5. Wielowierszowe wyjscie: joinToString() i raw strings
Gdy drukujesz raport, zwykle masz zestaw linii: tytul, linie-separator, linie danych, podsumowanie. Najbardziej bezposrednia droga to wiele wywolan println(...). Ale jest bardziej elegancki pomysl: zlozyc liste linii, a potem polaczyc je w jeden wielowierszowy tekst.
Do tego swietnie nadaje sie joinToString(...): zamienia kolekcje elementow w jeden napis z separatorem, prefiksem, sufiksem i innymi ustawieniami. A jesli jako separator ustawisz "\n", wyjdzie wielowierszowy tekst.
Skladamy raport jako List<String>
fun main() {
val lines = listOf(
"CATEGORY".padEnd(12) + "AMOUNT".padStart(8),
"------------" + "--------",
"food".padEnd(12) + "450".padStart(8),
"transport".padEnd(12) + "120".padStart(8)
)
val report = lines.joinToString(separator = "\n")
println(report)
}
Kluczowa mysl: najpierw dostajesz „logiczne linie raportu”, a potem sklejasz je w finalny tekst. Latwiej to utrzymywac niz dziesiec println wymieszanych z obliczeniami.
prefix i postfix do oprawy
joinToString potrafi dodac prefiks i sufiks, co czasem bywa wygodne.
fun main() {
val rows = listOf("A", "B", "C")
val text = rows.joinToString(
separator = "\n",
prefix = "Report:\n",
postfix = "\n(end)"
)
println(text)
// Report:
// A
// B
// C
// (end)
}
Prefiks i sufiks to nie „magia formatowania”, tylko kolejne dwa kawalki napisu. Ale kiedy daje je standardowa funkcja, jest mniej okazji do pomylki.
Wielowierszowe napisy """...""" jako szablon
Czasem wygodniej nie skladac raportu z wielu malych kawalkow, tylko miec „szablon” z kilku linii — jak formularz. W Kotlin do tego sa raw strings: potrojny cudzyslow """...""".
Zaleta takich napisow jest to, ze w srodku mozna spokojnie pisac cudzyslowy i znaki nowej linii, a do tego wygodnie uzywa sie trimIndent(), zeby ladnie ustawic wciecia w kodzie, ale nie przenosic ich do wyjscia.
fun main() {
val title = "Expenses report"
val header = """
=== $title ===
Columns: category | amount | note
""".trimIndent()
println(header)
// === Expenses report ===
// Columns: category | amount | note
}
Zauwaz: nadal uzywamy $title. Raw string nie znosi string templates — po prostu sprawia, ze wielowierszowosc jest naturalna.
6. Przyklad praktyczny: raport wydatkow
Teraz zrobimy maly „praktyczny” fragment dla naszego projektu ewidencji wydatkow: funkcje, ktora przyjmuje liste wydatkow i zwraca ladny tekst raportu. Uzyjemy tylko tego, co juz omowilismy: string templates, padStart/padEnd, skladanie linii w liscie i joinToString("\n").
Dane wydatkow to nadal Triple(category, amount, note). Tak, Triple nie jest najwygodniejszym typem na swiecie, ale uczciwie dziala, dopoki nie przejdziemy na klasy.
Formatowanie jednego wiersza tabeli
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"
}
Tutaj zrobilismy dwa „kontrakty”: szerokosc kategorii i szerokosc kwoty. Dodatkowo zauwazylismy „niebezpieczne miejsce” — dluga kategorie — i z gory zdecydowalismy, ze bedziemy ja ucinac.
Pelny raport jako jeden wielowierszowy napis
fun buildExpensesReport(expenses: List<Triple<String, Int, String>>): String {
val categoryW = 14
val amountW = 8
val title = "EXPENSES"
val header = "CATEGORY".padEnd(categoryW) + "AMOUNT".padStart(amountW) + " NOTE"
val separator = "-".repeat(categoryW) + "-".repeat(amountW) + " ----"
val rows = expenses.map { e ->
formatExpenseRow(e, categoryW, amountW)
}
val total = expenses.sumOf { it.second }
val footer = "TOTAL".padEnd(categoryW) + total.toString().padStart(amountW)
val lines = listOf(
"== $title ==",
header,
separator
) + rows + listOf(separator, footer)
return lines.joinToString(separator = "\n")
}
Tutaj jest kilka waznych obserwacji.
Po pierwsze, niczego nie wypisujemy wewnatrz buildExpensesReport. Zwracamy napis — to wygodniejsze niz „drukowanie po drodze”. Po drugie, jawnie ustalamy szerokosci kolumn i uzywamy ich wszedzie tak samo. Po trzecie, raport budujemy jako liste linii, a na koncu zamieniamy go w wielowierszowy tekst przez joinToString("\n").
Uzycie w 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)
// == EXPENSES ==
// CATEGORY AMOUNT NOTE
// ------------------------ ----
// food 450 coffee and croissant
// transport 120 metro
// books 900 kotlin book
// ------------------------ ----
// TOTAL 1470
}
Jesli teraz pomyslales „to wyglada jak male terminalowe UI”, to tak — wlasnie tak rodza sie konsolowe raporty: ze spacji, dyscypliny i lekkiej paranoi na punkcie szerokosci.
7. Typowe bledy
Blad nr 1: mieszanie obliczen i drukowania w jednym miejscu, az kod zamieni sie w „makaron”.
Poczatkujacy czesto robia tak: policzyli sume — od razu println, potem w srodku petli znow println, a potem gdzies w if tez. W efekcie nie da sie ponownie uzyc wyjscia, nie da sie zlozyc raportu w jeden napis i trudno sprawdzic poprawnosc. Leczy to nawyk: funkcja formatujaca zwraca String, a drukowanie zyje w jednym-dwoch miejscach.
Blad nr 2: oczekiwanie, ze padStart/padEnd „same zrozumieja”, jak wyrownac liczbe.
padStart dziala na napisie, a nie na liczbie. Dlatego „poprawny rytual” wyglada tak: amount.toString().padStart(width). Jesli zapomnisz o toString(), albo dostaniesz blad kompilacji, albo zaczniesz robic dziwne obejscia.
Blad nr 3: nieustalanie szerokosci kolumn i rozsmarowywanie liczb 10, 12, 8 po calym kodzie.
Na poczatku wydaje sie, ze „i tak jest jasne”. Potem zmieniasz szerokosc naglowka w jednym miejscu, a wiersze danych w innym — i tabela sie rozpada. Znacznie pewniej jest miec val categoryW = ..., val amountW = ... i uzywac ich jako mini-ustawien raportu.
Blad nr 4: brak myslenia o dlugich napisach, przez co cala tabela sie „rozjezdza”.
padEnd nie ucina napisu. Jesli w kategorii nagle pojawi sie tekst o dlugosci 40 znakow, przesunie inne kolumny i raport stanie sie nieczytelny. Trzeba z gory wybrac polityke: ucinać (substring), skrocic z wielokropkiem albo przeniesc do nastepnej linii. W wersji edukacyjnej wystarczy proste „uciac do szerokosci”.
Blad nr 5: skladanie wielowierszowego raportu dziesiatkami println, a potem zdziwienie, ze format trudno zmienic.
Gdy raport powstaje jako jeden napis przez joinToString("\n"), latwo go zapisac, zwrocic z funkcji, pokazac czesciowo albo dopisac cos na gorze/na dole. Gdy raport jest wypisywany „w trakcie”, tracisz kontrole.
GO TO FULL VERSION