반응형

https://play.kotlinlang.org/koans/Collections/Introduction/Task.kt

 

Kotlin Playground: Edit, Run, Share Kotlin Code Online

 

play.kotlinlang.org

코틀린 공식문서에 있는 Colletions 파트를 읽으며 정리해보았습니다.


Filter / Map

Mapping

코틀린 기본 라이브러리는 컬렉션을 변환할 수 있는 여러 확장함수를 제공하는데 이 함수들은 전에 있던 컬렉션을 기반으로 변환 기준에 맞춰 새로운 컬렉션을 만든다.

Map

mapping은 컬렉션에 대한 함수의 결과인 원소들로 새로운 컬렉션을 만든다. 가장 기본적인 매핑 함수로는 map()이 있다. 

주어진 람다식을 각 요소들에 적용시키고 그 결과값을 새로운 리스트로 반환합니다. 정렬의 결과는 원소의 original 정렬과 동일하다. 원소들의 인덱스도 사용하고 싶다면 mapIndexed()를 사용하면 된다.

val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 }) // print => [3, 6, 9]
println(numbers.mapIndexed { idx, value -> value * idx }) // print => [0, 2, 6]

특정한 원소가 null이라면 map() 대신 mapNotNull()을 사용하거나 mapIndexed()대신 mapIndexedNotNull()을 사용하여 null을 필터링할 수 있다.

val numbers = setOf(1, 2, 3)
println(numbers.mapNotNull { if ( it == 2) null else it * 3 }) // print => [3, 9]
println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx }) // print => [2, 6]

map을 변환시킬 때는 두 가지 옵션이 있다. key만 바꾸거나, 값만 바꾸거나. key를 변환하려면 mapKeys()를 value를 변환하려면 mapValues()를 사용하면 된다.

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
println(numbersMap.mapKeys { it.key.uppercase() }) // print => {KEY1=1, KEY2=2, KEY3=3, KEY11=11}
println(numbersMap.mapValues { it.value + it.key.length }) // print => {key1=5, key2=6, key3=7, key11=16}

Zip

zipping은 두 컬렉션의 같은 위치의 원소들을 쌍으로 만들어 준다. Kotlin standartd library에 있는zip() 함수로 사용할 수 있다.

zip()을 호출할 때 인자로 컬렉션과 컬렉션에 속하는 다른 배열을 인자로 넣으면 List of Pair 객체를 반환해준다.

receiver의 원소가 쌍의 첫번째 원소로 들어가게 된다.  만약 두 컬렉션의 크기가 다르다면 작은쪽의 크기로 따른다.

zip()은 a zip b 와 같은 형태로도 호출이 가능하다.

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors zip animals)

val twoAnimals = listOf("fox", "bear") // print => [(red, fox), (brown, bear), (grey, wolf)]
println(colors.zip(twoAnimals)) // print => [(red, fox), (brown, bear)]

형변환과 동시에 zip()을 사용할 수 있다(두 개의 parameters를 가짐 - receiver element / argument element).

이러한 경우에는 결과 List는 receiver, arugment 원소의 동일한 위치의 값이 형변환된 쌍의 값이 들어있습니다.

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")

println(colors.zip(animals) { color, animal -> "The ${animal.replaceFirstChar { it.uppercase() }} is $color"})
// print => [The Fox is red, The Bear is brown, The Wolf is grey]

List of Pair 즉 쌍의 리스트가 있으면 unzipping을 통하여 두 개의 리스트로 나눌 수 있다. 

첫 번째 리스트는 첫 번째 원소로 생성, 두 번째 리스트는 두 번째 원소로 생성.

unzip()을 사용하여 unzipping이 가능.

val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip()) // print => ([one, two, three, four], [1, 2, 3, 4])

Filtering

filtering은 컬렉션 작업중 가장 인기있는 것들 중 하나이다. 코틀린에서 filtering condition은 predicate(조건자)로 정해진다.  람다식은 컬렉션 원소를 받고 Boolean으로 반환한다. true는 조건이 성립할 때, false는 조건이 성립하지 않을 때.

기본 라이브러리는 single call로 컬렉션을 필터링할 수 있는 확장 함수 그룹을 포함하고 있다. 이 함수들은 기존의 컬렉션에는 영향이 가지 않기 때문에 read-only, mutable 컬렉션 모두에서 사용할 수 있다.

필터링된 결과를 사용하려면 필터링 후 그것을 변수로 할당하거나 메서드 체이닝을 해야한다.

Filter by predicate

기본적인 필터링 함수는 filter() 이다. filter() 함수를 predicate(조건자)와 호출하면 컬렉션에서 조건을 만족하는 원소들을 반환한다.

