1. Il report come pipeline
Quando senti la parola “report”, il cervello di un principiante spesso immagina qualcosa del tipo: “prendiamo una lista → scorriamo → stampiamo”. E uno schema che funziona… esattamente fino alla prima richiesta “aggiungi anche le top‑3 categorie”, “filtra anche solo gli importi ≥ X”, “fai anche in modo che ‘Food’, ‘ food ’ e ‘FOOD’ siano un’unica categoria”. A quel punto il codice inizia a sembrare un groviglio di cavi sotto la scrivania: sembra funzionare, ma fa paura toccarlo.
Per questo ci serve un’altra prospettiva: un report e una pipeline.
Una pipeline e quando non facciamo “tutto subito”, ma costruiamo una catena di passi:
dati in ingresso → trasformazioni → dati finali → testo del report
Importante: un report quasi sempre parte da dati “grezzi” (come l’utente ha inserito categorie, importi, spazi, maiuscole/minuscole) e finisce con qualcosa di “ufficiale”: numeri, strutture, righe ordinate.
Fissiamo l’idea base della giornata:
Un report non e “un’unica catena magica”, ma diversi passi semplici messi nel giusto ordine.
2. L’ordine dei passi della pipeline
Se inizi a scrivere un report “a ispirazione”, e facile confondere cosa fare prima e cosa dopo. Per questo e utile tenere a mente un ordine standard, adatto a un enorme numero di compiti di analisi dati.
Quasi sempre si parte con la normalizzazione. E il portare i valori in ingresso a una forma “canonica”: togliere gli spazi, convertire in minuscolo, sostituire i sinonimi. La normalizzazione e importante perché il report si costruisce sulla corrispondenza delle chiavi, e “food” e “FOOD” per il computer sono stringhe diverse.
Poi di solito viene il filtraggio. Scartiamo cio che non partecipa al report: per esempio importi negativi (se vietati), oppure spese sotto una soglia, oppure categorie “test”.
Dopo di questo — aggregazione: trasformiamo “molte righe” in “risultati”. Qui compaiono fold, groupBy, groupingBy().eachCount() e altre cose dei giorni precedenti. groupBy() in Kotlin restituisce una Map in cui la chiave e il risultato della funzione di raggruppamento, e il valore e la lista degli elementi del gruppo. Invece groupingBy() offre un modo per calcolare un risultato per gruppi “senza liste esplicite dei gruppi” (per esempio, eachCount()).
Poi — ordinamento e scelta di cio che mostriamo davvero (top‑N via take(n)). Questo e gia piu vicino alla “presentazione” che al “significato”: non cambiamo i fatti, decidiamo come mostrarli.
E solo alla fine — preparazione delle stringhe: trasformiamo i dati finali (numeri, coppie, mappe) in un testo gradevole.
Se ti sembra che suoni come “ci stiamo complicando la vita” — si, al momento di scrivere il codice e un po’ piu complesso. Ma tra una settimana ti dirai “grazie”, quando dovrai aggiungere un nuovo report senza rompere quello vecchio.
3. Controllo dei tipi nella pipeline
Ora un punto importante, che spesso manda in confusione i principianti: ogni passo della pipeline cambia il tipo del risultato. Ed e normale.
Il cambio di tipo non e un errore, ma un indizio: “siamo passati al livello successivo di elaborazione”. Se segui i tipi, vedi letteralmente l’architettura del report.
Immaginiamo che nella nostra app console didattica (un tracker di spese “ipotetico”) i dati siano ancora senza classi, in forma di:
// category to amount
val expenses: List<Pair<String, Int>>
Poi una pipeline tipica puo apparire cosi (nella tabella i tipi sono intenzionalmente “evidenziati” — e la tua bussola):
| Passo | Cosa facciamo | Tipo in ingresso | Tipo in uscita |
|---|---|---|---|
| 1 | Normalizziamo le categorie | |
|
| 2 | Filtriamo (per esempio, importi > 0) | |
|
| 3 | Aggreghiamo “somma per categoria” | |
|
| 4 | Prepariamo le righe per l’output (tramite ordinamento) | |
o direttamente |
| 5 | Renderizziamo il testo del report | |
|
Nota: in alcuni punti restiamo su List, e in altri “passiamo” bruscamente a Map. Questa transizione e un confine importante.
E particolarmente utile ricordare groupBy(): dopo di esso ottieni una mappa di liste Map<K, List<V>>. Spesso e proprio quel “momento di svolta” in cui cambia lo stile del codice: invece di “elaboriamo elementi”, iniziamo a “elaborare gruppi”.
Mini-dati per gli esempi
Per rendere gli esempi coerenti, concordiamo un piccolo set di dati. Non e “l’architettura finale del progetto”, e solo un formato comodo per la giornata: una coppia (categoria, importo).
fun main() {
val expenses = listOf(
" Food " to 120,
"FOOD" to 80,
"taxi" to 250,
" books" to 90
)
println(expenses)
// [( Food , 120), (FOOD, 80), (taxi, 250), ( books, 90)]
}
Si, le categorie qui sono “sporche” (spazi, maiuscole/minuscole diverse). E voluto: la vita reale non e obbligata a essere bella.
4. La normalizzazione come passo iniziale
La normalizzazione e quando portiamo i dati a uno stile unico, cosi che le stesse entita coincidano. Nei report, questo riguarda piu spesso le chiavi: categorie, parole, nomi, citta. Se normalizzi tardi (dopo il raggruppamento), otterrai risultati frammentati: “food” separato, “FOOD” separato, “ Food ” separato. Il report sembrera “plausibile”, ma sbagliato — ed e il tipo piu pericoloso di errore.
Facciamo una normalizzazione semplice della categoria: trim() + lowercase().
fun normalizeCategory(raw: String): String {
return raw.trim().lowercase()
}
fun main() {
val raw = listOf(" food ", "FOOD", " taxi")
val normalized = raw.map(::normalizeCategory)
println(normalized) // [food, food, taxi]
}
Qui e importante che applichiamo la normalizzazione tramite map, che costruisce una nuova collezione dai risultati della trasformazione. Non “correggiamo l’originale”, creiamo un nuovo risultato, piu pulito.
Ora applichiamolo alle nostre spese:
fun normalizeCategory(raw: String): String = raw.trim().lowercase()
fun main() {
val expenses = listOf(" Food " to 120, "FOOD" to 80, "taxi" to 250)
val normalizedExpenses = expenses.map { (cat, amount) ->
normalizeCategory(cat) to amount
}
println(normalizedExpenses)
// [(food, 120), (food, 80), (taxi, 250)]
}
Nota il tipo: era List<Pair<String, Int>> ed e rimasto lo stesso tipo. E un “passo iniziale” che non cambia la forma dei dati: li rende piu adatti all’analisi.
5. Raggruppamento e cambio di forma dei dati
Dopo la normalizzazione spesso viene il raggruppamento, perche i report amano frasi come “per categorie”, “per utenti”, “per giorni della settimana”.
In Kotlin il raggruppamento base si fa con groupBy(): restituisce una Map in cui la chiave e cio che ha restituito la funzione di raggruppamento e il valore e la lista degli elementi del gruppo.
Raggruppiamo le spese per categoria:
fun normalizeCategory(raw: String): String = raw.trim().lowercase()
fun main() {
val expenses = listOf(" Food " to 120, "FOOD" to 80, "taxi" to 250)
val normalized = expenses.map { (cat, amount) -> normalizeCategory(cat) to amount }
val grouped = normalized.groupBy { it.first }
println(grouped)
// {food=[(food, 120), (food, 80)], taxi=[(taxi, 250)]}
}
Ecco dove e avvenuto il “momento di svolta”. Il tipo era List<Pair<String, Int>>, ed e diventato:
Map<String, List<Pair<String, Int>>>
Cioe una mappa di liste. Ed e logico: in ogni categoria possono esserci molte voci.
Se non servono le liste dei gruppi
A volte al report non servono gli elementi dei gruppi, serve solo la statistica. Per esempio: “quante volte e comparsa la categoria”. In questi casi spesso e piu comodo groupingBy().eachCount(), che calcola le frequenze.
Mini‑esempio con le parole:
fun main() {
val words = listOf("a", "bb", "ccc", "bb")
val counts: Map<String, Int> = words.groupingBy { it }.eachCount()
println(counts) // {a=1, bb=2, ccc=1}
}
Significato: otteniamo subito una Map<String, Int>, cioe una “mappa finale”, senza liste intermedie. E particolarmente piacevole quando i dati sono tanti.
Per le spese, eachCount() dara “quante operazioni in ogni categoria”, non le somme. A volte e utile come report separato (“numero di acquisti per categorie”), ma piu spesso ci servono comunque le somme — lo faremo nei passi successivi della pipeline.
6. Aggregazione e fold
L’aggregazione e il momento in cui diciamo: “basta dettagli, dammi il risultato”. L’aggregazione piu semplice e la somma.
In Kotlin uno degli strumenti chiave per questo e fold(). Scorre con ordine una collezione e accumula il risultato. A differenza di reduce(), fold() accetta un valore iniziale dell’accumulatore, quindi e piu sicuro per collezioni vuote e piu adatto ai principianti.
Calcoliamo la somma totale delle spese (e il report total, ma oggi ci interessa proprio il passo di aggregazione):
fun main() {
val amounts = listOf(10, 20, 30)
val total = amounts.fold(0) { acc, x -> acc + x }
println(total) // 60
}
Se l’hai mai scritto usando var sum = 0 e un ciclo — allora conosci gia fold, solo che prima lo facevi manualmente. Kotlin rende semplicemente questo pattern esplicito e compatto.
Ora l’aggregazione “somma per categoria”. Abbiamo gia ottenuto Map<String, List<Pair<String, Int>>>. Ora dentro ogni gruppo dobbiamo “ripiegare” la lista in un numero.
fun main() {
val grouped = mapOf(
"food" to listOf("food" to 120, "food" to 80),
"taxi" to listOf("taxi" to 250)
)
val totalsByCategory: Map<String, Int> = grouped.mapValues { (_, items) ->
items.fold(0) { acc, item -> acc + item.second }
}
println(totalsByCategory) // {food=200, taxi=250}
}
Qui c’e un punto importante sul controllo dei tipi. Dopo mapValues non abbiamo piu “liste di operazioni”, abbiamo “numeri finali”. La pipeline e passata dai “dettagli” al “risultato”.
E si, mapValues e una trasformazione della mappa (chiavi uguali, valori nuovi). E proprio quello stile “trasformare, non mutare” verso cui Kotlin spinge tramite le operazioni sulle collezioni.
7. Ordinamento e preparazione dell’output
Dopo l’aggregazione vogliamo mostrare il risultato in modo bello: in alto le categorie piu grandi, sotto quelle piu piccole. Questo non e “cambiare il significato”, e preparazione della presentazione.
Problema tipico del principiante: “Come faccio a ordinare una Map?”. La risposta e semplice: nella maggior parte dei casi non ordini la mappa “cosi com’e”, la trasformi in una lista di elementi (per esempio coppie), ordini la lista e poi stampi quella.
Esempio: Map<String, Int> → List<Pair<String, Int>> → ordinamento per somma in ordine decrescente.
fun main() {
val totals = mapOf("taxi" to 250, "food" to 200, "books" to 90)
val sorted = totals.entries
.map { it.key to it.value }
.sortedByDescending { (_, sum) -> sum }
println(sorted)
// [(taxi, 250), (food, 200), (books, 90)]
}
Di nuovo seguiamo i tipi:
- totals — e una Map<String, Int>
- totals.entries — e l’insieme degli elementi della mappa (in pratica, una “lista di record della mappa”)
- .map { ... } lo trasforma in una List<Pair<String, Int>>
- .sortedByDescending restituisce una nuova lista ordinata (e non tocca i dati originali)
Poi puoi fare top‑N con take(n) — ma il principio e gia chiaro: l’ordinamento si fa piu vicino all’output.
8. Rendering del report in una stringa
Quando vuoi vedere il risultato il prima possibile, la mano tende a scrivere println() direttamente dentro l’aggregazione. Da soddisfazione immediata, ma rovina l’architettura: il calcolo si mescola con l’output. Poi vorrai salvare il report in una stringa, stamparlo in un altro formato o testarlo — e scoprirai che “tutto si stampa da solo”.
Quindi la regola della giornata: prima calcoliamo i dati, poi costruiamo il testo.
Per costruire testo e comodo usare StringBuilder, perche e pensato per assemblare stringhe senza concatenazioni pesanti in un ciclo. In Kotlin ci sono estensioni comode (per esempio .appendLine()).
Facciamo un semplice rendering di una lista di coppie (categoria, somma):
fun renderTotals(rows: List<Pair<String, Int>>): String {
val sb = StringBuilder()
for ((category, total) in rows) {
sb.append(category)
sb.append(": ")
sb.append(total)
sb.append('\n')
}
return sb.toString()
}
fun main() {
val rows = listOf("taxi" to 250, "food" to 200)
print(renderTotals(rows))
// taxi: 250
// food: 200
}
Nota: la funzione non stampa nulla. Restituisce una stringa. E la stampa e compito di main o del livello CLI.
E una sciocchezza, ma e proprio da queste piccole cose che nasce “un codice mantenibile”.
9. Incolliamo l’intera pipeline
Ora mettiamo insieme un piccolo esempio completo di pipeline che fa un report quasi completo: normalizzazione → aggregazione → ordinamento → testo.
Si, potremmo farlo in un’unica catena di 8 chiamate. Ma oggi conta un’architettura leggibile: i passi sono in ordine, e a colpo d’occhio vediamo dove cambia il tipo.
fun normalizeCategory(raw: String): String = raw.trim().lowercase()
fun main() {
val expenses = listOf(
" Food " to 120,
"FOOD" to 80,
"taxi" to 250,
" books" to 90
)
val normalized = expenses.map { (cat, amount) ->
normalizeCategory(cat) to amount
}
val grouped = normalized.groupBy { it.first }
val totalsByCategory = grouped.mapValues { (_, items) ->
items.fold(0) { acc, item -> acc + item.second }
}
val sortedRows = totalsByCategory.entries
.map { it.key to it.value }
.sortedByDescending { (_, sum) -> sum }
val report = renderTotals(sortedRows)
print(report)
// taxi: 250
// food: 200
// books: 90
}
Cosa e importante qui (proprio dal punto di vista dell’“architettura del report”):
- Non facciamo output a meta, perche la meta e ancora “dati”, e possiamo riutilizzarli per altri report. Questo e particolarmente utile nelle app in cui vorrai mostrare report diversi sulla stessa base di operazioni.
- Vediamo chiaramente il cambio di tipo su groupBy(), perche groupBy() restituisce una Map. E poi vediamo chiaramente che dopo fold dentro mapValues otteniamo proprio numeri (risultati).
- Ordiniamo prima dell’output, cosi l’ordine delle righe e prevedibile. (Se ti affidi a “come la mappa stampa”, un giorno potresti sorprenderti. La sorpresa non e il formato migliore per i report finanziari.)
10. Errori tipici nella progettazione di un report-pipeline
Errore n. 1: mescolare normalizzazione e formattazione.
Quando fai trim().lowercase() e subito trasformi il risultato in una bella stringa “Categoria: …”, leghi l’elaborazione dei dati e l’impaginazione in un unico nodo. Poi scopri che ti serve la stessa normalizzazione per il raggruppamento, ma senza “Categoria:”. Molto meglio: prima normalizzare “come dati” e solo alla fine formattare “come testo”.
Errore n. 2: non accorgersi del cambio di forma del risultato.
I principianti spesso continuano a pensare “ho ancora una lista”, anche se dopo groupBy() hanno gia una Map<String, List<...>>. Da qui nascono strani tentativi di chiamare sortedBy “su una mappa” o di scrivere una lambda che si aspetta un elemento della lista ma riceve un Map.Entry. Se tieni a mente il tipo dopo ogni passo, meta della confusione sparisce da sola.
Errore n. 3: raggruppare per una chiave sporca.
Se fai groupBy { it.first } prima di trim/lowercase, ottieni una mappa “corretta”… con chiavi sbagliate. Il report inizia a spezzettarsi, le somme si disperdono, e poi l’utente chiede: “Perche ho due categorie food?”. Perche una e “food” e l’altra e “ Food ”. La normalizzazione dovrebbe quasi sempre venire prima del raggruppamento.
Errore n. 4: provare a ordinare una Map “cosi com’e”, e poi stupirsi.
Una mappa non e una lista. Nei report l’ordinamento quasi sempre si fa sulla lista degli elementi: prendiamo entries, trasformiamo in coppie/righe, ordiniamo, poi stampiamo. Non e una “scorciatoia”, e un pattern standard di preparazione della presentazione.
Errore n. 5: usare reduce senza pensare ai dati vuoti.
reduce non ha un valore iniziale e puo fallire su una collezione vuota. fold e piu sicuro perche ha un accumulatore iniziale. Nei report, i dati vuoti sono una situazione normale (per esempio, non e ancora stato aggiunto nulla), quindi fold e spesso piu comodo.
GO TO FULL VERSION