👻

[Kotlin]Collection操作を8つの分類で解説③Grouping (groupBy / groupingBy)

2024/03/30に公開

Collection操作を8つの分類で解説③Grouping collection

さあ〜〜前回から引き続き書いていきます!!!^^
3回目になりました。今回は、Grouping を書いていきます!

< 分類表 >

collection operation 分類表

8つに分類したらこうなる!

collections operations 概要 関数
transformation
前の記事はここです⭐️
既存のコレクションから新しいコレクションを構築 Map, Zip, Associate, Flatten, StringPresentation
それぞれのメソッドは詳細でまた別途記載
Filtering
前の記事はここです⭐️
元のコレクションを変更せずに特定の条件で要素をフィルタリングし、新しいコレクションを返す filter() (filterIndexed()) ,filterNot(), filterNotNull(), partition(),any,all,none
Plus / Minus コレクションに要素を追加または削除するもの plus(), minus()
Grouping
→ 今回はここ書きます!
コレクションの要素を特定の基準でグループ化するもの groupBy(), eachCount(),fold(), reduce(),aggregate()
Retrieving collection parts コレクションの一部を取得するもの slice(),Chunked ,Windowed ,take()(takeLast(),takeLastWhile(),takeWhile()),drop()(dropLast(),dropLastWhile(),dropWhile()),
Retrieving Single Elements コレクションから単一の要素を取得するもの By position ( elementAt(), elementAtOrElse(), elementAtOrNull()), by condition(first(), firstOrNull(), last() , lastOrNull(),find() ),Check Element Existence(contains(), isEmpty(), isNotEmpty())
Ordering コレクションの要素を並べ替えるもの Natural order(sorted(), sortedDescending()), Custom orders( sortedBy(), sortedByDescending(), sortedWith() ),Reverse order( reversed()),Random order( shuffled())
Aggreregate Operation コレクションの要素を集計するもの reduce(), fold(), count(), sum(), average(), max(), min() etc

4.Grouping

関数 概要
groupBy ラムダ関数を受け取り、その内容を処理しMap を返す
groupingBy groupByと同じくグループ化するが、遅延評価を行い、Grouping オブジェクトを介して後で処理を適用

groupBy

  • ラムダ関数を受け取り、Map を返す
     → このMAPは...各キーはラムダの結果で、対応する値はその結果が返す要素のリスト

基本形

fun <T, K> Iterable<T>.groupBy(
        keySelector: (T) -> K
): Map<K, List<T>>
  • groupBy()は、元のコレクションを指定されたキーに基づいてグループ化する。
  • keySelectorは、各要素に適用され、その結果に基づいてグループが作成される。
  • 返されるMapの各エントリは、keySelectorの結果をキーとし、それにマッチする要素のリストを値として持つ。

ex.

val numbers = listOf("one", "two", "three", "four", "five")
// 要素の最初の文字を大文字に変換してキーに
println(numbers.groupBy { it.first().uppercase() })
// 出力 {O=[one], T=[two, three], F=[four, five]}

// キーと値の変換をする
println(numbers.groupBy(keySelector = { it.first() }, valueTransform = { it.uppercase() }))
// 出力 {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}

groupingBy

  • groupBy() と同様にコレクション要素をグループ化するが、一度にすべてのグループに操作を適用したい場合は、groupingByを使用する。
  • groupingBy()遅延評価を行い、中でGroupingオブジェクトを生成しています。
  • Groupingオブジェクトは、要素をキーでグループ化するための機能を提供し、そのグループに対して操作を適用することができる

遅延評価とは:
評価を遅延して行うこと。
関数や式が呼び出されたときではなく、実際にその結果が必要な場合に評価や処理が行われる。

  fun <T, K> Iterable<T>.groupingBy(
          keySelector: (T) -> K
  ): Grouping<T, K>
source
/**
 * Creates a [Grouping] source from a collection to be used later with one of group-and-fold operations
 * using the specified [keySelector] function to extract a key from each element.
 * 
 * @sample samples.collections.Grouping.groupingByEachCount
 */