List와 Set 모두에서 결과는 List로 나오게 된다. Map에서는 Map으로 나오게 된다.

val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3) // print => [three, four]

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap) // print => {key11=11}

filter() 내부의 predicates는 원소의 값만 확인할 수 있다. 만약 원소의 위치가 필터링하는데 필요하다면 filterIndexed()를 사용해야 한다. 

필터링을 negative condition으로 하고 싶다면 filterNot()을 사용하면 된다. 이것은 조건식이 false인 값의 리스트를 반환한다.

val numbers = listOf("one", "two", "three", "four")

val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5)  }
val filteredNot = numbers.filterNot { it.length <= 3 }

println(filteredIdx) // print => [two, four]
println(filteredNot) // print => [three, four]

주어진 원소의 타입을 필터링하여 원소의 타입을 줄일 수도 있다.

filterIsInstance()는 주어진 타입의 컬렉션 원소를 반환한다. List<Any> 타입의 리스트에서 filterIsInstance<T>를 사용하게 된다면 List<T>만 반환하게 된다.

val numbers = listOf(null, 1, "two", 3.0, "four")
println("All String elements in upper case:") // print => All String elements in upper case:
numbers.filterIsInstance<String>().forEach {
    println(it.uppercase())
}
// print => TWO
// 			FOUR

filterNotNull()은 null이 아닌 모든 원소를 반환한다. 

val numbers = listOf(null, "one", "two", null)
numbers.filterNotNull().forEach {
    println(it.length)   // length is unavailable for nullable Strings
}
// print => 3
//			3

Basic Code

Shop.kt 의 코드 구성

data class City(val name: String) {
    override fun toString() = name
}

data class Product(val name: String, val price: Double) {
    override fun toString() = "'$name' for $price"
}

data class Order(val products: List<Product>, val isDelivered: Boolean)

data class Customer(val name: String, val city: City, val orders: List<Order>) {
    override fun toString() = "$name from ${city.name}"
}

data class Shop(val name: String, val customers: List<Customer>)

문제

// Find all the different cities the customers are from
fun Shop.getCustomerCities(): Set<City> =
        TODO()

// Find the customers living in a given city
fun Shop.getCustomersFrom(city: City): List<Customer> =
        TODO()

정답

// Find all the different cities the customers are from
fun Shop.getCustomerCities(): Set<City> =
    customers.map{ it.city }.toSet()

// Find the customers living in a given city
fun Shop.getCustomersFrom(city: City): List<Customer> =
    customers.filter{it.city == city}

 


All, Any, and other predicates

Test predicates

  • any()는 적어도 하나의 원소가 주어진 조건자를 만족시키면 true를 반환한다.
  • none()는 모든 원소들이 조건자를 만족시키지 못할 때 true를 반환한다.
  • all()은 모든 원소가 조건자를 만족시킬 때 true를 반환한다.
    • all()은 빈 컬렉션에서는 조건식에 상관없이 true를 반환한다. 이것을 vacuous truth 라고 한다.
val numbers = listOf("one", "two", "three", "four")

println(numbers.any { it.endsWith("e") }) // print => true
println(numbers.none { it.endsWith("a") }) // print => true
println(numbers.all { it.endsWith("e") }) // print => false

// vacuous truth
println(emptyList<Int>().all { it > 5 }) // print => true

any()와 none()은 또한 조건식없이 사용할 수 있다.

any()는 컬렉션이 비어있으면 false를 반환하고 none()은 컬렉션이 비어있으면 true를 반환한다.

val numbers = listOf("one", "two", "three", "four")
val empty = emptyList<String>()

println(numbers.any()) // print => true
println(empty.any()) // print => false

println(numbers.none()) // print => false
println(empty.none()) // print => true

Retrieve by condition

first()와 last() 함수는 컬렉션에서 주어진 조건식과 함께 원소를 찾을 수 있다.

first()는 조건에 맞는 첫 번째 원소를 반환하고 last()는 조건에 맞는 마지막 원소를 반환한다.

val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.first { it.length > 3 }) // print => three
println(numbers.last { it.startsWith("f") }) // print => five

만약 조건에 조건에 맞는 원소가 없다면 예외가 발생한다. 이것을 피하기위해 firstOrNull() 혹은 lastOrNUll()을 사용해야한다.

이 함수들은 조건에 맞는 원소가 없다면 null을 반환한다.

val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.firstOrNull { it.length > 6 }) // print => null
  • find() 는 firstOrNull() 대신 사용 가능.
  • findLast() 는 lastOrNull() 대신 사용 가능.
val numbers = listOf(1, 2, 3, 4)
println(numbers.find { it % 2 == 0 }) // print => 2
println(numbers.findLast { it % 2 == 0 }) // print => 4

문제

 

