Saltar al contenido
K
Kotlin: el lenguajePaso a paso25 min Básico

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.

ejemplo mínimo: lista frente a secuencia (con comentarios)
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: como forEach, 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; %d es un entero y %s un texto.
ejemplo mínimo: contar y alinear en columnas (con comentarios)
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é      3

En 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   1
Ver solución
solución: la votación de la clase
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 2 y 3. sushi 1 en 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, %-6s y %3d en format.
  • El proyecto no ha cambiado: ./gradlew :torneo:test sigue 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. 1.Una cadena de pasos empieza con asSequence(). ¿Qué cambia respecto a encadenar las operaciones directamente sobre la lista?

  2. 2.Una secuencia encadena filter y map, pero no termina con toList() ni con otra operación que pida el resultado. ¿Qué pasa?

  3. 3.¿Qué devuelve lista.groupingBy { it.producto }.eachCount()?

  4. 4.En ventas.forEachIndexed { indice, venta -> … }, ¿qué valor tiene indice en la primera vuelta?

  5. 5.En "%-7s".format(nombre), ¿qué hace %-7s?

Faltan 5 de 5