La tabla de posiciones
Al terminar: Construir la tabla de posiciones del torneo con flatMap, groupBy, comparadores y sortedWith.
Al terminar esta lección Marcador calculará su tabla de posiciones —partidos jugados, ganados, goles y puntos, ordenada como en una liga de verdad— a partir de una lista de partidos, la imprimirá con las columnas alineadas y, debajo, mostrará su tabla de goleadores. Es el primer cálculo real de la app.
Temas: flatMap, groupBy con dos lambdas, keys, orEmpty y los comparadores con sortedWith; y, en el paso, asSequence, groupingBy y format de la lección anterior.
Las piezas de la tabla de posiciones
La tabla de posiciones de una liga tiene una fila por equipo con sus partidos jugados, ganados, empatados y perdidos, sus goles y sus puntos. Se calcula con piezas que ya casi conoces y tres nuevas:
flatMap { … }: comomap, pero la lambda devuelve una lista por cada elemento yflatMaplas une todas en una sola lista. Sirve cuando cada partido cuenta dos veces, una por equipo.groupBycon dos lambdas: la primera da la clave del grupo y la segunda, lo que se guarda en él.- Un comparador (un objeto que dice cuál de dos elementos va antes) con
compareByDescending,thenByDescendingythenBy, que se pasa asortedWith. Hace falta cuando se ordena por varios criterios a la vez, porquesortedByadmite solo uno.
Además, mapa.keys son las claves del mapa, mapa[clave].orEmpty() cambia un null por una lista vacía (listOf()) y sum() suma los números de una lista. Todo junto, en un ejemplo con notas de examen en vez de partidos, explicado línea a línea:
data class Examen(val alumnoA: String, val alumnoB: String, val notaA: Int, val notaB: Int)
data class Fila(val alumno: String, val total: Int, val examenes: Int)
fun main() {
val examenes = listOf(Examen("Eva", "Leo", 7, 9), Examen("Leo", "Sara", 6, 6))
// flatMap: la lambda devuelve una LISTA por examen (aquí, dos pares) y flatMap las une en una sola
val notas = examenes.flatMap { listOf(it.alumnoA to it.notaA, it.alumnoB to it.notaB) }
println(notas) // [(Eva, 7), (Leo, 9), (Leo, 6), (Sara, 6)]
// La lambda de flatMap puede llevar dentro un map: cada lista se transforma y luego se unen todas
println(listOf(listOf(1, 2), listOf(3)).flatMap { lista -> lista.map { it * 2 } }) // [2, 4, 6]
// groupBy con DOS lambdas: la 1.ª da la clave del grupo; la 2.ª, lo que se guarda en él
val porAlumno = notas.groupBy({ it.first }, { it.second })
println(porAlumno) // {Eva=[7], Leo=[9, 6], Sara=[6]}
// keys: las claves del mapa. + une dos colecciones en una nueva; toSet() quita los repetidos
val todos = (listOf("Eva", "Iker") + porAlumno.keys).toSet()
println(todos) // [Eva, Iker, Leo, Sara]
val filas = todos.map { alumno ->
// porAlumno[alumno] es null si no hay clave (Iker); orEmpty() lo cambia por una lista vacía
val suyas = porAlumno[alumno].orEmpty()
Fila(alumno, total = suyas.sum(), examenes = suyas.size) // sum(): suma los números
}
// Un comparador dice cuál de dos elementos va antes:
val orden = compareByDescending<Fila> { it.total } // 1.º: más total primero (<Fila>: el tipo de los elementos)
.thenByDescending { it.examenes } // desempate: más exámenes primero
.thenBy { it.alumno } // desempate final: nombre de la A a la Z
filas.sortedWith(orden).forEach { println(it) } // sortedWith ordena con el comparador
}Al ejecutarlo, imprime:
[(Eva, 7), (Leo, 9), (Leo, 6), (Sara, 6)]
[2, 4, 6]
{Eva=[7], Leo=[9, 6], Sara=[6]}
[Eva, Iker, Leo, Sara]
Fila(alumno=Leo, total=15, examenes=2)
Fila(alumno=Eva, total=7, examenes=1)
Fila(alumno=Sara, total=6, examenes=1)
Fila(alumno=Iker, total=0, examenes=0)Reto · Ejercicio: lo que gasta cada cliente
En el Kotlin Playground, fuera de tu proyecto.
Una tienda guarda cada pedido como Pedido(cliente, precios), con la lista de precios de lo que compró. Con estos datos:
data class Pedido(val cliente: String, val precios: List<Int>)
val pedidos = listOf(
Pedido("Ana", listOf(10, 5)),
Pedido("Leo", listOf(8)),
Pedido("Ana", listOf(20)),
)
val registrados = listOf("Ana", "Leo", "Zoe") // Zoe aún no ha comprado nadaCalcula cuánto ha gastado cada cliente registrado (Zoe, 0) y escribe una línea por cliente, del que más gastó al que menos y, a igualdad, por nombre. Usa flatMap para convertir cada pedido en una pareja cliente to precio por cada precio, groupBy con dos lambdas para agrupar por cliente, orEmpty() para el cliente sin compras y sortedWith con un comparador (que lleva el tipo de los elementos entre <>: aquí, compareByDescending<Pair<String, Int>> { … }).
La salida tiene que ser exactamente esta:
Ana: 35
Leo: 8
Zoe: 0Ver solución
data class Pedido(val cliente: String, val precios: List<Int>)
fun main() {
val pedidos = listOf(
Pedido("Ana", listOf(10, 5)),
Pedido("Leo", listOf(8)),
Pedido("Ana", listOf(20)),
)
val registrados = listOf("Ana", "Leo", "Zoe")
// flatMap: por cada pedido, una pareja (cliente, precio) por cada precio; todas en una sola lista
val compras = pedidos.flatMap { pedido -> pedido.precios.map { precio -> pedido.cliente to precio } }
// groupBy con dos lambdas: clave = cliente, valor = el precio
val preciosPorCliente = compras.groupBy({ it.first }, { it.second })
// Registrados y los que ya compraron, sin repetir; orEmpty() cambia el null de Zoe por una lista vacía
val clientes = (registrados + preciosPorCliente.keys).toSet()
val gastos = clientes.map { cliente -> cliente to preciosPorCliente[cliente].orEmpty().sum() }
// Comparador: más gasto primero; a igualdad, por nombre
gastos
.sortedWith(compareByDescending<Pair<String, Int>> { it.second }.thenBy { it.first })
.forEach { (cliente, total) -> println("$cliente: $total") }
}Paso · La tabla de posiciones y los goleadores
Objetivo. Un fichero nuevo, Tabla.kt, con FilaDePosicion, la función tablaDePosiciones(partidos, equipos) y la función tablaDeGoleadores(partidos); después, hacer que Main imprima la tabla con tres equipos y dos goleadores y copiar TablaTest.kt.
En la vista Android, despliega torneo → kotlin+java. Allí hay dos paquetes con el mismo nombre: el que lleva (test) guarda los tests y el que no lo lleva guarda el código del programa. Tabla.kt va en el segundo: clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (sin (test)) → New → Kotlin Class/File → elige File, escribe Tabla y pulsa Intro. Main.kt ya existe: ábrelo con doble clic.
Las columnas de la tabla usan las abreviaturas habituales en las ligas: PJ partidos jugados, G ganados, E empatados, P perdidos, GF goles a favor, GC goles en contra, DG diferencia de goles (GF menos GC) y Pts puntos.
El plan de tablaDePosiciones, en cinco pasos:
flatMap: cada partido cuenta dos veces, una desde el lado local y otra desde el visitante.flatMapconvierte cada partido en dos entradas y las une en una sola lista. Cada entrada es unTriple: como unPair, pero con tres partes (first,secondythird): el equipo, sus goles a favor y sus goles en contra.groupBycon dos lambdas: agrupa por equipo y guarda solo su marcador (a favor, en contra).toSet: une los equipos que ya jugaron con los que se pasen aparte, sin repetir.mapconcountysumOf: cada equipo se convierte en una fila con sus columnas.sortedWith: ordena por puntos, diferencia de goles, goles a favor y nombre. Hay cuatro criterios encadenados ysortedByadmite uno solo, así que se usa un comparador, como en el ejemplo de las notas.
tablaDeGoleadores te la damos escrita, más abajo: no hay que inventarla, solo pegarla al final de Tabla.kt y leer sus comentarios. Usa la secuencia de la lección anterior.
Una cosa de formato: en FilaDePosicion verás una coma después del último parámetro (val puntos: Int,) y después del último argumento de una llamada. Kotlin la permite y es opcional; sirve para añadir una línea sin tocar la anterior.
Dónde va cada cosa: Tabla.kt es un fichero nuevo entero (tú escribes FilaDePosicion y tablaDePosiciones; tablaDeGoleadores se pega al final, como se explica abajo). En Main.kt, pega el bloque nuevo en lugar de la última } de main (el bloque ya trae esa llave al final); el resto del fichero no cambia.
Intenta escribirlo tú, en este orden:
FilaDePosicion: una fila de la tabla. Guarda el equipo, los partidos jugados, ganados, empatados y perdidos, los goles a favor y en contra y los puntos; la diferencia de goles se calcula, no se guarda.tablaDePosiciones(partidos, equipos): recibe los partidos jugados y, opcionalmente, equipos que deben salir aunque no hayan jugado. Devuelve una fila por equipo con los puntos depuntosPor, ordenada por puntos, después por diferencia de goles, después por goles a favor (todo de mayor a menor) y, si siguen empatados, por nombre. Sigue el plan de cinco pasos de arriba.main: añade a los Lobos (Pedro, dorsal 4), juega una jornada de tres partidos (el Rayo FC-Toros de antes, un Toros-Lobos que gana Toros con gol de Iván y un Lobos-Rayo FC que gana Rayo con gol de Ana) e imprime la tabla y, debajo, los goleadores. La cabecera es"Pos Equipo PJ G E P GF GC DG Pts"y cada fila usaformatcon"%2d %-7s %2d %2d %2d %2d %3d %2d %3d %3d"(diez valores: puesto, equipo, PJ, G, E, P, GF, GC, DG y Pts). Cada goleador se imprime comoAna: 3.
Tras las líneas de la lección anterior, debe verse:
Pos Equipo PJ G E P GF GC DG Pts
1 Rayo FC 2 2 0 0 3 1 2 6
2 Toros 2 1 0 1 2 2 0 3
3 Lobos 2 0 0 2 0 2 -2 0
Ana: 3
Iván: 2Cuando termines, abre el bloque y compara con tu código. Los bloques con ruta llevan la del fichero en tu proyecto.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.torneo
// Una fila de la tabla de posiciones.
data class FilaDePosicion(
val equipo: Equipo,
val jugados: Int,
val ganados: Int,
val empatados: Int,
val perdidos: Int,
val golesAFavor: Int,
val golesEnContra: Int,
val puntos: Int,
) {
val diferencia: Int get() = golesAFavor - golesEnContra
}
// Tabla de posiciones a partir de los partidos jugados.
// `equipos` permite que salgan también los que aún no han jugado.
fun tablaDePosiciones(partidos: List<Partido>, equipos: List<Equipo> = emptyList()): List<FilaDePosicion> {
// Cada partido cuenta dos veces: una desde el lado local y otra desde el visitante.
// Cada elemento es (equipo, goles a favor, goles en contra).
val apariciones = partidos.flatMap { partido ->
listOf(
Triple(partido.local, partido.golesLocal, partido.golesVisitante),
Triple(partido.visitante, partido.golesVisitante, partido.golesLocal),
)
}
// groupBy con dos lambdas: la clave (equipo) y el valor (el marcador visto por él).
// El resultado es un Map<Equipo, List<Pair<Int, Int>>>.
val marcadoresPorEquipo = apariciones.groupBy({ it.first }, { it.second to it.third })
// Un Set no admite repetidos: une los equipos dados con los que ya jugaron.
val todos: Set<Equipo> = (equipos + marcadoresPorEquipo.keys).toSet()
return todos
.map { equipo ->
val marcadores = marcadoresPorEquipo[equipo].orEmpty()
FilaDePosicion(
equipo = equipo,
jugados = marcadores.size,
ganados = marcadores.count { (aFavor, enContra) -> aFavor > enContra },
empatados = marcadores.count { (aFavor, enContra) -> aFavor == enContra },
perdidos = marcadores.count { (aFavor, enContra) -> aFavor < enContra },
golesAFavor = marcadores.sumOf { it.first },
golesEnContra = marcadores.sumOf { it.second },
puntos = marcadores.sumOf { (aFavor, enContra) -> puntosPor(aFavor, enContra) },
)
}
.sortedWith(
compareByDescending<FilaDePosicion> { it.puntos }
.thenByDescending { it.diferencia }
.thenByDescending { it.golesAFavor }
.thenBy { it.equipo.nombre },
)
}Este es el bloque de Main.kt (el … marca el código que ya tienes):
…
// Colecciones: una jornada completa, la tabla y los goleadores.
val lobos = Equipo("Lobos", listOf(Jugador("Pedro", 4)))
val jornada = listOf(
alFinal,
Partido(toros, lobos).registrar(Gol(20, ivan, toros)),
Partido(lobos, rayo).registrar(Gol(40, ana, rayo)),
)
println("Pos Equipo PJ G E P GF GC DG Pts")
tablaDePosiciones(jornada).forEachIndexed { indice, fila ->
println(
"%2d %-7s %2d %2d %2d %2d %3d %2d %3d %3d".format(
indice + 1, fila.equipo.nombre, fila.jugados, fila.ganados, fila.empatados,
fila.perdidos, fila.golesAFavor, fila.golesEnContra, fila.diferencia, fila.puntos,
),
)
}
tablaDeGoleadores(jornada).forEach { (jugador, goles) -> println("${jugador.nombre}: $goles") }
}Pega la tabla de goleadores. Al final de Tabla.kt, después de tablaDePosiciones, pega esta función (ya está en el bloque de Main.kt, que la llama):
// Tabla de goleadores: cada jugador con sus goles, de más a menos.
// Usa una Sequence: las operaciones se encadenan y se ejecutan elemento a
// elemento, sin crear una lista intermedia entre paso y paso.
fun tablaDeGoleadores(partidos: List<Partido>): List<Pair<Jugador, Int>> =
partidos.asSequence()
.flatMap { it.eventos.asSequence() }
.filterIsInstance<Gol>()
.groupingBy { it.jugador }
.eachCount()
.toList()
.sortedWith(compareByDescending<Pair<Jugador, Int>> { it.second }.thenBy { it.first.nombre })Ahora, el test. TablaTest.kt prueba el orden de la tabla, cada columna, el desempate, los equipos que aún no han jugado y la tabla vacía. Comprueba lo que acabas de escribir y no enseña nada nuevo de kotlin.test, así que no hace falta que lo escribas: lo copias. Las lecciones Extensiones y Comparadores, Iterable y el ranking lo modifican y suponen que ya lo tienes.
- En
torneo → kotlin+java, clic derecho sobre el paquetecom.sebastiannarvaez.marcador.torneo (test)→ New → Kotlin Class/File → File, escribeTablaTesty pulsa Intro. Se abre un fichero con solo la líneapackage. - Pulsa el botón de abajo: copia el contenido al portapapeles (no escribe nada en el proyecto).
- Con el cursor en el fichero vacío, selecciona todo (Cmd+A) y pega (Cmd+V). Lo pegado trae su propia línea
package, así que sustituye a la tuya.
Punto de control
- Ejecuta el programa desde la Terminal de Android Studio (View → Tool Windows → Terminal):
./gradlew :torneo:runtermina enBUILD SUCCESSFULy las últimas líneas son:
Pos Equipo PJ G E P GF GC DG Pts
1 Rayo FC 2 2 0 0 3 1 2 6
2 Toros 2 1 0 1 2 2 0 3
3 Lobos 2 0 0 2 0 2 -2 0
Ana: 3
Iván: 2- Los puntos siguen la regla de
puntosPor: 3 por victoria, 1 por empate. ./gradlew :torneo:testpasa, conTablaTest.ktcopiado. Abre el informe como en Bucles y tu primer test (open torneo/build/reports/tests/test/index.html) y comprueba que arriba pone 41 tests: los 36 de antes más los 5 deTablaTest.- Cambia el orden de los partidos en
jornaday la tabla sale igual: no depende del orden de entrada. Deshaz el cambio antes de seguir.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Cada partido cuenta dos veces en la tabla: una por el local y otra por el visitante. ¿Qué hace
partidos.flatMap { listOf(a, b) }?2.¿Qué hace
notas.groupBy({ it.first }, { it.second })sobre una lista de parejas (alumno, nota)?3.Para ordenar la tabla por puntos y, a igualdad, por diferencia de goles y por nombre, ¿por qué se usa
sortedWithcon un comparador y nosortedBy?4.Un
Mapde notas no tiene la clave"Iker". ¿Qué devuelvenotas["Iker"].orEmpty().sum()?5.Con
compareByDescending<Fila> { it.total }.thenBy { it.alumno }, dos alumnos con el mismo total, ¿cómo quedan?