@SinceKotlin("1.1")
public inline fun <T, K> Iterable<T>.groupingBy(crossinline keySelector: (T) -> K): Grouping<T, K> {
    return object : Grouping<T, K> {
        override fun sourceIterator(): Iterator<T> = this@groupingBy.iterator()
        override fun keyOf(element: T): K = keySelector(element)
    }
}

→ この Grouping は、グループ化された要素のSetを表し、
その要素に対して集計操作を行うための機能を提供する.その機能が、以下になる。

eachCount Grouping オブジェクト内の各グループ内の要素数をカウント
fold コレクションの各要素に対して指定された初期値と累積演算を適用
reduce コレクションの各要素に対して指定された演算を順番に適用し、結果を一つの値にまとめる
aggregate グループ化された要素に対して与えられた操作を順次適用し、その結果を返す汎用的なメソッド.
foldreduceだけでは十分でない場合に、カスタム操作を実装するために使用

補足:Groupingについて

source Grouping
/**
 * Represents a source of elements with a [keyOf] function, which can be applied to each element to get its key.
 *
 * A [Grouping] structure serves as an intermediate step in group-and-fold operations:
 * they group elements by their keys and then fold each group with some aggregating operation.
 *
 * It is created by attaching `keySelector: (T) -> K` function to a source of elements.
 * To get an instance of [Grouping] use one of `groupingBy` extension functions:
 * - [Iterable.groupingBy]
 * - [Sequence.groupingBy]
 * - [Array.groupingBy]
 * - [CharSequence.groupingBy]
 *
 * For the list of group-and-fold operations available, see the [extension functions](#extension-functions) for `Grouping`.
 */
@SinceKotlin("1.1")
public interface Grouping<T, out K> {
    /** Returns an [Iterator] over the elements of the source of this grouping. */
    fun sourceIterator(): Iterator<T>
    /** Extracts the key of an [element]. */
    fun keyOf(element: T): K
}

eachCount

  • Grouping オブジェクト内の各グループ内の要素数をカウントする.
  • 各グループ内の要素の数を数えて、各キーに対応する要素の数を値として持つマップを返す

groupingBy() 関数を使用し指定された条件に基づいて要素がグループ化され、
各グループに対してキーが作成される
その後、eachCount() 関数を使用すると、各グループ内の要素の数がカウントされ、
その結果がキーに対応する値としてマップに追加される。

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

ex.

val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.groupingBy { it.first() }.eachCount())
// 出力 {o=1, t=2, f=2, s=1}

各文字列の先頭文字をキーとして使用してリストをgroupingBy()でグループ化して、
(同じ先頭文字を持つ文字列が同じグループになる)
次に、eachCount() を使用し、各グループ内の要素数を数えている。
その値を各グループのキーに対応する値としマップを作って返している!

fold / reduce

fold() and reduce() perform fold and reduce operations on each group as a separate collection and return the results.
各グループを個別のコレクションとして fold および reduce オペレーションを実行し、その結果を返す。

fold

  • コレクションの各要素に対して指定された初期値と累積演算を適用し結果を返す
  • 最初の引数は初期値であり、2番目の引数は累積演算を表すラムダ関数。
  • ラムダ関数は、累積された値(アキュムレータ)と次の要素を受け取り、次の累積値を返す。
  • 最終的な結果は、最後の要素が処理された後にアキュムレータの値を持つMap

accumulator: アキュムレータ
演算を行う際,被演算子の値を保持するためのメモリ

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

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

initialValueSelector ラムダ関数
=> 各グループのキーと要素を受け取り、各グループの初期値を選択。
operation ラムダ関数
=> 各グループのキー、累積された値(アキュムレータ)、および次の要素を受け取り、
次の累積値を計算。戻り値は、各キーと対応する累積値を持つMap。

ex.1

val fruits = listOf("apple", "apricot", "banana", "blueberry", "cherry", "coconut")

// リスト内の各要素の最初の文字をキーとしてgroupingByでグループ化
val evenFruits = fruits.groupingBy { it.first() } // {a=[apple, apricot], b=[banana, blueberry], c=[cherry, coconut]}
    .fold(listOf<String>()) { acc, e -> if (e.length % 2 == 0) acc + e else acc }

