실제 서비스를 위해 코딩하다보면 String 다뤄야할 때가 많다.
또한 내가 기업 코테를 많이 본 것은 아니지만 코테 볼 때마다 매번 간접적으로나 직접적으로나 String 핸들링 문제가 나온 걸로 기억한다.
매우 쉬운 거라고 생각해서 방심하다가 뒷통수 맞기 딱 좋다. (사실 최근에 맞고 울었다.)
자주 만나는 문제일 수록 많은 코드를 작성하게 되니, 그 코드를 간결하게, 우아하게 그리고 정확하게 코딩하기 위해 String 클래스와 확장함수를 뜯어보며 정리해보려고한다.
실제 서비스이든 알고리즘 테스트이든 도구를 사용해서 시간을 아끼고 로직에 집중하자는 내 스스로의 취지이다.
또한 내부구현을 살펴보면 내가 커스텀 기능을 만들어야할 때 좋은 귀감이 될 수 있다고 생각한다.
일단 넓고 얇게 다루어 상황에 맞게 떠올려보고 실전에서 쓰며 확신을 가지고 사용하려고 한다. (콜렉션에 쓰이는 방식과 비슷한 것들도 많으니까 더 좋다.)
이후 다른 글에서 (Algorithm - 풀이 회고) 문제를 풀어보며 도구를 활용해볼 예정이다.
이 글을 편하게 읽으려면 확장함수와 람다에 대한 이해가 있으면 더 좋다.
[Hello, String]
클래스부터 살펴보자
package kotlin
public class String : Comparable<String>, CharSequence {
companion object {}
public operator fun plus(other: Any?): String
public override val length: Int
public override fun get(index: Int): Char
public override fun subSequence(startIndex: Int, endIndex: Int): CharSequence
public override fun compareTo(other: String): Int
}
Comparable 과 CharSequence 를 구현하고 있다.
생각보다 심플해서 놀란 사람도 있을 것이다. (난 그랬었다 헤헤) CharSequence 에 내가 아는 substirng, replace 같은 함수가 있는 건가 싶었다.
package kotlin
public interface CharSequence {
public val length: Int
public operator fun get(index: Int): Char
public fun subSequence(startIndex: Int, endIndex: Int): CharSequence
}
CharSequence 는 Char 의 연속으로 위와같은 멤버를 가진 인터페이스이다.
우리가 자연스레 사용해왔던 String 함수는 String (또는 CharSequence) 의 확장함수였던것..!
package kotlin.text 를 찾아가보니.. 너무 많다!
그래서 내가 자주 썼던 것과 새로 써보고 싶은 것들의 내부 코드를 돌아보고 정리해보려고 한다.
[String 핸들링에 쓸만한 확장함수 정리]
[A]
- all
- (Char) -> Boolean 함수를 인자로 받아서 하나라도 만족하지 못하면 false 반환하고
- 모두 만족하면 true 를 반환한다.
- associate
- (Char) -> Pair<K, V> 함수를 인자로 받아서 Map<K, V> 로 반환해준다.
- Pair 는 to 연산자를 사용해서 간단하게 만들 수 있고
- LinkedHashMap 을 반환해서 순서를 지켜준다는 게 특징이다.
- 이거만 제대로 쓰면 associateTo, associateBy, associateWith 는 몰라도 될 것 같다.
// 람다의 입력은 각 Char 이고, 반환이 하나라도 false 면 false
public inline fun CharSequence.all(predicate: (Char) -> Boolean): Boolean {
for (element in this) if (!predicate(element)) return false
return true
}
// 람다의 입력은 각 Char, 반환은 키 벨류 Pair 로 K to V 로 만들 수 있다.
// LinkedHashMap 으로 순서를 지켜준다는 게 특징
// 이걸 알면 associateBy, associateWith 는 몰라도 될 것 같다.
public inline fun <K, V> CharSequence.associate(transform: (Char) -> Pair<K, V>): Map<K, V> {
val capacity = mapCapacity(length).coerceAtLeast(16) // 문자열 길이만큼 잡고 16보다 작으면 16을 크기로 설정
return associateTo(LinkedHashMap<K, V>(capacity), transform)
}
public inline fun <K, V, M : MutableMap<in K, in V>> CharSequence.associateTo(destination: M, transform: (Char) -> Pair<K, V>): M {
for (element in this) {
destination += transform(element)
}
return destination
}
[C]
- contains, in
- 인자 종류는 크게 세 가지 CharSequence (고로 String 도 가능), Char, Regex
- CharSequence, Char 는 ignoreCase 가 들어가고 true 면 대소문자 무시
- operator contains 이니 in 으로 대체사용 가능 (본문 최하단 사용 예시 참고)
- compareTo
- 인자로 주어진 다른 String 과 우선순위가 같다면 0 반환
- 인자로 주어진 다른 String 보다 작다면 음수 반환
- 인자로 주어진 다른 String 보다 크다면 양수 반환
- codePointAt
- 인자로 인덱스를 전달하면 해당 Char 를 Int 로 반환해준다.
- StringsJVM
- contentEquals
- 인자로 주어진 CharSequence 나 StinrgBuffer 가
- 같은 개수의 문자를 같은 순서로 가지고 있으면 true, 아니면 false 를 반환한다.
- chunk
- 인자로 주어진 size 씩 새 String 으로 잘라 그것을 원소로 가진 List<String> 를 반환한다. (마지막은 원소는 size 길이가 아닐 수 있다.)
- size 와 함께 (CharSequence) -> R 의 람다를 추가로 전달하면 List<R> 을 반환한다. 즉 각 size 만큼 묶인 문자열 값으로 조작을 거쳐 List 로 반환받을 수 있다는 말이다.
- 내부적으로 window 를 사용한다.
- commonPrefixWith, commonSuffixWith
- 인자로 주어진 CharSequence 와 같이 똑같이 가진 공통된 Prefix 를 String 으로 반환한다.
- 공통된 Prefix 가 없다면 빈 문자열을 반환한다.
- ignoreCase 도 받는다.
- count
- 인자로 함수를 넣지 않으면 내부적으로는 length 프로퍼티를 반환한다.
- 하지만 (Char) -> Boolean 함수를 전달하면 해당 조건에 맞는 문자들의 갯수를 반환한다.
- filter 랑 length 여서 뭐가 장점인지 모르겠다.
// contains 세 가지
public operator fun CharSequence.contains(other: CharSequence, ignoreCase: Boolean = false): Boolean =
if (other is String)
indexOf(other, ignoreCase = ignoreCase) >= 0
else
indexOf(other, 0, length, ignoreCase) >= 0
public operator fun CharSequence.contains(char: Char, ignoreCase: Boolean = false): Boolean =
indexOf(char, ignoreCase = ignoreCase) >= 0
public inline operator fun CharSequence.contains(regex: Regex): Boolean = regex.containsMatchIn(this)
// commonPrefixWith
public fun CharSequence.commonPrefixWith(other: CharSequence, ignoreCase: Boolean = false): String {
val shortestLength = minOf(this.length, other.length)
var i = 0
while (i < shortestLength && this[i].equals(other[i], ignoreCase = ignoreCase)) {
i++
}
if (this.hasSurrogatePairAt(i - 1) || other.hasSurrogatePairAt(i - 1)) {
i--
}
return subSequence(0, i).toString()
}
[D]
- drop
- 앞에서부터 인자로 주어진 수만큼 제거한 String 을 반환한다.
- substring 보다 직관적이어서 좋다.
- dropLast
- 뒤에서부터 인자로 주어진 수만큼 제거한 String 을 반환한다.
- dropLastWhile
- 뒤에서부터 (Char) -> Boolean 함수를 만족하는 문자들만 제거해서 String 으로 반환한다.
- 시간복잡도 O(N) 이다.
- 구현 코드를 참고하는 게 이해가 더 빠를 것이다.
public inline fun String.dropLastWhile(predicate: (Char) -> Boolean): String {
for (index in lastIndex downTo 0)
if (!predicate(this[index]))
return substring(0, index + 1)
return ""
}
[E]
- elementAtOrNull
- get 이랑 똑같지만 인자로 주어진 index 가 out of bounds 여도 Exception 이 나지 않고 null 을 반환
- elementAtOrElse
- 마찬가지로 get 이랑 똑같지만 인자로 주어진 index 가 out of bounds 라면 (Int) -> Char 함수에 있는 문자를 반환한다. (디폴트 밸류 개념)
- endsWith
- 인자로 전달된 다른 String 과 똑같이 끝난다면 true 아니라면 false 반환
- ignoreCase Boolean 값도 인자로 받아서 true 면 대소문자 구분을 무시할 수 있다.
[F]
- filter
- 인자로 (Char) -> Boolean 함수를 전달해서 조건에 맞는 문자들만 남겨서 String 으로 반환한다.
- 시간복잡도 O(N) 이다.
- filterNot
- 조건에 맞지 않는 문자만 남겨서 String 으로 반환한다.
- filterIndexed
- 인자가 (Int, Char) -> Boolean 으로 전달하는 함수에 Index 정보를 받을 수 있는 것 외에는 filter 와 동일
- first
- 인자 없이 호출하면 첫 문자열 반환
- 빈 문자열일 경우 NoSuchElementException
- 인자로 (Char) -> Boolean 함수를 전달하면 시작부터 하나씩 문자를 돌면서 조건에 Boolean 처음으로 조건에 맞는 문자 반환 (이 경우 역시 빈 문자열일 경우 NoSuchEelementException)
- 예외가 싫다면 firstOrNull 사용
- find
- 이 역시 (Char) -> Boolean 인자를 전달해서
- 앞에서부터 끝까지 각 Char 를 돌며 처음으로 조건에 맞는 Char 를 반환한다.
- first 와 차이점은 없다면 예외를 던지는 것이 아닌 null 을 반환한다는 것이다.
- findLast
- 뒤에서부터 find
- findAnyOf (findLastAnyOf 생략)
- Collection<String> 타입을 인자로 받고 옵션으로 startIndex (Int) 와 ignoreCase (Boolean) 를 전달할 수 있다.
- 반환은 이 함수를 호출한 String 의 startInex 부터 고려했을 때, 가장 먼저 매칭되는 원소가 포함된 인덱스와 그 원소를 반환한다.
- 말로 하면 어렵고 하단에 사용 예시 코드를 보면 이해가 더 쉬울 것이다.
- 시간복잡도 collection 의 원소갯수를 N 이라고하고 이 함수를 호출한 Stirng 길이를 M 이라고 했을 때 시간복잡도 O(N*M*a) 으로 예상된다. a 는 CharSequence.regionMatches 이다.
- 내부구현 코드를 살펴봤는데 왜 저렇게 구현했는지 이해할 수 없다. contains 와 indexOf 를 활용하는 게 훨신 직관적일 것 같은데..? 의도를 숙제로 남기기로했다.
// findAnyOf 의 핵심이 되는 코드
for (index in indices) {
val matchingString = strings.firstOrNull { it.regionMatches(0, this, index, it.length, ignoreCase) }
if (matchingString != null)
return index to matchingString
}
val text = "Android developers"
println(text.findAnyOf(listOf("and", "And", "roid", "developers"))) // (0, And)
println(text.findAnyOf(listOf("and", "roid", "developers"))) // (3, roid)
println(text.findAnyOf(listOf("and", "developers"))) // (8, developers)
- format
- C 언어에서 프린트할 때 생각하면 된다
- "입력된 숫자는 %d 입니다.".format(7) 이런식으로 사용할 수 있다.
- forEach (forEachIndexed 생략)
- (Char) -> Unit 함수 전달로 각 원소 이터레이트 할 수 있다.
- flatMap
- (Char) -> Iterable<R> 함수를 인자로 전달하여 List<R> 반환
- 각 원소마다 인자로 전달한 함수를 호출하여 반환된 Iterable 을 addAll 하는 방식으로 구현되어있다.
- 언제써야하는지는 문제를, 사용법은 하단 예를 보는 게 더 이해가 잘 될 것 같다.
- flatMapIndexed
- ~Indexed 가 붙었으니, (Int, Char) -> Iterable<R> 함수를 인자로 전달하고 List<R> 반환이다.
- fold
- 인자로 초기값(타입 R) 과 (R, Char) -> R 을 전달하고,
- 문자열의 각 문자를 돌며 R 을 변형시켜가서 최종 R 타입 값을 반환한다.
- List<Int> 같은 클래스의 각 원소의 합 계산할 때 주로 썼던 것 같다.
- 아래는 예시코드이다.
val text = "Android developers"
println(text.fold("") { r, c -> "$r${c}_" }) //A_n_d_r_o_i_d_ _d_e_v_e_l_o_p_e_r_s_
- foldRight
- fold 를 lastIndex 부터 실행하도록 구현되어있다.
- 인자로 초기값(타입 R)과 (Char, R) -> R 을 전달한다. (순서 주의)
[G]
- get, []
- 전달된 Index (Int) 로 Char 를 반환한다.
- [i] 방식으로 배열접근하듯이 가져올 수 있다.
- 문자열을 벗어나는 인덱스를 쓰면 예외발생하는 게 싫으면 getOrNull, getOrElse 를 사용
- getOrNull 은 단순히 인덱스가 문자열을 벗어나면 null 을 반환하게 구현되어있고
- getOrElse 는 인자에 인덱스와 추가적으로 (Int) -> Char 함수를 전달해서 인덱스가 문자열을 벗어날 때 디폴트 값을 설정할 수 있다.
- groupBy
- 두 가지로 오버라이딩 되어있다. 반환은 Map 으로 같다.
- 1. 문자열의 각 문자에 대해 (Char) -> K 만 전달하여 Map<K, List<Char>> 로 반환받는 방식
- 내부 구현은 groupBy 를 호출한 문자열의 각 Char 를 돌면서 전달한 함수로 Key 를 생성하고 그 키를 getOrPut 함수에 인자에 넣어서 키에해당하는 Value 가 없으면 ArrayList<Char> 생성하고 가져오고, 있으면 저장된 ArrayList<Char> 가져와서 현재 현재 Char 를 추가한다. (쓰고나니 코드로보는 게 더 이해 빠를듯)
- 2. 문자열의 각 문자에 대해 (Char) -> K 와 (Char) -> V 함수 두 개를 전달하여 Map<K, List<V>> 를 반환받는 방식
- 1과 내부구현 차이는 Map 의 Value 즉 ArrayList 에 원소를 넣을 때 (Char) -> V 로 생성해서 넣는다는 점이다.
- 사실 이건 써본 적 없는데, 당장 생각나는 유용한 상황은 문자열의 각 원소 갯수 셀 때이다.
// 함수 두 개 받는 2번 구현
public inline fun <K, V, M : MutableMap<in K, MutableList<V>>> CharSequence.groupByTo(destination: M, keySelector: (Char) -> K, valueTransform: (Char) -> V): M {
for (element in this) {
val key = keySelector(element)
val list = destination.getOrPut(key) { ArrayList<V>() }
list.add(valueTransform(element))
}
return destination
}
// 예시
println(
text.groupBy(
keySelector = {c -> c},
valueTransform = {c -> "${c}변형"}
)
)
//출력 : {A=[A변형], n=[n변형], d=[d변형, d변형, d변형], r=[r변형, r변형], o=[o변형, o변형], i=[i변형], =[ 변형], e=[e변형, e변형, e변형], v=[v변형], l=[l변형], p=[p변형], s=[s변형]}
[I]
- indices (함수 아님)
- 0..length-1 까지의 IntRange
- isBlank, isEmpty, isNotBlank, isNotEmpty, isNullOrBlank, isNullOrEmpty
- Blank 와 Empty 의 차이만 알면 될 것 같다.
- Empty 는 빈 문자열에만 true 를 반환하고
- Blank 는 빈 문자열 또는 whitespace characters(' ', \t, \n, \u00A0 같은 것들) 로만 이루어져있으면 true 를 반환한다.
- Not 이 붙으면 부정이고
- NullOr 은 말그대로 Null 이어도 true 를 반환한다는 의미이다.
- indexOf
- 인자로 Char, Int, Boolean 또는 String, Int, Boolean 을 받고
- Int 는 startIndex 를, Boolean 은 IgnoreCase 옵션을 의미한다. 옵션 안 넣으면 0, false 가 디폴트.
- 인자로 받은 Char 또는 String 이 처음으로 발견되는 index (Int) 를 반환한다. 없으면 -1 을 반환한다.
- 나중에 [L] 에서 나올 lastIndexOf 와 내부 구현이 같은 함수로 이뤄진다.
- 시간복잡도 O(N) 인듯 regionMatches 때문
- indexOfAny
- 인자로 Collection<String> 을 받는 버전은 findAnyOf 로 구현되어있다.
- findAnyOf 는 Pair 객체를 반환한다고 했고
- 거기서 인덱스만 즉 Pair 의 first 만 반환한다고 보면 된다.
- 나중에 [L] 에서 나올 lastIndexOfAny 와 내부 구현이 같은 함수로 이뤄진다.
- indexOfFirst, indexOfLast
- CharSequence 의 각 Char 를 돌며 (Char) -> Boolean 함수를 인자로 받고 맨앞 또는 맨뒤에서부터 처음으로 만족하는 인덱스 (Int) 를 반환
- 만족하는 Char 가 없다면
[L]
- last
- first 대칭, 맨 마지막 문자를 반환하거나 빈문자열일 경우 예외
- 예외가 싫다면 lastOrNull 사용
- (Char) -> Boolean 으로 뒤에서부터 차례로 Char 를 돌며 처음으로 Boolean 을 만족하는 Char 를 반환
- 구현 매우 단순 for문이고
- lastIndexOf, lastIndexOfAny
- 각 indexOf, indexOfAny 와 대칭
- lowercase
- java.lang.String 의 toLowerCase(Locale.ROOT) 를 따름
- 보통 모든 대문자를 소문자로 바꾸고 싶을 때 사용
- lines
- CRLF 나 LF or CR 을 구분자로 split 해주어 List<String> 을 반환한다.
- split 으로 할 걸 간결하게 할 수 있어서 좋은 듯
- lastIndex
- length - 1 대신 쓰기 좋다.
- 변수임
[M]
- map
- 각 Char 에 대해 (Char) -> R 함수를 인자로 전달해서 Live<R> 를 반환한다.
- Char 를 원소로 가지는 컬렉션에서의 map 과 동일하게 생각하면 될 것 같다.
- 시간복잡도 O(N)
- mapIndexed
- (Int, Char) -> R 를 인자로 받는 map 이다. Int 가 index를 나타낸다고 보면 된다.
- minOf
- Char) -> R 함수를 인자로 전달해서 R 중에 가장 작은 것을 반환하게 된다.
- 빈 문자열에 이 함수를 호출하면 예외 발생
- 시간복잡도 O(N) 이다.
- minOfOrNull
- minOf 와의 차이점은 빈 문자열이면 예외가 아니라 null 을 반환한다는 점이다.
- minOfWith, minOfWithOrNull
- Comparator<R> 객체와 (Char) -> R 함수를 전달해서
- Comparator 의 compare 에 정의한 기준으로 가장 작은 것을 반환
- maxOf, maxOfOrNull, maxOfWith, maxOfWithOrNull
- 각 min~ 의 반대이다.
- matches
- Regex 를 인자로 전달하고 전달하고 해당 regular expression과 매치되면 true 를 반환하고 그렇지 않다면 false 를 반환한다.
[O]
- orEmpty
- null 일 수도 있는 String 에 null 이면 빈문자열을 반환하고 null 이 아니라면 그대로 반환하는 함수
- string ?: "" 대신 쓰기 보기 좋은 것 같다.
[P]
- plus
- String 클래스에 있던 것이다. operator fun plus 이므로 + 로 대체해서 사용할 수 있다.
- padStart, padEnd
- Int 와 Char 를 인자로 각각 길이와 넣으려는 문자로 생각하고 시작 또는 끝에 길이만큼 만들기 위한 문자를 붙여서 String 으로 반환한다.
- Int 에 음수를 넣으면 에외고 이 함수를 호출한 String 의 length 보다 작거나 같으면 그대로 반환된다.
- 내부구현은 StringBuilder 로 되어있다.
- 예를들어 "Android developers".padStart(19, '*') 라고 하면 반환 값은 "Android developers*" 이된다.
- partition
- (Char) -> Boolean 을 인자로 받아 각 Char 에 대해 Boolean 을 만족하는 것과 만족하지 않는 Pair<String, String> 을 반환한다.
- 내부구현은 StringBuilder 두 개 만들고, for 문으로 String 의 각 Char 돌면서 Boolean 을 만족하는 것과 만족하지 않는 것 하나씩 append 한다.
- 참고로 Pair 의 first 에 Boolean 을 만족하는 게 들어간다.
[R]
- random, randomOrNull
- 문자열 내의 Char 중 랜덤하게 Char 를 반환한다.
- 빈 문자열이 random 함수를 호출하면 예외 던짐 (NoSuchElementException)
- 이를 방지하려면 randomOrNull 사용, 빈 문자열일 경우 null 반환
- 알고리즘 테스트보다 프로젝트에서 테스트할 때나 쓸 것 같다.
- reversed
- 문자열 순서를 거꾸로 뒤집은 String 을 반환한다.
- 내부적으로는 CharSequence 의 reversed 그 내부적으로는 StringBulider 의 reverse 를 사용
- repeat
- Int 를 인자로 받아 해당 값만큼 반복한 String 을 반환한다.
- 내부적으로는 StringBuilder 사용
- replace
- 기본적으로 인자로 두 가지를 받는다. 문자열에서 바꾸고 싶은 것을 Char or String or Regex 로 하나 받고 나머지하나는 어떤 것으로 바꾸고 싶은지 Char or String (첫 인자가 Char 였다면 Char, String or Regex 였다면 String)으로 받는다.
- 그래서 첫 인자에 매칭되는 모든 문자 또는 문자열을 두 번째 인자로 바꾼다는 게 이 함수의 기능이다.
- 첫 번째 인자로 받는 종류에 따라 각각 다르게 구현되어있다.
- 첫 인자가 Char 면 정말 간단하게 forEach 와 StringBuilder 로 구현되어있고
- 첫 인자로 String 을 받으면 반복문과 StringBuilder 로 구현되어있고
- 첫인자로 Regex 를 받으면 Regex 의 replace 를 이용한다.
- 바꿀 게 없다면 호출했던 문자열 그대로 반환된다.
- replaceFirst
- replace 를 사용할 때 매칭되는 모든 값이 아니라 시작부터 처음으로 매칭되는 문자 또는 문자열만 바꾸고 싶다면 이 함수를 사용하면 된다.
- replaceAfter, replaceAfterLast
- 인자로 Char 또는 String 타입의 delimeter 와 String 타입의 replacement 를 받아서 delimeter 이후 (포함 x) 의 스트링을 replacement 로 바꾼 String 을 반환해준다.
- Last 가 안 붙은 것은 시작부터 delimeter 를 찾는 것이고, Last 가 붙은 것은 끝에서부터 delimeter 를 찾는 것이 차이점이다.
- lastIndexOf 와 replaceRange 조합으로 만들어져있다.
- replaceBefore, replaceBeforeLast
- replaceAfter, replaceAfterLast 가 delimeter 의 오른쪽에 replacement 로 바꾼 문자열을 반환한다면 이 함수는 왼쪽을 replacement 로 바꾼 문자열을 반환한다.
- replaceRange
- 인자로 startIndex: Int, endIndex: Int, replacement: CharSequence 를 받는다.
- startIndex 와 endIndex-1 까지의 String 을 replacement 로 바꾼 문자열을 반환한다.
- Int 두 개 대신 IntRange 인자로 사용했을 때 주의점은 range 의 끝부분도 인덱스로 포함된다는 것이다.
- 즉 replaceRange(0, 3, "replacement") 와 replaceRange(0..3, "replacement") 는 결과가 다르다.
- removeRange
- replaceRange 의 remove 버전 replacement 를 빈 문자열로 주는 거랑 같다. (StringBuilder append 한 번 더 하는 정도)
- removePrefix, removeSuffix
- 인자로 CharSequence 타입의 delimeter 를 받고 그 값으로 시작(prefix)하거나 끝(suffix)한다면 그 값을 지운 문자열을 반환한다.
- 만약 시작 끝이 일치하지 않는담녀 그냥 새로운 같은 값을 가진 새로운 문자열로 반환해준다.
- removeSurrounding
- removePrefix 와 removeSuffix 를 같은 delimeter 로 한 번에 제거하고 싶을 때 사용하면된다.
- delimeter 를 하나또는 두 개 인자로 넘겨줄 수있다.
- 주의할 점은 prefix, suffix 둘 중 하나라도 delimeter 와 일치하지 않으면 호출한 문자열을 그대로 반환한다는 것이다.
- reduce
- fold 로 대체해서 쓰는 게 편할 것 같다. 왜냐면 fold 와 헷갈리고 fold 가 반환값이 자유롭기 때문. (acc 위치가 다른 점, 인자 갯수) 그래도 남 코드 읽을 때 참고하려고 썼다.
- fold 는 첫 인자를 개발자가 직접 정할 수 있었지만 reduce 는 문자열의 첫 Char 로 들어간다.
- runningFold, runningFoldIndexed
- 인자로 R 타입 초기 값과, (R, Char) -> R 함수를 인자로 받는 것은 fold 와 같지만 반환이 List<R> 로 그 과정을 모두 담아서 반환해준다는 차이점이 있다.
- Indexed 가 붙으면 역시 (R, Char) -> R 가 아닌 (Int, R, Char) -> R 함수를 인자로 받아서 Int 인덱스 값을 활용할 수 있다는 차이점이 있다.
[S]
- substring
- 인자로 (Int) or (Int, Int) or (IntRange) 중 하나로 받아서 문자열의 일부분을 새로운 문자열로 반환한다.
- (Int) 를 인자로 주면 그 값의 인덱스부터 끝까지의 문자열을 반환한다.
- (Int, Int) 를 인자로 주면 각각 start 와 end 를 의미하고 start..end-1 인덱스를 가진 문자열을 반환한다.
- (IntRange) 를 인자로 주면 그 range 에 있는 인덱스의 문자열을 반환한다.
- 이 역시 IntRange 줄 때 주의할 점은 end 가 포함된다는 것이다.
- substringAfter, substringBefore, substringAfterLast, substringBeforeLast
- 인자로 Char or String 타입의 delimeter 를 받고 그 delimeter 이후 부터의 문자열을 반환한다.
- delimeter 를 발견하지 못한다면 디폴트로는 이 함수를 호출한 문자열 그대로 반환하고, 두 번째 인자로 디폴트 문자열을 전달해 반환받을 수도 있다.
- After/Before, Last 수식어는 replace 와 동일하니 참고한다.
- slice
- 인자로 IntRange 를 주거나 Iterable<Int> 를 주면 해당 인덱스에 해당하는 부분 문자열을 반환한다.
- IntRange 는 사실상 substring 과 다를 게없다. (내부적으로 substring 을 쓰기 때문)
- Iterable<Int> 을 인자로주면 예를들어 "Android developers".slice(listOf(0, 5, 17)) 의 반환은 Ais 이다. 이 경우의 내부구현은 역시나 반복문과 StringBuilder 로 되어있다.
- 어느 경우이든 OutOfBoundException 에 주의한다.
- startWith
- 인자로 String 타입의 prefix 와 옵션으로 Int 타입의 startIndex Boolean 타입의 ignoreCase 를 줘서 startIndex 부터 시작하는 부분 문자열의 앞부분과 prefix 가 일치하면 true 아니라면 false 를 반환한다.
- startIndex 를 주지 않으면 디폴트 값은 0 이다.
- split
- 인자로 Char or String or Regex 타입의 delimeter 들 (Char, String 은 vararg) 줄 수 있고 그 delimeter 를 기준으로 문자열을 나눠 List<String> 을 반환한다.
- 옵션으로 Boolean 타입의 ignoreCase 와 Int 타입의 Limit 을 줄 수 있다.
- limit 은 반환되는 List 의 size 이다. 왼쪽에서부터 delimeter 를 찾는데 limit 만큼까지의 사이즈의 List 로 쪼개 받을 수 있다.
- 내부 구현은 ArrayList, indexOf, substring 으로 이루어져있다. (regex 는 Regex 의 split)
- 주의할 점은 delimeter 가 경계에 있거나 연속으로 있다면 빈 문자열이 List 에 낄 수 있다는 것이다. (filter 를 통해서 가공하든지 하자)
- scan, sanIndexed
- 내부적으로 runningFold 호출함
[T]
- to ~
- 다양한 타입으로 전환 (ex toInt, toFloat ..)
- toInt, toLong, toShort, toByte 등의 정수 류의 to~ 의 인자로 radix 를 전달하면 10진수 이외의 진수도 10진수로 읽어올 수 있다. (ex "1A".toInt(16) 은 26으로 반환됨)
- toList, toMutableList, toSet, toHashSet, toSortedSet 등과 같이 다른 Char 타입 컬렉션으로 바꿀 수도 있다.
- trim
- 맨 앞 맨 끝의 whitespace 를 지워준 부분문자열을 반환한다.
- 인자로 (Char) -> Boolean 의 predicate 함수를 줘서 지울 기준을 정할 수도 있다.
- trimStart, trimEnd 로 앞 뒤만 자르도록할 수도 있다.
- take, takeLast
- 인자로 Int 를 전달해서 그 숫자만큼 취한 부분문자열을 반환한다.
- 음수만 안넣으면 coerceAt~ 으로 알아서 경계처리해준다.
- takeWhile, takeLastWhile
- 인자로 (Char) -> Boolean 의 predicate 함수를 전달해서 조건을 앞에서부터 또는 뒤에서부터 만족할때까지 취한 부분문자열을 반환한다.
- 처음부터 만족하지 않는다면 빈 문자열을 반환한다.
[U]
- uppercase
- lowercase 의 반대이다. 다 대문자로 바꾼 문자열을 반환한다.
[W]
- windowed
- 인자로 Int, Int, Boolean 타입을 받고 각각 size, step, partialWindow 를 의미한다.
- size 는 윈도우 사이즈이고, step 은 윈도우가 매번 움직이는 칸수이다. step 의 디폴트 값은 1 이다.
- partialWindow 가 헷갈리는데 size 를지킬 것인가를 나타낸다. 디폴트는 false 이다.
- partialWindow 가 false 면 딱 size 를 지킬 수 있을 때까지 반복해서 list 의 String 원소의 길이가 모두 같다.
- 예를 들어 "abcd".windowed(3, 1, true) 의 결과는 [abc, bcd, cd, d] 인 반면
- "abcd".windowed(3, 1, false) 의 결과는 [abc, bcd] 이다.
- 앞서 말한 chunked 가 이 함수로 구현되어있다. chunked(size: Int) 라면 windowed(size, size, true) 를 호출한다.
- transform (CharSequence) -> R 을 넣어서 List<R> 로 반환받을 수 있다. (그냥 map 써도 괜찮을듯)
public fun <R> CharSequence.windowed(size: Int, step: Int = 1, partialWindows: Boolean = false, transform: (CharSequence) -> R): List<R> {
checkWindowSizeStep(size, step)
val thisSize = this.length
val resultCapacity = thisSize / step + if (thisSize % step == 0) 0 else 1
val result = ArrayList<R>(resultCapacity)
var index = 0
while (index in 0 until thisSize) {
val end = index + size
val coercedEnd = if (end < 0 || end > thisSize) { if (partialWindows) thisSize else break } else end
result.add(transform(subSequence(index, coercedEnd)))
index += step
}
return result
}
[Z]
- zip
- 인자로 다른 CharSequence 를 넣어주고 그 호출한 문자열과 인자로 들어간 문자열의 일치하는 인덱스로 Pair 를 만들고 그것을 List<Pair<Char, Char> 로 반환해준다.
- 참고로 first 가 호출한 문자열이고 second 가 인자로 들어간 문자열이다.
- CharSequence 와 함께 추가 인자로 (Char, Char) -> V 인자를 넣으면 List<V> 을 반환한다.
- 두 문자열의 길이가 다르면 더 짧은 쪽을 기준으로 반환한다.
- zipWithNext
- 이건 인자전달 안하면 각 Char 를 돌면서 다음 문자와 Pair 객체로 만들어 List<Pair<Char, Char>> 로 내보내준다.
- 인자로 (Char, Char) -> R 이렇게 tranform 함수를 전달하면 List<R> 을 반환해준다.
- 인자전달 없는 버전이 인자전달 있는 버전으로 구현되어있다. zipWithNext { a, b -> a to b }
- 호출한 문자열의 사이즈가 1이하면 빈 List 를 반환한다.
[예시 코드 정리]
fun main() {
val text = "Android developers"
println("\n[A]")
println(text.all { c -> c <= 'z' }) // true
println(text.all { c -> c <= 'b' }) // false
println(text.associate { c -> c to (c.code) })
println("\n[C]")
println(Regex("d d") in text)
println("b".compareTo("c"))
println("c".compareTo("c"))
println("z".compareTo("c"))
println("A".compareTo("c"))
println(text.compareTo("c"))
println(text.codePointAt(0))
println(text.codePointAt(1))
println(text.contentEquals("Android developer"))
println(text.contentEquals("Android developers"))
println(text.chunked(2))
println(text.chunked(5))
println(text.chunked(2) { c -> "${c}_"})
println(text.commonPrefixWith("android experts", true))
println(text.commonPrefixWith("android experts", false))
println(text.count())
println(text.lowercase().count { c -> c > 'b'})
println("\n[D]")
println(text.drop(1))
println(text.drop(3))
println(text.dropLast(1))
println(text.dropLast(3))
println(text.dropLastWhile { c -> c > 'a' })
println(text.dropLastWhile { c -> c == 's' })
println("\n[E]")
println(text.elementAtOrNull(1))
println(text.elementAtOrNull(100))
println(text.elementAtOrElse(1) { n -> 'd'})
println(text.elementAtOrElse(100) { n -> 'd'})
println(text.endsWith("devleo"))
println(text.endsWith("pers"))
println("\n[F]")
println(text.filter { c -> c > 'a'})
println(text.filterNot { c -> c > 'a' })
println(text.filterIndexed { idx, c -> idx < 7 && c > 'a'})
println(text.first()) // A
println(text.first { c -> c > 'a'}) // n
println(text.firstOrNull { c -> c > 'a'}) // n
println(text.find { c -> c > 'a' }) // n
println(text.findLast { c -> c > 'a' }) // s
println("입력된 숫자는 %d 입니다.".format(7))
println(text.findAnyOf(listOf("and", "And", "roid", "developers"))) // (0, And)
println(text.findAnyOf(listOf("and", "roid", "developers"))) // (3, roid)
println(text.findAnyOf(listOf("and", "developers"))) // (8, developers)
text.forEach { c -> print(c) } // Android developers
println()
println(text.flatMap { c -> listOf(c, c) }) // [A, A, n, n, d, d, r, r, o, o, i, i, d, d, , , d, d, e, e, v, v, e, e, l, l, o, o, p, p, e, e, r, r, s, s]
println(text.flatMapIndexed { index, c -> listOf(c, c) })
println(text.fold("") { r, c -> "$r${c}_" }) //A_n_d_r_o_i_d_ _d_e_v_e_l_o_p_e_r_s_
println(text.foldRight("") {c, r -> "$r${c}_"}) // s_r_e_p_o_l_e_v_e_d_ _d_i_o_r_d_n_A_
println(text.foldRight("") {c, r -> "$r$c"}) // srepoleved diordnA
println("\n[G]")
println(text[1]) // n
println(text.getOrNull(1)) // n
println(text.getOrNull(100)) // null
println(text.getOrElse(1) { n -> 'd'}) // n
println(text.getOrElse(100) { n -> 'd'}) // d
println(text.groupBy { c -> c })
println(text.groupBy { c -> "c" })
println(
text.groupBy(
keySelector = {c -> c},
valueTransform = {c -> "${c}변형"}
)
)
println("\n[I]")
println(text.indices)
println(text.iterator().next()) // A
println(" ".isBlank()) // true
println(" ".isEmpty()) // false
println(" ".isNotBlank()) // false
println(" ".isNotEmpty()) // true
println(" ".isNullOrBlank()) // true
println(" ".isNullOrEmpty()) // false
println(text.indexOf('d', 5)) // 6
println(text.indexOf("ev", 5)) // 9
println(text.indexOf("ev", 10)) // -1
println(text.indexOfFirst { c -> c > 'a'}) // 1
println(text.indexOfLast { c -> c > 'a' }) // 17
println(text.indexOfAny(listOf("and", "And", "roid", "developers"))) // (0, And)
println(text.indexOfAny(listOf("and", "roid", "developers"))) // (3, roid)
println(text.indexOfAny(listOf("and", "developers"))) // (8, developers)
println("\n[L]")
println(text.lowercase()) // android developers
println(text.last()) // s
println(text.last { c -> c < 's'}) // r
println(text.lastOrNull()) // s
println(text.length) // 18
println(text.lastIndexOf('p')) // 14
println(text.lastIndexOf("pe")) // 14
println(text.lastIndexOf("an", ignoreCase = true)) // 0
println(text.lastIndexOf("an", 3, ignoreCase = true)) // 0
println(text.lastIndexOfAny(listOf("and", "And", "roid", "developers"))) // 8
println(text.lastIndexOfAny(listOf("developers", "and", "And"))) // 8
println(text.lastIndexOfAny(listOf("and", "developers"))) // 8
println(text.lastIndex) // 17
println(text.lineSequence())
println(text.lines())
println("\n[M]")
println(text.map { c -> c.code })
println(text.map { c -> null })
println(text.mapIndexed { i, c -> "${c.code} $i"})
println(text.matches(Regex("o")))
println(text.mapIndexedNotNull { index, c -> null})
println(text.maxOf { c -> c.code }) // 비었으면 예외 // 118
println(text.maxOfOrNull {c -> c.code }) // 비면 null 반환 //118
println(text.minOf { c -> c.code }) // 32
println(text.minOfOrNull { c -> c.code }) // 32
println(text.minOfWith(Comparator<Int> { o1, o2 -> if (o1 == o2) 0 else if (o1 < o2) 1 else -1 }) { c -> c.code }) // 내림차순일 때 min // 118
println(text.minOfWithOrNull(Comparator<Int> { o1, o2 -> if (o1 == o2) 0 else if (o1 < o2) 1 else -1 }) { c -> c.code }) // 내림차순일 때 min // 118
println(text.maxOfWith(Comparator<Int> { o1, o2 -> if (o1 == o2) 0 else if (o1 < o2) 1 else -1 }) { c -> c.code }) // 내림차순일 때 max // 32
println(text.maxOfWithOrNull(Comparator<Int> { o1, o2 -> if (o1 == o2) 0 else if (o1 < o2) 1 else -1 }) { c -> c.code }) // 내림차순일 때 max // 32
println("\n[O]")
text.orEmpty()
println("\n[P]")
println(text.plus(1))
println(text + 1)
println(text.padStart(20, '['))
println(text.padEnd(20, ']').padStart(22, '['))
println(text.partition { c -> c > 'l' })
println(text.prependIndent())
println("\n[R]")
println(text.random())
println("".randomOrNull()) // null
println(text.reversed()) // srepoleveD diordnA
println(text.repeat(3)) // Android developersAndroid developersAndroid developers
println(text.replace('d', '-')) // An-roi- -evelopers
println(text.repeat(3).replace("dev", "-")) // Android -elopersAndroid -elopersAndroid -elopers
println("-".repeat(3).replace("dev", "-")) // Android -elopersAndroid -elopersAndroid -elopers
println(text.replace(Regex("d."), "-")) // An-oi--velopers
println(text.replaceFirst('d', '-'))
println(text.replaceAfter('d', "|replacement|"))
println(text.replaceAfterLast('d', "|replacement|"))
println(text.replaceBefore('d', "|replacement|"))
println(text.replaceBeforeLast('d', "|replacement|"))
println(text.replaceRange(0, 3, "-"))
println(text.replaceRange(0..3, "-")) // -oid developers
println(text.removePrefix("An")) // droid developers
println(text.removePrefix("-")) // Android developers
println(text.removeSuffix("rs")) // Android develope
println(text.removeSuffix("-")) // Android developers
println(text.removeRange(0, 3)) // roid developers
println(text.removeRange(0..3)) // oid developers
println(text.removeSurrounding("An", "An")) // Android developers
println(text.removeSurrounding("An", "rs")) // droid develope
println(text.runningFold(0) {a, c -> a + c.code})
println("\n[S]")
println(text.substring(0..3)) // Andr
println(text.substring(0, 3)) // And
println(text.substring(3)) // roid developers
println(text.substringAfter(delimiter = 'd', "default")) // roid developers
println(text.substringAfter(delimiter = '-', "default")) // default
println(text.substringAfterLast(delimiter = 'd', "default")) // evelopers
println(text.substringBefore(delimiter = 'd', "default")) // An
println(text.substringBeforeLast(delimiter = 'd', "default")) // Android (' ' space 있음 주의)
println(text.slice(0..3))
println(text.slice(listOf(0, 5, 17)))
println(text.startsWith("a", 1, true)) // false
println(text.startsWith("a", 0, true)) // true
println(text.startsWith("a")) // false
println(text.startsWith("A")) // true
println(text.split("d")) // [An, roi, , evelopers]
println(text.split("d", limit = 2)) // [An, roi, , evelopers]
println(text.split("A")) // [, ndroid developers]
println(text.split("A")) // [, ndroid developers]
println(text.split('d')) // [An, roi, , evelopers]
println(text.split(Regex("d."))) // [An, oi, , velopers]
println("dd".split("d")) // [, , ]
println(text.scan(0) { a, c -> a + c.code })
println("\n[T]")
println("1A".toInt(16))
println("13".toInt())
println(text.toIntOrNull())
println(text.trim())
println(text.trimEnd())
println(text.trimStart())
println(text.trim { c -> c > 'd'})
println(text.trimEnd { c -> c > 'd'})
println(text.trimStart { c -> c > 'd'})
println(text.take(5)) // Andro
println(text.takeLast(5)) // opers
println(text.takeWhile { c -> c > 'd' }) //
println(text.takeWhile { c -> c < 'e' }) // A
println(text.takeLastWhile { c -> c > 'd' }) // evelopers
println("\n[U]")
println(text.uppercase()) // ANDROID DEVELOPERS
println("\n[W]")
println("abcd".windowed(3, 1, true)) // [abc, bcd, cd, d]
println("abcd".windowed(3, 1, false)) // [abc, bcd]
println(text.windowed(5, 5, true)) // [Andro, id de, velop, ers]
println(text.windowed(5, 1, false)) // [Andro, ndroi, droid, roid , oid d, id de, d dev, deve, devel, evelo, velop, elope, loper, opers]
println(text.windowed(5, 1, false) { c -> "${c}_"})
println(text.windowed(5, 1, false).map { c -> "${c}_"})
println("\n[Z]")
println(text.zip("other string"))
println(text.zip("other string") { c1, c2 -> c1.code + c2.code })
println(text.zipWithNext())
println(text.zipWithNext { a, b -> a.code+b.code })
}
이제 프로젝트나 알고리즘 문제를 풀어보면서 더 활용해보고 내용을 또 보충해가야겠다.
이 글은 계속 수정될 것이다.