// Return true if all customers are from a given city
fun Shop.checkAllCustomersAreFrom(city: City): Boolean =
        TODO()

// Return true if there is at least one customer from a given city
fun Shop.hasCustomerFrom(city: City): Boolean =
        TODO()

// Return the number of customers from a given city
fun Shop.countCustomersFrom(city: City): Int =
        TODO()

// Return a customer who lives in a given city, or null if there is none
fun Shop.findCustomerFrom(city: City): Customer? =
        TODO()

정답

// Return true if all customers are from a given city
fun Shop.checkAllCustomersAreFrom(city: City): Boolean =
        customers.all{it.city == city}

// Return true if there is at least one customer from a given city
fun Shop.hasCustomerFrom(city: City): Boolean =
        customers.any{it.city == city}

// Return the number of customers from a given city
fun Shop.countCustomersFrom(city: City): Int =
        customers.count{it.city == city}

// Return a customer who lives in a given city, or null if there is none
fun Shop.findCustomerFrom(city: City): Customer? =
        customers.find{it.city == city}

Associate

Associate 형변환은 컬렉션이 원소들과 특정한 값을 연관지어서 맵을 만든다. 다른 association 형태로는 원소들이 map의 key나 value가 될 수 있다.

association의 기본 함수는 associateWith()이다. 원래 컬렉션은 map의 key가 되고 주어진 부분은 value가 된다. 만약 두 개의 원소가 동일하다면 map에서 마지막 원소만 남게 된다.

val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length }) // Print => {one=3, two=3, three=5, four=4}

map을 만들 때 컬렉션 원소를 값으로하고 싶다면 associateBy() 함수가 있다.

associateBy()sms 함수를 받고 원소의 값을 기반으로한 key를 반환한다. 만약 두 원소의 키가 동일하다면 마지막 원소만 map에 남는다.

associateBy()은 또한 value 를 형변환하는 함수와 같이 호출할 수 있다.

val numbers = listOf("one", "two", "three", "four")

println(numbers.associateBy { it.first().uppercaseChar() })
// print => {O=one, T=three, F=four}
println(numbers.associateBy(keySelector = { it.first().uppercaseChar() }, valueTransform = { it.length }))
// print => {O=3, T=5, F=4}

키와 맵이 있는 컬렉션에서 map을 생성하기 위한 또 다른 함수인 associate() 함수도 존재한다.

 

associate() 함수는 람다식을 받고 Pair을 반환한다. associate()를 퍼포먼스에 영향이 가는 Pair를 객체를 아주 잠깐 생성한다. 

그러므로 associate()는 퍼포먼스가 중요하지 않거나 다른 옵션보다 선호될 때 사용하는 것이 좋다.

키와 그에 상응하는 값은 하나의 원소에서 생성될 수 있다.

// 공식문서에서는 parseFullName 함수가 없어 약간의 수정을 함.
fun main() {

    val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
    println(names.associate  { name -> parseFullName(name).let { name ->
        var lastName = name[1]
        var firstName = name[0]
        lastName to firstName } } )
    // print => {Adams=Alice, Brown=Brian, Campbell=Clara}

}

fun parseFullName(fullName : String) : List<String> = fullName.split(" ")

문제

// Build a map from the customer name to the customer
fun Shop.nameToCustomerMap(): Map<String, Customer> =
        customers.associateBy{it.name}

// Build a map from the customer to their city
fun Shop.customerToCityMap(): Map<Customer, City> =
        customers.associateWith{it.city}

// Build a map from the customer name to their city
fun Shop.customerNameToCityMap(): Map<String, City> =
        customers.associate{it.name to it.city}

정답

// Return a list of customers, sorted in the descending by number of orders they have made
fun Shop.getCustomersSortedByOrders(): List<Customer> = 
    customers.sortedByDescending{it.orders.size}

Grouping

코틀린 기본 라이브러리는 컬렉션 원소들을 그루핑 할 수 있는 확장 함수를 제공한다. 가장 기본적인 grouping 함수는 groupBy()이다. 람다식을 받고 Map을 반환한다. 이 Map에서 각각의 key는 람다 결과이고 그에 상응하는 value는 반환된 원소의 List 이다.

groupBy()는 두 번째 람다 인자(값을 형변환 해주는 함수)와 같이 호출될 수 있다. 

두 가지 람다식이 있는 groupBy()로 생성된 map의 key는 keySelector 함수에 의해 생성된다. KeySelector은 원래의 원소가 아니라value transformation 함수의 결과를 매핑한다.

val numbers = listOf("one", "two", "three", "four", "five")

