Operaciones con colecciones IV: Operaciones de agrupamiento

Operaciones con colecciones IV: Operaciones de agrupamiento

Retomando la serie dedicada a las funciones para realizar operaciones con las colecciones, en esta entrada nos centramos en las operaciones de agrupamiento (Grouping).

La biblioteca estándar de Kotlin proporciona funciones de extensión para agrupar los elementos de una colección, siendo la función básica groupBy(). Esta función toma como argumento una función lambda y devuelve un diccionario (Map). En este diccionario cada clave es el resultado de la función lambda y cada valor correspondiente es una lista de los elementos de ese resultado. Esta función se puede utilizar, por ejemplo, para agrupar una lista de elementos tipo String por su primera letra.
val lista = 1.rangeTo(10).toList()
val paresImpares = lista.groupBy { it % 2 }
println(paresImpares)
// {1=[1, 3, 5, 7, 9], 0=[2, 4, 6, 8, 10]}

groupBy() también se puede utilizar con un segundo argumento lambda, una función de transformación del valor, y en este caso las claves producidas por la función keySelector se asignan a los resultados de la función de transformación de los valores en lugar de a los elementos originales. Otros ejemplos:
val numeros = listOf("uno", "dos", "tres", "cuatro", "cinco")
println(numeros.groupBy { it.first().toUpperCase() })
// {U=[uno], D=[dos], T=[tres], C=[cuatro, cinco]}
println(numeros.groupBy { it[1] })
// {n=[uno], o=[dos], r=[tres], u=[cuatro], i=[cinco]}
println(numeros.groupBy { it.length })
// {3=[uno, dos], 4=[tres], 6=[cuatro], 5=[cinco]}
println(numeros.groupBy { it.first() == 'c' })
// {false=[uno, dos, tres], true=[cuatro, cinco]}
println(numeros.groupBy(keySelector = { it.first() }, valueTransform = { it.toUpperCase() }))
// {u=[UNO], d=[DOS], t=[TRES], c=[CUATRO, CINCO]}

Si queremos agrupar elementos y luego aplicar una operación a todos los grupos a la vez, podemos usar la función groupingBy() que devuelve una instancia del tipo Grouping que permite aplicar operaciones a todos los grupos de manera que los grupos en realidad se crean justo antes de la ejecución de la operación.

Grouping soporta las siguientes operaciones:
  • eachCount(), que  cuenta los elementos en cada grupo.
  • fold() y reduce() que realizan operaciones en cada grupo como una colección separada y devuelve los resultados, con la diferencia de que fold toma un valor inicial que recibirá como argumento la primera invocación de la lambda mientras que reduce no toma ningún valor inicial sino que comienza con el primer elemento de la colección.
  • aggregate(), que aplica una operación dada posteriormente a todos los elementos en cada grupo y devuelve el resultado.

Seguramente agregate() es la forma genérica de realizar cualquier operación en una agrupación y se usa para implementar operaciones personalizadas cuando fold y reduce no son suficientes.
val numeros = listOf("uno", "dos", "tres", "cuatro", "cinco", "seis")
println(numeros.groupingBy { it.first() }.eachCount())
// {u=1, d=1, t=1, c=2, s=1}
println(numeros.groupingBy { it.length }.eachCount())
// {3=2, 4=2, 6=1, 5=1}

println(listOf(1, 2, 3).fold(0) { sum, element -> sum + element })
// 6
println(listOf(1, 2, 3).reduce { sum, element -> sum + element })
// 6
println(listOf(1, 2, 3).fold(1) { sum, element -> sum + element })
// 7
println(listOf(1, 2, 3, 4, 5).fold(10) { max, element ->
    if (element > max) element else max
})
// 10

Otro ejemplo con groupBy:
class Contacto(private val nombre: String, val edad: Int, val ciudad: String) {
    override fun toString(): String = nombre
}

fun main() {
    val contactos = mutableListOf(
        Contacto("Andres", 22, "Barcelona"),
        Contacto("Víctor", 32, "Alicante"),
        Contacto("Lucía", 31, "Madrid"),
        Contacto("Tornasol", 73, "Vigo"),
        Contacto("Simon", 32, "Albacete"),
        Contacto("Nikita", 42, "Valencia"),
        Contacto("Igor", 20, "Valencia"),
        Contacto("Alex", 65, "Barcelona"),
        Contacto("Sergio", 52, "Madrid"),
        Contacto("Miguel", 33, "Madrid"),
        Contacto("David", 70, "Valencia")
    )

    val contactosPorCiudad = contactos.groupBy { it.ciudad }
    println(contactosPorCiudad)
    // {Barcelona=[Andres, Alex], Alicante=[Víctor], Madrid=[Lucía, Sergio, Miguel], Vigo=[Tornasol], Albacete=[Simon], Valencia=[Nikita, Igor, David]}
    val contactosMayores40 = contactos.groupBy { it.edad > 40 }
    println(contactosMayores40)
    // {false=[Andres, Víctor, Lucía, Simon, Igor, Miguel], true=[Tornasol, Nikita, Alex, Sergio, David]}
    if (true in contactosMayores40) println("${contactosMayores40[true]}")
    // [Tornasol, Nikita, Alex, Sergio, David]
}

Comentarios

Entradas populares

I/O: entrada y salida de datos en consola

Recursos gratis para aprender Kotlin

Lectura y escritura de archivos