diff --git a/app/src/test/java/com/example/learningtest/collection/functional/AggregationTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/AggregationTest.kt new file mode 100644 index 0000000..223b25d --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/AggregationTest.kt @@ -0,0 +1,203 @@ +package com.example.learningtest.collection.functional + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class AggregationTest : FreeSpec({ + data class User( + val name: String, + val age: Int, + ) + + "List Aggregation" - { + + "count - 컬렉션의 요소 개수" { + val numbers: List = listOf(1, 2, 3, 4, 5) + + numbers.count() shouldBe 5 + numbers.count { it % 2 == 0 } shouldBe 2 + } + + "sum - 컬렉션의 숫자 요소 합계" { + val numbers: List = listOf(1, 2, 3, 4, 5) + + numbers.sum() shouldBe 15 + } + + "sumOf - 컬렉션의 특정 속성을 기준으로 숫자 요소 합계" { + val users: List = + listOf( + User("Alice", 30), + User("Bob", 25), + User("Charlie", 35), + ) + + users.sumOf { user -> + user.age.takeIf { age -> age >= 30 } ?: 0 + } shouldBe 65 + } + + "average - 컬렉션의 숫자 요소 평균" { + val numbers: List = listOf(1.0, 2.0, 3.0, 4.0, 5.0) + + numbers.average() shouldBe 3.0 + emptyList().average() shouldBe Double.NaN + } + + "max - 컬렉션에서 가장 큰 요소(빈 컬렉션이면 NoSuchElementException 예외 발생)" { + val numbers: List = listOf(10, 5, 20, 15) + + numbers.max() shouldBe 20 + shouldThrow { + emptyList().max() + } + } + + "maxOrNull - 컬렉션에서 가장 큰 요소 (없으면 null)" { + val numbers: List = listOf(10, 5, 20, 15) + + numbers.maxOrNull() shouldBe 20 + emptyList().maxOrNull() shouldBe null + } + + "maxOf - 특정 속성 기준으로 컬렉션에서 가장 큰 요소 (빈 컬렉션이면 예외)" { + val numbers: List = + listOf( + User("Alice", 30), + User("Bob", 25), + User("Charlie", 35), + ) + + numbers.maxOf { user -> user.age } shouldBe 35 + } + + "maxOfOrNull - 특정 속성 기준으로 컬렉션에서 가장 큰 요소 (없으면 null)" { + val users: List = + listOf( + User("Alice", 30), + User("Bob", 25), + User("Charlie", 35), + ) + + users.maxOfOrNull(User::age) shouldBe 35 + emptyList().maxOfOrNull(User::age) shouldBe null + } + + "minOfOrNull - 특정 속성 기준으로 컬렉션에서 가장 작은 요소 (없으면 null)" { + val users: List = + listOf( + User("Alice", 30), + User("Bob", 25), + User("Charlie", 35), + ) + + users.minOfOrNull(User::age) shouldBe 25 + emptyList().minOfOrNull(User::age) shouldBe null + } + + "reduce - 첫 번째 요소부터 시작하여 누적 연산 (빈 컬렉션에는 사용 불가)" { + val numbers: List = listOf(1, 2, 3, 4) + + // acc: 0, number: 1 -> 1 + // acc: 1, number: 2 -> 3 + // acc: 3, number: 3 -> 2 + // acc: 2, number: 4 -> 6 + numbers.reduce { acc, number -> + if (acc >= number) acc - 1 else acc + number + } shouldBe 6 + + val words: List = listOf("hello", " ", "world") + + words.reduce { acc, word -> acc + word } shouldBe "hello world" + + shouldThrow { + emptyList().reduce { acc, i -> acc + i } + } + } + + "fold - 초기값을 가지고 누적 연산(빈 리스트도 가능)" { + val numbers: List = listOf(1, 2, 3, 4) + val initialValue = 10 + + // acc: 10, number: 1 -> 11 + // acc: 11, number: 2 -> 11 + // acc: 11, number: 3 -> 14 + // acc: 14, number: 4 -> 18 + numbers.fold(initialValue) { acc, number -> + if (number % 2 == 0) acc else acc + number + } + + val words: List = listOf("hello", " ", "world") + val initialString = "Start: " + words.fold(initialString) { acc, s -> acc + s } shouldBe "Start: hello world" + + emptyList().fold(0) { accumulator, element -> accumulator + element } shouldBe 0 + } + + "runningReduce - 각 단계의 누적 결과를 포함하는 리스트 반환 (빈 컬렉션에는 사용 불가)" { // New test case + val numbers: List = listOf(1, 2, 3, 4) + + val runningSum: List = numbers.runningReduce { acc, number -> acc + number } + runningSum shouldBe listOf(1, 3, 6, 10) // 1, 1 + 2, 1 + 2 + 3, 1 + 2 + 3 + 4 + } + + "runningFold - 초기값과 함께 각 단계의 누적 결과를 포함하는 리스트 반환 (빈 리스트도 가능)" { // New test case + val numbers: List = listOf(1, 2, 3, 4) + + val runningNumbersFolded: List = + numbers.runningFold(10) { acc, number -> acc + number } + runningNumbersFolded shouldBe listOf(10, 11, 13, 16, 20) + + val words: List = listOf("a", "b", "c") + + val runningWordsFolded: List = + words.runningFold("Start: ") { acc, word -> acc + word } + runningWordsFolded shouldBe listOf("Start: ", "Start: a", "Start: ab", "Start: abc") + } + } + + "Map Aggregation" - { + val map: Map = mapOf("a" to 1, "b" to 2, "c" to 3, "d" to 4) + + "count - Map의 항목 개수" { + map.count() shouldBe 4 + emptyMap().count() shouldBe 0 + } + + "reduce - Map.Entry의 누적 연산 (빈 Map에는 사용 불가)" { + val map: Map = mapOf("a" to 1, "b" to 2, "c" to 3) + + val reducedEntry: Map.Entry = + map.entries.reduce { acc, entry -> + object : Map.Entry { + override val key: String = acc.key + entry.key + override val value: Int = acc.value + entry.value + } + } + + reducedEntry.key shouldBe "abc" + reducedEntry.value shouldBe 6 + } + + "fold - Map.Entry의 누적 연산 (초기값 사용)" { + val map: Map = mapOf("a" to 1, "b" to 2, "c" to 3) + val initial: Map.Entry = + object : Map.Entry { + override val key: String = "start " + override val value: Int = 100 + } + + val foldedValue: Map.Entry = + map.entries.fold(initial) { acc, entry -> + object : Map.Entry { + override val key: String = acc.key + entry.key + override val value: Int = acc.value + entry.value + } + } + + foldedValue.key shouldBe "start abc" + foldedValue.value shouldBe 106 + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/CombinationTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/CombinationTest.kt new file mode 100644 index 0000000..ae5b306 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/CombinationTest.kt @@ -0,0 +1,121 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class CombinationTest : FreeSpec({ + "List Combination" - { + "plus (+) - 두 컬렉션을 이어붙이거나 요소 하나를 추가" { + val list1 = listOf(1, 3) + val list2 = listOf(2, 4) + + list1 + list2 shouldBe listOf(1, 3, 2, 4) + + list1 + 2 shouldBe listOf(1, 3, 2) +// (2 + list1) 처럼 (원소 + 리스트)는 불가능 + } + + "minus (-) - 다른 컬렉션이나 특정 요소를 제거" { + val list1 = listOf(1, 2, 3, 2) + val list2 = listOf(1, 4) + + list1 - list2 shouldBe listOf(2, 3, 2) + list1 - 1 shouldBe listOf(2, 3, 2) + } + + "union - 두 컬렉션의 합집합 (중복 제거)" { + val list1 = listOf(1, 2, 3) + val list2 = listOf(3, 4, 5) + + (list1 union list2) shouldBe setOf(1, 2, 3, 4, 5) + } + + "intersect - 두 컬렉션의 교집합 (중복 제거)" { + val list1 = listOf(1, 2, 3, 4, 2) + val list2 = listOf(3, 4, 5, 4) + + (list1 intersect list2) shouldBe setOf(3, 4) + } + + "subtract - 첫 번째 컬렉션에서 두 번째 컬렉션의 요소를 제외한 차집합 (중복 제거)" { + val list1 = listOf(1, 2, 3, 4, 2) + val list2 = listOf(3, 4, 5, 4) + + (list1 subtract list2) shouldBe setOf(1, 2) // list1 에서의 중복도 제거됨. + } + } + + "Map Combination" - { + val map1 = mapOf("a" to 1, "b" to 2) + val map2 = mapOf("b" to 20, "c" to 30) + + "plus (+) " { + map1 + map2 shouldBe mapOf("a" to 1, "b" to 20, "c" to 30) + } + + "merge - 키 존재 시 값 결합, 키 부재 시 추가" { + val mutableMap = mutableMapOf("a" to 1, "b" to 2) + + mutableMap.merge("c", 10) { oldValue, newValue -> + oldValue + newValue + } + + mutableMap shouldBe mapOf("a" to 1, "b" to 2, "c" to 10) + + mutableMap.merge("a", 10) { oldValue, newValue -> + oldValue + newValue + } + + mutableMap shouldBe mapOf("a" to 11, "b" to 2, "c" to 10) + } + + "entries, keys, values 를 이용한 union, intersect, subtract (기존 테스트 유지)" - { + "entries 를 이용한 union (결과 Set)" { + val unionResult: Set> = map1.entries union map2.entries + unionResult shouldBe + setOf( + mapOf("a" to 1).entries.first(), + mapOf("b" to 2).entries.first(), + mapOf("b" to 20).entries.first(), + mapOf("c" to 30).entries.first(), + ) + } + + "entries 를 이용한 intersect (결과 Set)" { + val intersectResult: Set> = + map1.entries intersect map2.entries + + intersectResult shouldBe emptySet() + + val map3 = mapOf("a" to 1, "b" to 2) + val map4 = mapOf("a" to 1, "b" to 4) + + val intersectResult2: Set> = + map3.entries intersect map4.entries + + intersectResult2 shouldBe setOf(mapOf("a" to 1).entries.first()) + } + + "keys 를 이용한 union (결과 Set)" { + val unionResult: Set = map1.keys union map2.keys + unionResult shouldBe setOf("a", "b", "c") + } + + "keys 를 이용한 intersect (결과 Set)" { + val intersectResult: Set = map1.keys intersect map2.keys + intersectResult shouldBe setOf("b") + } + + "keys 를 이용한 subtract (결과 Set)" { + val subtractResult: Set = map1.keys subtract map2.keys + subtractResult shouldBe setOf("a") + } + + "values 를 이용한 조합 union (결과 Set)" { + val unionResult: Set = map1.values union map2.values + + unionResult shouldBe setOf(1, 2, 20, 30) + } + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/ControlFlowExtensionTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/ControlFlowExtensionTest.kt new file mode 100644 index 0000000..84d6027 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/ControlFlowExtensionTest.kt @@ -0,0 +1,129 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class ControlFlowExtensionTest : FreeSpec({ + + data class Person(var name: String, var age: Int, var city: String) + + "Scope Functions (스코프 함수) - 객체 컨텍스트에서 코드 블록 실행" - { + + "let - 객체를 it 으로 참조, 람다 결과 반환" { + val person = Person("Alice", 20, "Amsterdam") + val result: String = + person.let { + it.age += 1 + "Name: ${it.name}, Age: ${it.age}" + } + result shouldBe "Name: Alice, Age: 21" + person.age shouldBe 21 + } + + "run - 객체를 this로 참조, 람다 결과 반환 (Non-extension run은 객체 없이 사용)" { + val person = Person("Bob", 25, "London") + val result: String = + person.run { + this.city = "Paris" + age = 26 + "Age: $age, City: $city" + } + result shouldBe "Age: 26, City: Paris" + person.city shouldBe "Paris" + } + + "with - 객체를 첫 번째 인자로, 객체를 this로 참조, 람다 결과 반환 (확장 함수 아님)" { + val person = Person("Charlie", 30, "New York") + val result: String = + with(person) { + age += 5 + "New Age: $age" + } + result shouldBe "New Age: 35" + person.age shouldBe 35 + } + + "apply - 객체를 this로 참조, 객체 자체 반환 (주로 객체 초기화/설정)" { + val person: Person = + Person("David", 35, "Tokyo").apply { + age += 2 + city = "Seoul" + } + + person.apply { + age += 2 + } + + person.age shouldBe 39 + person.city shouldBe "Seoul" + } + + "also - 객체를 it 으로 참조, 객체 자체 반환 (주로 부수 효과 로깅 등)" { + val numbers = mutableListOf(1, 2, 3) + val resultList: MutableList = + numbers.also { + it.add(4) + println("List after adding element: $it") + } + resultList shouldBe listOf(1, 2, 3, 4) + numbers shouldBe listOf(1, 2, 3, 4) + } + } + + "Conditional Execution Functions (조건부 실행 함수) - 조건에 따라 객체 반환" - { + + "takeIf - 조건이 true 이면 객체 자체 반환, false 이면 null 반환" { + val number = 10 + val evenNumber: Int? = number.takeIf { it % 2 == 0 } + val oddNumber: Int? = number.takeIf { it % 2 != 0 } + + evenNumber shouldBe 10 + oddNumber shouldBe null + } + + "takeUnless - 조건이 false 이면 객체 자체 반환, true 이면 null 반환" { + val number = 10 + val notOddNumber = number.takeUnless { it % 2 != 0 } + val notEvenNumber = number.takeUnless { it % 2 == 0 } + + notOddNumber shouldBe 10 + notEvenNumber shouldBe null + } + } + + "Combining Scope and Conditional Functions - 스코프 함수와 조건부 함수 조합" - { + "takeIf 와 let 을 함께 사용하여 조건 만족 시에만 코드 실행" { + val person: Person? = Person("Grace", 22, "Sydney") + + val result1 = + person?.takeIf { it.age >= 18 }?.let { + "Adult: ${it.name}" + } + result1 shouldBe "Adult: Grace" + + val anotherPerson: Person? = Person("Heidi", 17, "Vienna") + val result2 = + anotherPerson?.takeIf { it.age >= 18 }?.let { + "Adult: ${it.name}" + } + result2 shouldBe null // 조건 불만족 시 takeIf가 null 반환하고 let 실행 안됨 + } + + "takeUnless 와 run 을 함께 사용하여 조건 불만족 시에만 코드 실행" { + val person: Person? = Person("Ivy", 28, "Moscow") + + val result1 = + person?.takeUnless { it.age < 18 }?.run { + "Not a minor: ${this.name}" + } + result1 shouldBe "Not a minor: Ivy" + + val anotherPerson: Person? = Person("Jack", 15, "Cairo") + val result2 = + anotherPerson?.takeUnless { it.age < 18 }?.run { + "Not a minor: ${this.name}" + } + result2 shouldBe null // 조건 만족 시 takeUnless가 null 반환하고 run 실행 안됨 + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/FilteringTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/FilteringTest.kt new file mode 100644 index 0000000..f83b204 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/FilteringTest.kt @@ -0,0 +1,149 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class FilteringTest : FreeSpec({ + "List Filter Api" - { + "filter - 조건을 만족하는 것만 남기기" { + val numbers: List = listOf(1, 2, 3, 4, 5, 6) + numbers.filter { it % 2 == 0 } shouldBe listOf(2, 4, 6) + } + + "filterNot - 조건을 만족하지 않는 요소만 남기기" { + val words: List = listOf("a", "be", "cat") + words.filterNot { it.length > 1 } shouldBe listOf("a") + } + + "filterIndexed - 인덱스와 함께 필터링" { + val words: List = listOf("zero", "one", "two", "three") + val result = words.filterIndexed { index, _ -> index % 2 == 0 } + result shouldBe listOf("zero", "two") + } + + "take - 앞에서부터 N개 가져오기" { + val numbers: List = listOf(0, 1, 2, 3, 4) + numbers.take(2) shouldBe listOf(0, 1) + } + + "drop - 앞에서부터 N 개 버리고 나머지 가져오기" { + val numbers: List = listOf(0, 1, 2, 3, 4) + numbers.drop(2) shouldBe listOf(2, 3, 4) + } + + "takeWhile - 앞에서부터 조건이 거짓이기 전까지 가져오기(거짓이되면 순회 중지)" { + val numbers: List = listOf(1, 2, 3, 0, 4) + numbers.takeWhile { number -> number > 0 } shouldBe listOf(1, 2, 3) + } + + "dropWhile - 앞에서부터 조건이 참이기 전까지 버리고 나머지 가져오기(참이 되면 순회 중지)" { + val numbers: List = listOf(1, 2, 3, 0, 4) + numbers.dropWhile { it > 0 } shouldBe listOf(0, 4) + } + + "filterTo - 필터링 결과를 기존 컬렉션에 추가" { + val target: MutableList = mutableListOf(0, 9) + val source: List = listOf(1, 2, 3, 4) + source.filterTo(target) { it > 2 } + + target shouldBe listOf(0, 9, 3, 4) + } + + "filterIndexedTo - 인덱스와 함께 필터링 결과를 기존 컬렉션에 추가" { + val target: MutableList = mutableListOf("hello", "world") + val source: List = listOf("a", "b", "c", "d") + source.filterIndexedTo(target) { index, _ -> index % 2 == 0 } + + target shouldBe listOf("hello", "world", "a", "c") + } + + "filterNotTo - 조건을 만족하지 않은 것만 필터링해서 기존 컬렉션에 추가" { + val target: MutableList = mutableListOf(3, 3) + val source: List = listOf(1, 2, 3, 4) + source.filterNotTo(target) { it % 2 == 0 } + + target shouldBe listOf(3, 3, 1, 3) + } + + "filterIsInstance - 타입 필터링" { + val list: List = listOf(1, "a", 2L, 'b', "c") + list.filterIsInstance() shouldBe listOf("a", "c") + } + + "filterIsInstanceTo - 타입 필터링 후 다른 컬렉션에 저장" { + val target: MutableList = mutableListOf("d") + val source: List = listOf("a", 1, 'b', 2L, "c") + + source.filterIsInstanceTo(target) shouldBe listOf("d", "a", "c") + } + + "distinct - 중복 제거" { + val list: List = listOf(1, 2, 3, 1, 2) + list.distinct() shouldBe listOf(1, 2, 3) + } + + "distinctBy - 조건에 따라 중복 제거 (대표 요소 하나만 남김)" { + val list: List = listOf("aaa", "bbb", "ccc", "aa", "bb", "cc", "a", "b", "c") + list.distinctBy { it.length } shouldBe listOf("aaa", "aa", "a") + } + + "partition - 조건에 따라 두 그룹으로 나누기" { + val list: List = listOf(1, 2, 3, 4, 5) + val (even: List, odd: List) = list.partition { it % 2 == 0 } + + even shouldBe listOf(2, 4) + odd shouldBe listOf(1, 3, 5) + } + } + + "Map Filter Api" - { + val map: Map = + mapOf( + "a" to 1, + "b" to 2, + "c" to 3, + "d" to 4, + ) + + "filter - (key, value) 쌍으로 필터링" { + // requires a pair of parentheses around the key and value in the lambda block. + val result: Map = + map.filter { (key, value) -> key in listOf("a", "c") && value % 2 == 1 } + + result shouldBe mapOf("a" to 1, "c" to 3) + } + + "filterKeys - key 기준 필터링" { + val result: Map = map.filterKeys { it > "b" } + result shouldBe mapOf("c" to 3, "d" to 4) + } + + "filterValues - value 기준 필터링" { + val result: Map = map.filterValues { it % 2 == 0 } + result shouldBe mapOf("b" to 2, "d" to 4) + } + + "filterNot - 조건을 만족하지 않는 (key, value) 필터링" { + val result: Map = map.filterNot { (_, value) -> value > 2 } + result shouldBe mapOf("a" to 1, "b" to 2) + } + + "filterTo - 조건을 만족하는 항목을 다른 MutableMap에 추가" { + val source: Map = mapOf("one" to 1, "two" to 2, "three" to 3) + val target: MutableMap = mutableMapOf() + + source.filterTo(target) { (_, v) -> v % 2 == 1 } + + target shouldBe mapOf("one" to 1, "three" to 3) + } + + "filterNotTo - 조건을 만족하지 않는 항목을 target에 추가" { + val source: Map = mapOf("a" to 1, "b" to 2, "c" to 3) + val result: MutableMap = mutableMapOf() + + source.filterNotTo(result) { (_, v) -> v > 1 } + + result shouldBe mapOf("a" to 1) + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/FlatteningTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/FlatteningTest.kt new file mode 100644 index 0000000..23d2524 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/FlatteningTest.kt @@ -0,0 +1,200 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class FlatteningTest : FreeSpec({ + "List 의 flattening" - { + "flatten - 중첩된 리스트를 평탄화" { + val nested: List> = + listOf( + listOf(1, 2), + listOf(3, 4), + ) + + nested.flatten() shouldBe listOf(1, 2, 3, 4) + } + + "flatten - 깊이 n 번의 중첩은 n 번의 flatten 으로 평탄화" { + val nested: List>> = + listOf( + listOf( + listOf(1, 2), + listOf(11, 22), + ), + listOf( + listOf(3, 4), + listOf(33, 44), + ), + ) + + val onceFlattened: List> = nested.flatten() + val twiceFlattened: List = onceFlattened.flatten() + + onceFlattened shouldBe + listOf( + listOf(1, 2), + listOf(11, 22), + listOf(3, 4), + listOf(33, 44), + ) + twiceFlattened shouldBe listOf(1, 2, 11, 22, 3, 4, 33, 44) + } + + "flatMap - 각 요소를 여러 개로 펼친 후 평탄화" { + val nested: List> = + listOf( + listOf(1, 2), + listOf(3, 4), + ) + + val flattened: List = nested.flatMap { it } + flattened shouldBe listOf(1, 2, 3, 4) + } + + "flatMapIndexed - 인덱스와 함께 각 요소를 여러 개로 펼친 후 평탄화" { + val nested: List> = + listOf( + listOf(1, 2), + listOf(3, 4), + ) + + val flattened: List = + nested.flatMapIndexed { index, list -> + list.map { value -> index * value } + } + + flattened shouldBe listOf(0, 0, 3, 4) + } + + "flatMapTo - 중첩된 리스트를 평탄화해서 기존 컬렉션에 추가" { + val source: List> = + listOf( + listOf(1, 2), + listOf(3, 4), + ) + val destination: MutableList = mutableListOf(0) + source.flatMapTo(destination) { it } + + destination shouldBe listOf(0, 1, 2, 3, 4) + } + + "flatMapIndexed - 인덱스와 함께 각 요소를 여러 개로 펼친 후 평탄화해서 기존 컬렉션에 추가" { + val nested: List> = + listOf( + listOf(1, 2), + listOf(3, 4), + ) + val destination: MutableList = mutableListOf(0, 1, 2) + nested.flatMapIndexedTo(destination) { index, list -> + list.map { value -> value * 2 } + } + + destination shouldBe listOf(0, 1, 2, 2, 4, 6, 8) + } + } + + "Map 의 flattening" - { + "flatMap - Map> 구조를 평탄화" { + val map: Map> = + mapOf( + "a" to listOf(1, 2), + "b" to listOf(3), + ) + + val result: List = + map.flatMap { (key, numbers) -> + numbers.map { number -> "$key:$number" } + } + + result shouldBe listOf("a:1", "a:2", "b:3") + } + + "flatMap - 사실 flatMap 의 람다 파라미터의 리턴 타입이 Iterable 타입이기만 하면 된다." { + val map: Map = + mapOf( + "a" to 1, + "b" to 2, + "c" to 3, + ) + + val result: List = + map.flatMap { (_, number) -> + List(number) { number } + } + + result shouldBe listOf(1, 2, 2, 3, 3, 3) + } + + "toList - Map 를 List> 로 변환" { + val map: Map> = + mapOf( + "a" to listOf(1, 2), + "b" to listOf(3), + ) + + val toList: List>> = map.toList() + + toList shouldBe + listOf>>( + "a" to listOf(1, 2), + "b" to listOf(3), + ) + } + + "flatMapValues 메서드는 따로 없어서 Map.values.flatten 사용" { + val map: Map> = + mapOf( + "a" to listOf(1, 2), + "b" to listOf(3), + ) + + val result: List = map.values.flatten() + result shouldBe listOf(1, 2, 3) + } + + "flatPairValues 메서드는 따로 없어서 아래처럼 재구성 필요" { + val map: Map> = + mapOf( + "a" to listOf(1, 2), + "b" to listOf(3), + ) + + val result1: List> = + map.flatMap { entry -> + entry.value.map { value -> entry.key to value } + } + + val result2: List> = + map.entries.flatMap { entry -> + entry.value.map { value -> entry.key to value } + } + + result1 shouldBe + listOf>( + "a" to 1, + "a" to 2, + "b" to 3, + ) + result1 shouldBe result2 + } + + "Map.toList(). 은 단순 List> 리스트로 변환" { + val map: Map = + mapOf( + "a" to "1", + "b" to "2", + "c" to "3", + ) + + val flatList: List> = map.toList() + + flatList shouldBe + listOf>( + "a" to "1", + "b" to "2", + "c" to "3", + ) + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/GroupingTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/GroupingTest.kt new file mode 100644 index 0000000..acb360b --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/GroupingTest.kt @@ -0,0 +1,118 @@ +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class GroupingTest : FreeSpec({ + data class Person(val name: String, val city: String, val age: Int) + + val people = + listOf( + Person("Alice", "London", 30), + Person("Bob", "Paris", 25), + Person("Charlie", "London", 35), + Person("David", "Paris", 30), + Person("Eve", "London", 25), + ) + + "List Grouping" - { + "groupBy - 키를 기준으로 요소들을 그룹화. 리턴 타입은 Map>" { + val groupedByCity: Map> = people.groupBy(Person::city) + + groupedByCity shouldBe + mapOf>( + "London" to + listOf( + Person("Alice", "London", 30), + Person("Charlie", "London", 35), + Person("Eve", "London", 25), + ), + "Paris" to + listOf( + Person("Bob", "Paris", 25), + Person("David", "Paris", 30), + ), + ) + } + + "groupBy - 키와 값 변환을 함께 적용하여 그룹화" { + val groupedCityNames: Map> = + people.groupBy( + keySelector = (Person::city), + valueTransform = { it.name + 1 }, + ) + + groupedCityNames shouldBe + mapOf>( + "London" to listOf("Alice1", "Charlie1", "Eve1"), + "Paris" to listOf("Bob1", "David1"), + ) + } + + "groupingBy - eachCount() 로 각 그룹의 요소 개수를 계산" { + // 도시별 인구 수 map + val groupingByCity: Grouping = people.groupingBy(Person::city) + val cityWithPeopleCount = groupingByCity.eachCount() + + cityWithPeopleCount shouldBe + mapOf( + "London" to 3, + "Paris" to 2, + ) + } + + "groupingBy - fold() 로 각 그룹 내에서 누적 연산 수행" { + // 도시별 인구 나이의 합 map + val groupingByCity: Grouping = people.groupingBy(Person::city) + val cityWithPeopleAgeSum: Map = + groupingByCity.fold(0) { acc, person -> + acc + person.age + } + + cityWithPeopleAgeSum shouldBe + mapOf( + "London" to 30 + 35 + 25, + "Paris" to 25 + 30, + ) + } + + "groupingBy - reduce() 로 각 그룹 내에서 누적 연산 수행 (빈 그룹에는 사용 불가)" { + val animals = listOf("raccoon", "reindeer", "cow", "camel", "giraffe", "goat") + // 모음을 가장 많이 포함한 문자열만 수집 + val groupingByFirstChar: Grouping = animals.groupingBy(String::first) + + val comparator = compareBy { str: String -> str.count { it in "aeiou" } } + + val firstCharWithMaxVowelsAnimal: Map = + groupingByFirstChar.reduce { key: Char, acc: String, animal: String -> + maxOf(acc, animal, comparator) + } + firstCharWithMaxVowelsAnimal shouldBe + mapOf( + 'r' to "reindeer", + 'c' to "camel", + 'g' to "giraffe", + ) + } + + "groupingBy - aggregate() 로 더 복잡한 그룹별 집계 수행" { + val numbers = listOf(3, 4, 5, 6, 7, 8, 9) + + val groupedElements: Map = + numbers + .groupingBy { it % 3 } + .aggregate { key: Int, accumulator: StringBuilder?, element: Int, first -> + if (first) { + StringBuilder().append(key).append("-").append(element) + } else { + accumulator!!.append("-").append(element) + } + }.mapValues { (_, sb) -> sb.toString() } + + groupedElements shouldBe + mapOf( + 0 to "0-3-6-9", + 1 to "1-4-7", + 2 to "2-5-8", + ) + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/MappingTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/MappingTest.kt new file mode 100644 index 0000000..769de9d --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/MappingTest.kt @@ -0,0 +1,154 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class MappingTest : FreeSpec({ + "리스트 컬렉션의 mapping - 원소 변환" - { + "map - 각 원소를 변환" { + val numbers: List = listOf(1, 2, 3) + numbers.map { number -> number * 2 } shouldBe listOf(2, 4, 6) + } + + "mapIndexed - 인덱스를 함께 사용한 변환" { + val names: List = listOf("a", "b", "c") + names.mapIndexed { index, name -> "$index: $name" } shouldBe + listOf( + "0: a", + "1: b", + "2: c", + ) + } + + "mapNotNull - null 을 걸러내며 변환" { + val tags: List = listOf("1", "a", "2") + val numberTags: List = tags.mapNotNull { tag -> tag.toIntOrNull() } + + numberTags shouldBe listOf(1, 2) + } + + "mapIndexedNotNull - 인덱스를 함께 사용하며 null 을 걸러내며 변환" { + val tags: List = listOf("1", "a", "2", null) + val numberTags: List = + tags + .mapIndexedNotNull { index, tag -> + if (index == 0) { + null + } else { + tag?.toIntOrNull() + } + } + numberTags shouldBe listOf(2) + } + + "mapTo - 결과를 미리 만든 리스트에 추가" { + val source: List = listOf(1, 2, 3) + val destination: MutableList = mutableListOf(0, 0) + source.mapTo(destination) { it * it } + + destination shouldBe listOf(0, 0, 1, 4, 9) + } + + "mapIndexedTo -인덱스를 이용하고 결과를 미리 만든 리스트에 추가" { + val source: List = listOf(1, 2, 3) + val destination: MutableList> = mutableListOf() + source.mapIndexedTo(destination) { index, value -> + index to value + } + + destination shouldBe listOf(0 to 1, 1 to 2, 2 to 3) + } + + "mapNotNullTo - 결과를 미리 만든 리스트에 null 빼고 추가" { + val source: List = listOf(1, 2, null) + val destination: MutableList = mutableListOf(0, 0) + source.mapNotNullTo(destination) { it?.times(it) } + + destination shouldBe listOf(0, 0, 1, 4) + } + + "mapIndexedNotNullTo - 인덱스를 이용하고 null 을 제거하며 리스트에 추가 " { + val source: List = listOf("0", "a", "2", "b") + val result: MutableList> = mutableListOf() + + source.mapIndexedNotNullTo(result) { index, value -> + value.toIntOrNull()?.let { index to it } + } + + result shouldBe listOf(0 to 0, 2 to 2) + } + + "associate - List 를 Map 으로 변환 (Key, Value 모두 수동 지정)" { + val words: List = listOf("apple", "banana") + val wordsWithLength: Map = words.associate { word -> word to word.length } + wordsWithLength shouldBe mapOf("apple" to 5, "banana" to 6) + } + + "associateWith - value 만 지정하고 key 는 원본 사용" { + val fruits: List = listOf("apple", "banana") + val fruitsWithLength: Map = fruits.associateWith { it.length } + + fruitsWithLength shouldBe mapOf("apple" to 5, "banana" to 6) + } + + "associateBy - Key 만 지정하고 Value 는 원본 사용" { + data class User(val id: Int, val name: String) + + val users: List = listOf(User(1, "jimmy"), User(2, "tim")) + val usersByCode: Map = + users.associateBy { user -> + user.id * 32 + } + + usersByCode shouldBe + mapOf( + 32 to User(1, "jimmy"), + 64 to User(2, "tim"), + ) + } + + "associateTo - key 만 지정하고 value 는 원본 유지하며 기존 Map에 추가" { + val fruits: List = listOf("apple", "banana") + val fruitsWithLength: MutableMap = mutableMapOf("lemon" to 5) + + fruits.associateTo(fruitsWithLength) { fruit -> + fruit to fruit.length + } + + fruitsWithLength shouldBe mapOf("lemon" to 5, "apple" to 5, "banana" to 6) + } + + "associateWithTo - value 만 지정하고 key 는 원본 유지하며 기본 map 에 추가" { + val fruits: List = listOf("apple", "banana") + val fruitsWithLength: MutableMap = mutableMapOf("lemon" to 5) + + fruits.associateWithTo(fruitsWithLength) { fruit -> + fruit.length + } + + fruitsWithLength shouldBe + mapOf( + "lemon" to 5, + "apple" to 5, + "banana" to 6, + ) + } + + "associateByTo - key만 지정하고 value 는 원본 유지하며 기존 map 에 추가" { + data class User(val id: Int, val name: String) + + val users: List = listOf(User(1, "A"), User(2, "B")) + val codeWithUsers: MutableMap = mutableMapOf(10 to User(3, "ABC")) + + users.associateByTo(codeWithUsers) { user -> + user.id + 32 + } + codeWithUsers shouldBe + mapOf( + 10 to User(3, "ABC"), + 33 to User(1, "A"), + 34 to User(2, "B"), + ) + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/SearchingTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/SearchingTest.kt new file mode 100644 index 0000000..a853da6 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/SearchingTest.kt @@ -0,0 +1,158 @@ +package com.example.learningtest.collection.functional + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class SearchingTest : FreeSpec({ + data class User(val name: String?, val age: Int) + + "List Searching" - { + "first - 조건을 만족하는 첫 요소 (없으면 예외 던짐)" { + val numbers: List = listOf(1, 2, 3, 4) + + numbers.first() shouldBe 1 + numbers.first { it > 2 } shouldBe 3 + shouldThrow { numbers.first { it > 10 } } + } + + "find == firstOrNull - 조건을 만족하는 첫 요소(없으면 null)" { + val words: List = listOf("apple", "banana", "cherry") + + words.find { it.startsWith("b") } shouldBe "banana" + words.find { it.startsWith("z") } shouldBe null + words.firstOrNull { it.startsWith("z") } shouldBe null + } + + "last - 조건을 만족하는 마지막 요소 (없으면 예외)" { + val numbers: List = listOf(1, 3, 5, 2, 4) + + numbers.last() shouldBe 4 + numbers.last { it % 2 == 1 } shouldBe 5 + shouldThrow { numbers.last { it > 10 } } + } + + "findLast & lastOrNull - 조건을 만족하는 마지막 요소 (없으면 null)" { + val numbers: List = listOf(1, 2, 3, 4) + + numbers.findLast { it % 2 == 1 } shouldBe 3 + numbers.findLast { it > 5 } shouldBe null + + numbers.lastOrNull() shouldBe 4 + numbers.lastOrNull { it > 5 } shouldBe null + } + + "all - 모든 요소가 조건을 만족하면 true" { + val numbers: List = listOf(2, 4, 6) + + numbers.all { it % 2 == 0 } shouldBe true + numbers.all { it > 4 } shouldBe false + } + + "any - 조건을 만족하는 요소가 하나라도 있으면 true" { + val numbers: List = listOf(1, 2, 3) + + numbers.any { it > 2 } shouldBe true + numbers.any { it < 0 } shouldBe false + } + + "none - 모든 요소가 조건을 만족하지 않으면 true" { + val characters = listOf('a', 'b', 'c') + + characters.none { it == 'z' } shouldBe true + characters.none { it == 'a' } shouldBe false + } + } + + "indexOf - 해당 값의 첫 위치 (없으면 -1)" { + val names: List = listOf("jim", "pam", "jim") + + names.indexOf("jim") shouldBe 0 + names.indexOf("shim") shouldBe -1 + } + + "lastIndexOf - 해당 값의 마지막 위치 (없으면 -1)" { + val names: List = listOf("jim", "pam", "jim") + + names.lastIndexOf("jim") shouldBe 2 + names.lastIndexOf("shim") shouldBe -1 + } + + "indexOfFirst - 조건을 만족하는 원소 중 첫 인덱스 (없으면 -1)" { + val names: List = listOf("kim", "lee", "choi", "kim") + + names.indexOfFirst { it.startsWith("k") } shouldBe 0 + names.indexOfFirst { it == "park" } shouldBe -1 + } + + "indexOfLast - 조건을 만족하는 원소 중 마지막 인덱스(없으면 -1)" { + val names: List = listOf("kim", "lee", "choi", "kim") + + names.indexOfLast { it.startsWith("k") } shouldBe 3 + names.indexOfLast { it == "park" } shouldBe -1 + } + + "조건을 만족하는 원소가 없으면 -1 인덱스를 리턴하는 게 아닌 null 리턴하도록" { + val names: List = listOf("kim", "lee", "choi", "kim") + + names + .indexOf("park") + .takeIf { it != -1 } shouldBe null + names + .indexOfFirst { it.startsWith("p") } + .takeIf { it != -1 } shouldBe null + + names + .indexOfLast { it.startsWith("p") } + .takeIf { it != -1 } shouldBe null + } + + "firstNotNullOf - 변환 결과가 null 이 아닌 첫 값(없으면 예외)" { + + val users1: List = + listOf( + User(null, 20), + User("a", 25), + User("b", 30), + ) + val firstName = users1.firstNotNullOf { it.name } + firstName shouldBe "a" + + val users2: List = listOf(User(null, 20), User(null, 25)) + + shouldThrow { + users2.firstNotNullOf { it.name } + } + } + + "firstNotNullOfOrNull - 변환 결과가 null 이 아닌 첫 값(없으면 null)" { + val users1: List = + listOf( + User(null, 20), + User(null, 25), + User("Lilly", 30), + ) + + val firstName: String? = users1.firstNotNullOfOrNull { it.name } + firstName shouldBe "Lilly" + + val users2: List = + listOf( + User(null, 20), + User(null, 25), + ) + + users2.firstNotNullOfOrNull { it.name } shouldBe null + } + + "lastNotNullOf & lastNotNullOfOrNull 메서드는 없음 - 직접 구현 예시(mapNotNull 활용)" { + val users: List = + listOf( + User(null, 30), + User(null, 25), + ) + + val lastName: String? = users.mapNotNull { it.name }.lastOrNull() + lastName shouldBe null + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/SortingTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/SortingTest.kt new file mode 100644 index 0000000..c2ae916 --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/SortingTest.kt @@ -0,0 +1,394 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.collections.shouldBeSorted +import io.kotest.matchers.collections.shouldBeSortedBy +import io.kotest.matchers.collections.shouldBeSortedDescending +import io.kotest.matchers.collections.shouldBeSortedDescendingBy +import io.kotest.matchers.collections.shouldBeSortedWith +import io.kotest.matchers.shouldBe +import java.util.SortedMap + +class SortingTest : FreeSpec({ + "List Sorting" - { + data class User(val name: String, val age: Int) + + "정렬하여 새로운 컬렉션 생성" - { + "sorted - 오름차순 정렬" { + val numbers: List = listOf(3, 1, 4, 2) + val sorted: List = numbers.sorted() + + sorted shouldBe listOf(1, 2, 3, 4) + sorted.shouldBeSorted() + } + + "sortedDescending - 내림차순 정렬" { + val numbers: List = listOf(3, 1, 4, 2) + val sortedDescending: List = numbers.sortedDescending() + + sortedDescending shouldBe listOf(4, 3, 2, 1) + sortedDescending.shouldBeSortedDescending() + } + + "sortedBy - 특정 속성 기준 정렬" { + val users: List = + listOf( + User("A", 30), + User("B", 20), + User("C", 25), + ) + val sorted: List = users.sortedBy { it.age } + + sorted shouldBe + listOf( + User("B", 20), + User("C", 25), + User("A", 30), + ) + sorted shouldBeSortedBy { it.age } + } + + "sortedByDescending - 특정 속성 기준 내림차순 정렬" { + val users: List = + listOf( + User("A", 30), + User("B", 20), + User("C", 25), + ) + val sorted: List = users.sortedByDescending { it.age } + + sorted shouldBe + listOf( + User("A", 30), + User("C", 25), + User("B", 20), + ) + sorted shouldBeSortedDescendingBy { it.age } + } + + "sortedWith 는 Comparator 를 사용하여 정렬" - { + "sortedWith - 기본" { + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Bob", 28), + ) + val sortedByAge1: List = users.sortedWith(compareBy(User::age)) + val sortedByAge2: List = + users.sortedWith { user1, user2 -> + user1.age.compareTo(user2.age) + } + val sortedByAge3: List = + users.sortedWith { user1, user2 -> + user1.age - user2.age + } + + val expected: List = + listOf( + User("Cathy", 22), + User("Bob", 28), + User("Alice", 30), + ) + + sortedByAge1 shouldBe expected + sortedByAge2 shouldBe expected + sortedByAge3 shouldBe expected + + sortedByAge1.shouldBeSortedWith(compareBy { it.age }) + sortedByAge2.shouldBeSortedWith(compareBy { it.age }) + sortedByAge3.shouldBeSortedWith(compareBy { it.age }) + } + + "sortedWith - 나이로 정렬하되 다른 필드가 모두 같을 경우 입력 순서 유지 (Stable Sort)" { + val users: List = + listOf( + User("Alice", 30), + User("Bob", 25), + User("Alice", 22), + ) + val sortedByName1: List = users.sortedWith(compareBy { it.name }) + val sortedByName2: List = + users.sortedWith { user1, user2 -> + user1.name.compareTo(user2.name) + } + /* String 타입의 비교는 - 연산으로 불가능하다. + val sortedByName3 : List = users.sortedWith { user1, user2 -> + user1.name - user2.name + } + */ + + val expected: List = + listOf( + User("Alice", 30), + User("Alice", 22), + User("Bob", 25), + ) + + sortedByName1 shouldBe expected + sortedByName2 shouldBe expected + + sortedByName1 shouldBeSortedWith (compareBy { it.name }) + sortedByName2 shouldBeSortedWith (compareBy(User::name)) + } + + "sortedWith + compareByDescending" { + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Bob", 28), + ) + + val sortedByDescendingAge1: List = + users.sortedWith( + compareByDescending { + it.age + }, + ) + val sortedByDescendingAge2: List = + users.sortedWith { user1, user2 -> + user2.age.compareTo(user1.age) + } + val sortedByDescendingAge3: List = + users.sortedWith { user1, user2 -> + user2.age - user1.age + } + + val expected: List = + listOf( + User("Alice", 30), + User("Bob", 28), + User("Cathy", 22), + ) + + sortedByDescendingAge1 shouldBe expected + sortedByDescendingAge2 shouldBe expected + sortedByDescendingAge3 shouldBe expected + + sortedByDescendingAge1 shouldBeSortedWith (compareByDescending(User::age)) + sortedByDescendingAge2 shouldBeSortedWith (compareByDescending(User::age)) + sortedByDescendingAge3 shouldBeSortedWith (compareByDescending(User::age)) + } + + "sortedWith - 이름으로 오름차순 정렬, 이름이 같다면 나이로 오름차순 정렬" { + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Alice", 28), + ) + + val sorted1: List = + users.sortedWith(compareBy { it.name }.thenBy { it.age }) + val sorted2: List = + users.sortedWith { user1, user2 -> + val nameComparison = user1.name.compareTo(user2.name) + if (nameComparison != 0) { + return@sortedWith nameComparison + } + return@sortedWith user1.age.compareTo(user2.age) + } + + val expected: List = + listOf( + User("Alice", 28), + User("Alice", 30), + User("Cathy", 22), + ) + + sorted1 shouldBe expected + sorted2 shouldBe expected + } + + "sortedWith - 이름으로 오름차순 정렬, 이름이 같다면 나이로 내림차순 정렬" { + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Alice", 28), + ) + + val sorted1: List = + users.sortedWith(compareBy { it.name }.thenByDescending { it.age }) + val sorted2: List = + users.sortedWith { user1, user2 -> + val nameComparison = user1.name.compareTo(user2.name) + if (nameComparison != 0) { + return@sortedWith nameComparison + } + return@sortedWith user2.age.compareTo(user1.age) + } + + val expected: List = + listOf( + User("Alice", 30), + User("Alice", 28), + User("Cathy", 22), + ) + + sorted1 shouldBe expected + sorted2 shouldBe expected + } + + "naturalOrder() - Comparable 인터페이스를 구현하는 타입의 자연스러운 오름차순 순서에 따라 정렬하는 Comparator" { + data class User( + val name: String, + val age: Int, + ) : Comparable { + override fun compareTo(other: User): Int = + compareBy(User::name) + .thenBy(User::age) + .compare(this, other) + } + + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Alice", 28), + ) + + // sortedWith(naturalOrder()) == sorted() + val sorted1 = users.sortedWith(naturalOrder()) + val sorted2 = users.sorted() + + val expected: List = + listOf( + User("Alice", 28), + User("Alice", 30), + User("Cathy", 22), + ) + sorted1 shouldBe expected + sorted2 shouldBe expected + } + + "reversedOrder() - Comparable 인터페이스를 구현하는 타입의 자연스러운 순서의 역순에 따라 정렬하는 Comparator" { + data class User( + val name: String, + val age: Int, + ) : Comparable { + override fun compareTo(other: User): Int = + compareBy(User::name) + .thenBy(User::age) + .compare(this, other) + } + + val users: List = + listOf( + User("Alice", 30), + User("Cathy", 22), + User("Alice", 28), + ) + + // sortedWith(reversedOrder()) == sortedDescending() + val sorted1 = users.sortedWith(reverseOrder()) + val sorted2 = users.sortedDescending() + + val expected: List = + listOf( + User("Cathy", 22), + User("Alice", 30), + User("Alice", 28), + ) + sorted1 shouldBe expected + sorted2 shouldBe expected + } + } + + "reversed - 순서만 뒤집기 (뒤집으면서 정렬하지는 않음)" { + val original: List = listOf(1, 3, 2) + val reversed: List = original.reversed() + + reversed shouldBe listOf(2, 3, 1) + } + } + + "기존 Mutable 컬렉션을 정렬" - { + "sort" { + val original: MutableList = mutableListOf(3, 1, 2) + original.sort() + + original shouldBe listOf(1, 2, 3) + } + + "sortDescending" { + val original: MutableList = mutableListOf(3, 1, 2) + original.sortDescending() + + original shouldBe listOf(3, 2, 1) + } + + "sortBy - MutableList 특정 속성 기준 제자리 정렬" { + data class User(val name: String, val age: Int) + + val users: MutableList = + mutableListOf( + User("A", 30), + User("B", 20), + User("C", 25), + ) + users.sortBy { it.age } + users shouldBe + listOf( + User("B", 20), + User("C", 25), + User("A", 30), + ) + } + + "sortWith - MutableList Comparator 사용 제자리 정렬" { + data class User(val name: String, val age: Int) + + val users: MutableList = + mutableListOf( + User("Alice", 30), + User("Cathy", 22), + User("Bob", 28), + ) + users.sortWith(compareBy { it.age }) // 원본 리스트가 변경됨 + users shouldBe + listOf( + User("Cathy", 22), + User("Bob", 28), + User("Alice", 30), + ) + } + + "reverse - 순서만 뒤집기 (뒤집으면서 정렬하지는 않음)" { + val original: MutableList = mutableListOf(1, 3, 2) + original.reverse() + + original shouldBe listOf(2, 3, 1) + } + } + } + + "Map Sorting" - { + "toSortedMap - 키를 기준으로 오름차순 정렬하여 SortedMap 생성" { + val map: Map = mapOf("c" to 3, "a" to 1, "b" to 2) + val sortedMap: SortedMap = map.toSortedMap() + + sortedMap shouldBe mapOf("a" to 1, "b" to 2, "c" to 3) + } + + "sortedBy - Map의 값을 기준으로 오름차순 정렬하여 List> 생성" { + val map: Map = mapOf("c" to 3, "a" to 1, "b" to 2) + + val sortedList: List> = + map.entries.sortedBy(Map.Entry::value) + + sortedList.map(Map.Entry::value) shouldBe listOf(1, 2, 3) + sortedList.map(Map.Entry::key) shouldBe listOf("a", "b", "c") + } + + "sortedWith - Map의 키를 기준으로 내림차순 정렬하여 List> 생성" { + val map: Map = mapOf("c" to 3, "a" to 1, "b" to 2) + val sortedList: List> = + map.entries.sortedWith(compareByDescending { it.key }) + + sortedList.map(Map.Entry::key) shouldBe listOf("c", "b", "a") + sortedList.map(Map.Entry::value) shouldBe listOf(3, 2, 1) + } + } +}) diff --git a/app/src/test/java/com/example/learningtest/collection/functional/TransformationTest.kt b/app/src/test/java/com/example/learningtest/collection/functional/TransformationTest.kt new file mode 100644 index 0000000..34af80b --- /dev/null +++ b/app/src/test/java/com/example/learningtest/collection/functional/TransformationTest.kt @@ -0,0 +1,182 @@ +package com.example.learningtest.collection.functional + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class TransformationTest : FreeSpec({ + "List Transformation" - { + "distinct - 중복 제거" { + val list: List = listOf(1, 2, 3, 1, 2, 4, 5, 4) + list.distinct() shouldBe listOf(1, 2, 3, 4, 5) + } + + "distinctBy - 특정 조건에 따라 중복 제거" { + data class Person(val name: String, val age: Int) + + val people: List = + listOf( + Person("Alice", 30), + Person("Bob", 25), + Person("Alice", 35), + Person("Charlie", 30), + Person("Bob", 40), + ) + + people.distinctBy(Person::name) shouldBe + listOf( + Person("Alice", 30), + Person("Bob", 25), + Person("Charlie", 30), + ) + + people.distinctBy(Person::age) shouldBe + listOf( + Person("Alice", 30), + Person("Bob", 25), + Person("Alice", 35), + Person("Bob", 40), + ) + } + + "zip - 두 컬렉션을 Pair 로 묶기(크기가 작은 컬렉션에 맞춰서 Pair 가 생성됨)" { + val numbers: List = listOf(1, 2, 3) + val letters: List = listOf("a", "b", "c", "d") + + val zipped: List> = numbers.zip(letters) + zipped shouldBe listOf(1 to "a", 2 to "b", 3 to "c") + } + + "zipWithNext - 인접한 요소끼리 Pair 로 묶기" { + val numbers: List = listOf(1, 2, 3) + + val adjacentPairs: List> = numbers.zipWithNext() + adjacentPairs shouldBe listOf(1 to 2, 2 to 3) + } + + "windowed - 슬라이딩 윈도우를 만들고 각 윈도우에 대한 리스트 반환" { + val numbers = listOf(1, 2, 3, 4, 5, 6) + + // windowed(size = 3, step = 1, partialWindows = false) + numbers.windowed(3) shouldBe + listOf( + listOf(1, 2, 3), + listOf(2, 3, 4), + listOf(3, 4, 5), + listOf(4, 5, 6), + ) + + // windowed(size = 3, step = 2, partialWindows = false) + numbers.windowed(3, step = 2) shouldBe + listOf( + listOf(1, 2, 3), + listOf(3, 4, 5), + ) + + // windowed(size = 3, step = 2, partialWindows = true) + numbers.windowed(3, step = 2, partialWindows = true) shouldBe + listOf( + listOf(1, 2, 3), + listOf(3, 4, 5), + listOf(5, 6), + ) + + // (1,2,3), (2,3,4), (3,4,5), (4,5,6) => 6, 9, 12, 15 + numbers.windowed(3) { it.sum() } shouldBe listOf(6, 9, 12, 15) + } + + "chunked - 컬렉션을 지정된 크기의 덩어리로 나누기" { + val numbers: List = listOf(1, 2, 3, 4, 5, 6, 7) + + numbers.chunked(3) shouldBe + listOf>( + listOf(1, 2, 3), + listOf(4, 5, 6), + listOf(7), + ) + numbers.windowed(size = 3, step = 3, partialWindows = true) shouldBe numbers.chunked(3) + + numbers.chunked(size = 3, transform = { it.sum() }) shouldBe listOf(6, 15, 7) + numbers.chunked(3) { it.sum() } shouldBe listOf(6, 15, 7) + } + + "partition - 조건을 만족하는 요소와 만족하지 않는 요소로 분리" { + val numbers: List = listOf(1, 2, 3, 4, 5, 6) + + val (even, odd) = numbers.partition { it % 2 == 0 } + + even shouldBe listOf(2, 4, 6) + odd shouldBe listOf(1, 3, 5) + } + + "unzip - Pair를 요소로 갖는 리스트를 두 개의 리스트로 분리" { + val pairs = listOf("a" to 1, "b" to 2, "c" to 3) + + val (letters, numbers) = pairs.unzip() + + letters shouldBe listOf("a", "b", "c") + numbers shouldBe listOf(1, 2, 3) + } + } + + "Set Transformation" - { + val set: Set = setOf(1, 2, 3, 4) + + "zip - 두 Set을 Pair로 묶기" { + val set1: Set = setOf("a", "b", "c") + val set2: Set = setOf(1, 2, 3, 4) + + set1.zip(set2) shouldBe listOf("a" to 1, "b" to 2, "c" to 3) + } + + "zipWithNext - 인접한 요소끼리 Pair로 묶기" { + set.zipWithNext() shouldBe listOf(1 to 2, 2 to 3, 3 to 4) + } + + "windowed - 슬라이딩 윈도우를 만들고 각 윈도우에 대한 리스트 반환" { + set.windowed(2) shouldBe listOf(listOf(1, 2), listOf(2, 3), listOf(3, 4)) + } + + "chunked - 컬렉션을 지정된 크기의 덩어리로 나누기" { + set.chunked(3) shouldBe listOf(listOf(1, 2, 3), listOf(4)) + } + + "partition - 조건을 만족하는 요소와 만족하지 않는 요소로 분리" { + val sets: Set = setOf(1, 2, 3, 4, 5, 6) + val (even: List, odd: List) = sets.partition { it % 2 == 0 } + even shouldBe listOf(2, 4, 6) + odd shouldBe listOf(1, 3, 5) + } + + "unzip - Pair를 요소로 갖는 Set을 두 개의 Set로 분리" { + val sets: Set> = setOf(1 to 2, 3 to 4, 5 to 6) + val (set1: List, set2: List) = sets.unzip() + set1 shouldBe listOf(1, 3, 5) + set2 shouldBe listOf(2, 4, 6) + } + } + + "Map Transformation" - { + val map: Map = mapOf("a" to 1, "b" to 2, "c" to 3) + + "mapKeys - Key에 변환 함수 적용하여 새로운 Map 생성" { + val transformedKeysMap = map.mapKeys { (key, _) -> key.uppercase() } + transformedKeysMap shouldBe mapOf("A" to 1, "B" to 2, "C" to 3) + } + + "mapValues - Value에 변환 함수 적용하여 새로운 Map 생성" { + val transformedValuesMap = map.mapValues { (_, value) -> value * 10 } + transformedValuesMap shouldBe mapOf("a" to 10, "b" to 20, "c" to 30) + } + + "entries, keys, values - Map의 구성 요소를 Collection으로 변환" { + map.entries shouldBe + setOf( + mapOf("a" to 1).entries.first(), + mapOf("b" to 2).entries.first(), + mapOf("c" to 3).entries.first(), + ) + map.keys shouldBe setOf("a", "b", "c") + map.values shouldBe listOf(1, 2, 3) + } + } +})