Secuencias y formato de tablas
Al terminar: Procesar listas con secuencias perezosas, contar con groupingBy y alinear columnas con format.
Al terminar esta lección sabrás procesar una lista sin crear listas intermedias (con una secuencia), contar cuántas veces aparece cada cosa con groupingBy y eachCount, y escribir tablas con las columnas alineadas con format. Son las piezas con las que la lección siguiente imprimirá la tabla de posiciones de Marcador.
Temas: secuencias (asSequence), groupingBy con eachCount, forEachIndexed y format. En esta lección no cambias el proyecto: todo se hace en el Kotlin Playground.
Secuencias: encadenar sin listas intermedias
Con una lista, filter produce otra lista entera, y map otra más. Con asSequence() cada elemento recorre todos los pasos antes de que empiece a procesarse el elemento que viene detrás, y nunca se crean las listas intermedias. Solo compensa con colecciones grandes; con las de Marcador da igual, pero la tabla de goleadores de la lección siguiente la usa para que la veas.
fun main() {
val numeros = listOf(1, 2, 3)
// Lista: cada paso recorre TODOS los elementos antes de que empiece el siguiente
val conLista = numeros
.filter { n ->
println("filtra $n")
n > 1
}
.map { n ->
println("dobla $n")
n * 2
}
println(conLista)
// Secuencia: cada elemento recorre todos los pasos antes de que llegue el siguiente
val conSecuencia = numeros.asSequence()
.filter { n ->
println("filtra $n")
n > 1
}
.map { n ->
println("dobla $n")
n * 2
}
.toList() // sin toList() no se ejecuta nada: la secuencia espera a que pidas el resultado
println(conSecuencia)
}Salida:
filtra 1
filtra 2
filtra 3
dobla 2
dobla 3
[4, 6]
filtra 1
filtra 2
dobla 2
filtra 3
dobla 3
[4, 6]Contar con groupingBy y alinear con format
Para imprimir la tabla y los goleadores hacen falta cuatro piezas más, todas en el ejemplo de abajo (ventas de una cafetería, explicado línea a línea):
groupingBy { … }.eachCount(): agrupa los elementos y cuenta cuántos hay en cada grupo. Devuelve un mapa. (Sobre una secuencia, como la de la sección anterior, también funciona.)toList()sobre un mapa: lo convierte en una lista de parejas.forEachIndexed: comoforEach, pero la lambda recibe también la posición, que empieza en 0. Si cada elemento es una pareja, puedes abrirla con paréntesis en el segundo parámetro, como en las lambdas de la lección anterior:{ indice, (producto, veces) -> … }."…".format(…): construye un texto con huecos de ancho fijo para alinear columnas;%des un entero y%sun texto.
data class Venta(val producto: String, val unidades: Int)
fun main() {
val ventas = listOf(Venta("Pan", 2), Venta("Café", 1), Venta("Pan", 5), Venta("Té", 3))
val vecesVendido = ventas
.asSequence() // secuencia: cada elemento recorre todos los pasos, sin listas intermedias
.groupingBy { it.producto } // agrupa por producto…
.eachCount() // …y cuenta cuántos hay en cada grupo: un Map
println(vecesVendido) // {Pan=2, Café=1, Té=1}
println(vecesVendido.toList()) // toList() sobre un mapa: lista de Pair
// forEachIndexed con una pareja: (producto, veces) abre cada Pair en dos nombres
vecesVendido.toList().forEachIndexed { indice, (producto, veces) ->
println("${indice + 1}) $producto: $veces")
}
// forEachIndexed: como forEach, pero además da la posición (empieza en 0)
ventas.forEachIndexed { indice, venta ->
// "%2d": un entero (d) en 2 caracteres; "%-5s": un texto (s) a la izquierda en 5; "%3d": un entero en 3
println("%2d. %-5s %3d".format(indice + 1, venta.producto, venta.unidades))
}
}Al ejecutarlo, imprime:
{Pan=2, Café=1, Té=1}
[(Pan, 2), (Café, 1), (Té, 1)]
1) Pan: 2
2) Café: 1
3) Té: 1
1. Pan 2
2. Café 1
3. Pan 5
4. Té 3En format, la letra dice qué tipo de valor va: d es un número entero y s es un texto. El número es el ancho del hueco, y el - alinea a la izquierda.
Reto · Ejercicio: la votación de la clase
En el Kotlin Playground, fuera de tu proyecto.
La clase ha votado qué comer: pizza, tacos, pizza, blanco, sushi, tacos, pizza, blanco (los «blanco» no cuentan). Con una secuencia, descarta los votos «blanco» y cuenta cuántos votos tiene cada opción con groupingBy y eachCount. Ordena el resultado de más a menos votos con sortedBy (ordena de menor a mayor; si pones un - delante del número por el que ordenas, el orden se invierte) y, recorriéndolo con forEachIndexed, imprime cada línea con format: el puesto en 2 huecos, un punto, la opción a la izquierda en 6 huecos y los votos en 2 huecos.
La salida tiene que ser exactamente esta (los espacios cuentan):
1. pizza 3
2. tacos 2
3. sushi 1Ver solución
fun main() {
val votos = listOf("pizza", "tacos", "pizza", "blanco", "sushi", "tacos", "pizza", "blanco")
// asSequence + filter: los votos «blanco» se descartan; groupingBy + eachCount: cuántos votos tiene cada opción
val conteo = votos.asSequence()
.filter { it != "blanco" }
.groupingBy { it }
.eachCount()
// toList() pasa el mapa a parejas; sortedBy ordena de menor a mayor, y el signo menos
// delante de los votos invierte el orden: los que tienen más votos quedan primero
val ranking = conteo.toList().sortedBy { -it.second }
// forEachIndexed da la posición (desde 0); format alinea: "%2d" puesto, "%-6s" opción a la izquierda, "%2d" votos
ranking.forEachIndexed { indice, (opcion, total) -> // (opcion, total) abre cada pareja
println("%2d. %-6s %2d".format(indice + 1, opcion, total))
}
}Antes de seguir
Con lo de esta lección ya sabes encadenar pasos sobre una lista con una secuencia, contar con groupingBy { … }.eachCount() y alinear columnas con format. La lección siguiente, La tabla de posiciones, los usa para escribir el cálculo de la tabla en Marcador.
Punto de control
- El ejercicio de la votación imprime exactamente las tres líneas
1. pizza 3,2. tacos 2y3. sushi 1en el Kotlin Playground (los espacios cuentan). - Sabes decir por qué una secuencia no hace nada hasta que pides el resultado (por ejemplo con
toList()), y qué significan%2d,%-6sy%3denformat. - El proyecto no ha cambiado:
./gradlew :torneo:testsigue pasando, y el informe (open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test) sigue poniendo 36 tests.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Una cadena de pasos empieza con
asSequence(). ¿Qué cambia respecto a encadenar las operaciones directamente sobre la lista?2.Una secuencia encadena
filterymap, pero no termina contoList()ni con otra operación que pida el resultado. ¿Qué pasa?3.¿Qué devuelve
lista.groupingBy { it.producto }.eachCount()?4.En
ventas.forEachIndexed { indice, venta -> … }, ¿qué valor tieneindiceen la primera vuelta?5.En
"%-7s".format(nombre), ¿qué hace%-7s?