println(numbers.groupBy { it.first().uppercase() }) // print => {O=[one], T=[two, three], F=[four, five]}
println(numbers.groupBy(keySelector = { it.first() }, valueTransform = { it.uppercase() })) // print => {o=[ONE], t=[TWO, THREE], f=[FOUR, FIVE]}

원소들을 묶고 한 작업을 모든 그룹들에 한 번에 하고 싶다면 groupingBy() 함수를 사용할 수 있다. 이 함수는 Grouping 타입의 객체를 반환한다. Grouping 객체는 lazy manner로 모든 그룹에 작업을 적용해준다. 그루들은 작업이 실행되기 직전에 만들어 집니다.

따라서 Grouping은 아래의 작업들을 지원한다.

  • eachCoung() 는 각 그룹의 원소의 개수를 세준다.
  • fold()와 reduce()는 각 그룹을 개별적인 컬렉션으로 지정한 후 fold and reduce를 진행한 후 결과를 반환한다.
  • aggregate()는 주어진 작업을 각 그룹에 모든 요소에 적용하고 결과를 반환한다.
val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.groupingBy { it.first() }.eachCount()) // print => {o=1, t=2, f=2, s=1}

문제

// Build a map that stores the customers living in a given city
fun Shop.groupCustomersByCity(): Map<City, List<Customer>> =
        TODO()

정답

// Build a map that stores the customers living in a given city
fun Shop.groupCustomersByCity(): Map<City, List<Customer>> =
        customers.groupBy{it.city}

 


Partition

filtering의 또 다른 함수로 partition()이 있다. 이 함수는 조건식에 따라 조건에 맞는 원소들을 필터링하고 맞지 않는 원소들은 따로 유지한다. 따라서 리턴값으로 Pair of List(리스트의 쌍)을 가진다.  첫 번째 리스트는 조건에 맞는 원소들, 두 번째 리스트는 그 외의 모든 원소를 포함한다.

val numbers = listOf("one", "two", "three", "four")
val (match, rest) = numbers.partition { it.length > 3 }

println(match) // print => [three, four]
println(rest) // pring => [one, two]

문제

// Return customers who have more undelivered orders than delivered
fun Shop.getCustomersWithMoreUndeliveredOrders(): Set<Customer> = TODO()

정답

// Return customers who have more undelivered orders than delivered
fun Shop.getCustomersWithMoreUndeliveredOrders(): Set<Customer> = 
    customers.filter{ 
        val (delivered, undelivered) = it.orders.partition {it.isDelivered}
        undelivered.size > delivered.size
    }.toSet()

Flatten

중첩된 컬렉션을 작업하는 경우 Kotlin Standard Library이 제공하는 flatten()과 같은 flat access한 함수를 사용할 수 있다.

컬렉션안에 컬렉션이 있을 때 flatten()을 호출할 수 있다.

val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
println(numberSets.flatten()) // print => [1, 2, 3, 4, 5, 6, 1, 2]

또 다른 함수로는 flatMap()이 있다. flatMap()은 중첩된 컬렉션 처리를 유연하게 할 수 있다. 이 함수는 컬렉션 원소를 다른 컬렉션에 매핑하는 함수를 받는다. 결과적으로 flatMap()은 하나의 리스트에 모든 원소를 포함하여 반환한다. 이처럼 flatMap()은 map()을 호출한 후 flatten()을 호출한 것과 같다.

val containers = listOf(
    StringContainer(listOf("one", "two", "three")),
    StringContainer(listOf("four", "five", "six")),
    StringContainer(listOf("seven", "eight"))
)
println(containers.flatMap { it.values }) 
// print => [one, two, three, four, five, six, seven, eight]

문제

// Return all products the given customer has ordered
fun Customer.getOrderedProducts(): List<Product> =
        TODO()

// Return all products that were ordered by at least one customer
fun Shop.getOrderedProducts(): Set<Product> =
        TODO()

정답

// Return all products the given customer has ordered
fun Customer.getOrderedProducts(): List<Product> =
	orders.flatMap(Order::products)
// Return all products that were ordered by at least one customer
fun Shop.getOrderedProducts(): Set<Product> =
    	customers.flatMap(Customer::getOrderedProducts).toSet()

다음 글

코틀린[Kotlin] 컬렉션 정리 - 최대 및 최소 / 합계 등(About the Collections in Kotlin -Max or Min / Sum etc..)

 

코틀린[Kotlin] 컬렉션 정리 - 최대 및 최소 / 합계 등(About the Collections in Kotlin -Max or Min / Sum etc..)

https://play.kotlinlang.org/koans/Collections/Introduction/Task.kt Kotlin Playground: Edit, Run, Share Kotlin Code Online play.kotlinlang.org 코틀린 공식문서에 있는 Colletions 파트를 읽으며 정리해보..

soopeach.tistory.com

 

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기