Función groupingBy En Kotlin

En este tutorial verás cómo usar a la función groupingBy en Kotlin para agrupar los elementos de una colección y luego aplicarles una operación a todos los grupos generados, generando así un mapa con los resultados por cada grupo.

Función groupingBy()

La función de extensión groupingBy() funciona similar a groupBy(). Ambas toman una función lambda que hace de selector de clave y así crear grupos de elementos con dichas claves.

La diferencia está en que groupingBy() retorna en una instancia del tipo Grouping. Esta interfaz te permite aplicar funciones de agregación sobre los grupos, las cuales retornan un mapa con los valores finales de cada grupo:

// groupingBy() en arreglos
inline fun <T, K> Array<out T>.groupingBy(
    crossinline keySelector: (T) -> K
): Grouping<T, K>

// groupingBy() en iterables
inline fun <T, K> Iterable<T>.groupingBy(
    crossinline keySelector: (T) -> K
): Grouping<T, K>

Las funciones que puedes usar sobre el resultado de groupingBy() son:

  • eachCount(): Cuentas los elementos por cada grupo. El resultado en un mapa con pares cuyo valor es la cantidad contada
  • fold(): Aplica una operación acumulativa a partir de los valores de cada grupo. El valor inicial del acumulado lo pasas como primer parámetro
  • reduce(): Aplicar una operación que reduce a una instancia como resultado de cada grupo
  • aggregate(): Esta función es la base para generar las otras funciones nombradas. Toma una operación y la aplica sobre cada elemento para obtener el resultado acumulado

Contar El Número De Empleados En Cada Departamento

Tomemos como ejemplo un sencillo diseño para modelar a los empleados de una compañía que pertenecen a un departamento.

data class Employee(val name: String, val salary: Int, val department: String) {
    override fun toString(): String {
        return "%-10s %4s   %s".format(name, salary, department)
    }
}

val employees = listOf(
    Employee("Carolina", 2000, "Finanzas"),
    Employee("Shirley", 1500, "Recursos humanos"),
    Employee("Carlos", 1250, "Finanzas"),
    Employee("Jill", 4200, "Marketing"),
    Employee("Pedro", 500, "Logística"),
    Employee("Ana", 550, "Logística"),
    Employee("Santiago", 1200, "Logística"),
)

fun main() {
    employees.forEach(::println)
}

Salida:

Carolina   2000   Finanzas
Shirley    1500   Recursos humanos
Carlos     1250   Finanzas
Jill       4200   Marketing
Pedro       500   Logística
Ana         550   Logística
Santiago   1200   Logística

La clase Employee se constituye del nombre, salario y departamento a que pertenece el empleado. Y como ves la lista employees contiene siete empleados que usaremos para probar las diferentes funciones asociadas a Grouping.

El primer ejemplo será contar la cantidad de empleados que existen por departamento actualmente. Esto significa que debemos agrupar a la lista por la propiedad department y luego aplicar eachCount():

fun <T, K> Grouping<T, K>.eachCount(): Map<K, Int>

La función eachCount() retorna un mapa con pares cuyos valores son la cantidad contada por cada clave.

fun main() {
    val employeesByDepartment = employees.groupingBy(Employee::department).eachCount()
    println("Cantidad de empleados por departamento:")
    employeesByDepartment.forEach { (department, count) ->
        println("$department tiene $count empleados")
    }
}

Salida:

Cantidad de empleados por departamento:
Finanzas tiene 2 empleados
Recursos humanos tiene 1 empleados
Marketing tiene 1 empleados
Logística tiene 3 empleados

Calcular La Suma De Salarios Por Departamento

Para computar la suma total en cada departamento recurrimos de nuevo a la agrupación de la lista employees por la propiedad department.

La operación de suma es posible conseguirla a través del método fold() o agreggate(). Nuestro objetivo es acumular la suma de la propiedad salary en cada grupo existente.

inline fun <T, K, R> Grouping<T, K>.fold(
    initialValue: R,
    operation: (accumulator: R, element: T) -> R
): Map<K, R>

Veamos la solución:

fun main() {
    val salariesSumByDepartment = employees
        .groupingBy(Employee::department)
        .fold(0) { sum, employee -> sum + employee.salary }

    println("Suma total de salarios por departamento:")
    salariesSumByDepartment.forEach { (department, salarySum) ->
        println("$department: $$salarySum ")
    }
}

Salida:

Suma total de salarios por departamento:
Finanzas: $3250 
Recursos humanos: $1500 
Marketing: $4200 
Logística: $2250 

Obtener El Salario Más Alto Por Departamento

En este caso la operación de acumulación que necesitamos no es aritmética, si no de reducción. Necesitamos imprimir un solo valor seleccionado por cada departamento luego de usar groupingBy().

La solución consta del uso de la función reduce(), la cual nos fuerza a elegir una instancia de Employee en la función lambda de operación.

inline fun <S, T : S, K> Grouping<T, K>.reduce(
    operation: (key: K, accumulator: S, element: T) -> S
): Map<K, S>

Como ves en la sintaxis, el acumulado comparte al tipo S en común con element. El cuerpo de la operación asigna al acumulado el empleado que se considere preponderante en cuanto a salario:

fun main() {
    val salariesSumByDepartment = employees
        .groupingBy(Employee::department)
        .reduce { _, highestSalaryEmployee, employee ->
            if (highestSalaryEmployee.salary > employee.salary) highestSalaryEmployee else employee
        }
    salariesSumByDepartment.forEach { (department, employee) ->
        println("${employee.salary} es el mayor salario en $department")
    }
}

Salida:

2000 es el mayor salario en Finanzas
1500 es el mayor salario en Recursos humanos
4200 es el mayor salario en Marketing
1200 es el mayor salario en Logística

La Función aggregate()

La función aggregate() recibe una función de operación para acumular valores en un mapa con respecto a los grupos que se generan de Grouping.

inline fun <T, K, R> Grouping<T, K>.aggregate(
    operation: (key: K, accumulator: R?, element: T, first: Boolean) -> R
): Map<K, R>

Normalmente se usa cuando fold() y reduce() no cumplen con nuestras necesidades.

Por ejemplo, si quisieras obtener el valor de la suma de salarios por departamentos con aggregate(), podrías usar el parámetro first para inicializar el valor del acumulado y luego ir sumando los valores subsecuentes:

val salariesSumByDepartment = employees
    .groupingBy(Employee::department)
    .aggregate { _, sum: Int?, employee, first ->
        if (first) {
            employee.salary
        } else {
            sum!! + employee.salary
        }
    }

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!