println(evenFruits) // {a=[], b=[banana], c=[cherry]}
  • groupingBy { it.first() }で、リスト内の各要素の最初の文字をキーとしてグループ化
    この時点での形は,{a=[apple, apricot], b=[banana, blueberry], c=[cherry, coconut]}となる。
  • fold(listOf<String>()) { acc, e -> if (e.length % 2 == 0) acc + e else acc }
    初期値として空のリストを使用し fold操作(ラムダないの処理)を実行。
    -> 各要素の長さが偶数の場合、要素を現在のアキュムレータに追加します。それ以外の場合は、アキュムレータをそのまま返す。

ex.2

val fruits = listOf("cherry", "blueberry", "citrus", "apple", "apricot", "banana", "coconut")
// `fruits` リスト内の各要素の最初の文字をキーとしてグループ化
val evenFruits = fruits.groupingBy { it.first() } // {c=[cherry, citrus, coconut], b=[blueberry, banana], a=[apple, apricot]}
    .fold({ key, _ -> key to mutableListOf<String>() },
          { _, accumulator, element ->
              accumulator.also { (_, list) -> if (element.length % 2 == 0) list.add(element) }
          }) // 要素の長さが偶数の場合、リストに要素が追加

val sorted = evenFruits.values.sortedBy { it.first }
println(sorted) // [(a, []), (b, [banana]), (c, [cherry, citrus])]
  • fruits リスト内の各要素の最初の文字をキーとしてグループ化.
    この時点での形は、{c=[cherry, citrus, coconut], b=[blueberry, banana], a=[apple, apricot]}
  • fold 関数は、ここでは初期値として、キーと空のミュータブルリストを持つペアを生成。
  • ラムダ内の各要素が処理され、要素の長さが偶数の場合、リストに要素が追加される。
  • ペアのリストをキーの最初の文字でソート

reduce

  • リスト内の各要素に対して指定された演算を順番に適用し、結果を一つの値にまとめる
  • reduce 関数は、初期値を必要としない。最初の要素が初期値として使用される。
    → そのため、空のリストに reduce を適用すると例外が発生する。
inline fun <S, T : S, K> Grouping<T, K>.reduce(
    operation: (key: K, accumulator: S, element: T) -> S
): Map<K, S>

ex.

// 文字列リスト `animals` を最初の文字でグループ化し、
// 母音を含むものの中で最大のものだけを集める
val animals = listOf("raccoon", "reindeer", "cow", "camel", "giraffe", "goat")

// compareBy 関数を使用して、与えられた文字列をカウントして、その結果を基に比較
val compareByVowelCount = compareBy { s: String -> s.count { it in "aeiou" } }

val maxVowels = animals.groupingBy { it.first() }.reduce { _, a, b -> maxOf(a, b, compareByVowelCount) }

println(maxVowels) // {r=reindeer, c=camel, g=giraffe}
  • compareByVowelCount は、文字列内の母音の数に基づいて比較するための Comparator を定義。
  • compareBy 関数を使用して、与えられた文字列をカウントして、その結果を基に比較。
  • maxVowels は、 animals リストを最初の文字に基づいてグループ化し、
    各グループ内の要素を compareByVowelCount で比較し、最大の要素を見つける。
  • そして、各グループのキー(最初の文字)に対して、グループ内の最大の要素をマップに格納。

aggregate

aggregate()関数は、グループ化された要素に対して与えられた操作を順次適用し、その結果を返す汎用的なメソッド。
通常、各グループに対して同じ操作を適用する必要がある場合に使用。
fold()reduce()は、各グループ内の要素を1つの値に畳み込むのに対して、aggregate()はより柔軟性があり、カスタム操作を実行する際に役立つ。
たとえば、平均値や合計値などの結果を計算するために使用することができる。

// 以下は、数値のグループ化された平均値を計算する例です
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val averageByGroup = numbers.groupingBy { it % 2 == 0 } // 偶数と奇数でグループ化
    .aggregate { key, accumulator: Int?, element, first ->
        if (first) element // 最初の要素の場合、そのまま返す
        else accumulator?.plus(element) ?: element // 2つ目以降の要素の場合、前の結果に現在の要素を加算
    }

println(averageByGroup) // 出力: {false=25, true=30} (偶数グループの平均: 30, 奇数グループの平均: 25)


ということで、Groupingについては以上です!!!!!

Discussion