Algorithm/이론과 도구

[Kotlin] String 핸들링을 위한, 코틀린의 String 클래스를 뜯어보자

노소래 2022. 2. 24. 09:25

실제 서비스를 위해 코딩하다보면 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  })

}

 

 

이제 프로젝트나 알고리즘 문제를 풀어보면서 더 활용해보고 내용을 또 보충해가야겠다. 

이 글은 계속 수정될 것이다.

'Algorithm > 이론과 도구' 카테고리의 다른 글

정규표현식 정리  (0) 2021.06.26