예전부터 뭔가를 정렬할 때, 가장 많이 썼던 함수 중 하나가 compareTo()였다. 하지만 이 함수를 쓰면서도 정확하게 어떤 식으로 반환되는지 Int형일 경우와 String형일 경우 차이가 무엇인지 등 정확히 알지 못하고 그냥 느낌대로 다른 사람들이 쓰는 것을 그대로 사용하다보니 수동적인 형태로 사용했었다. 그래서 이번 기회에 compareTo() 함수를 이용하여 여러 상황에서 테스트해보며 개념 정리를 해보려 한다.
compareTo는 Comparable 인터페이스 안에 오버 로딩으로 정의되어 있고 아래와 같은 설명도 적혀있다.
Comparable.kt
"이 개체를 지정한 개체와 순서를 비교합니다. 이 개체가 지정된 다른 개체와 같으면 0을, 다른 개체보다 작으면 음수를, 다른 개체보다 크면 양수를 반환합니다."
이제 이런 Comparable 인터페이스를 String, Long(어째서 인지는 모르겠다.) 클래스에서 상속받아 재정의 하는 방식으로 구현되어 있는데, 코드는 아래와 같다.
String.kt
Primitives.kt 안의 Long 클래스
위에서 보이는 것처럼 compareTo는 Int, String 타입을 제외하고도 굉장히 많은 타입을 비교할 수 있다는 것을 확인할 수 있다. 하지만 여기선 Int와 String만 테스트해볼 예정이다.
compareTo를 이용해 String을 비교하면서 알게된 것은 아래와 같다.
두 문자열에서 각 각 하나하나 씩 Character형으로 변환해 ASCII 코드로 비교하는 방식이며 모든 문자의 차이가 0이 나오는 경우(문자열의 차이가 없는 경우) 0을 반환하고 하나라도 차이가 있다면 그 자리에서 break를 걸어 그 차이만큼을 반환해준다. 선행 문자의 차이는 없는데, 문자열 길이의 차이가 있는 경우 길이의 차이만큼 반환해준다.
0 ~ 9의 경우 십진법 기준으로 48부터 57까지 위치하고 있다. 아래 코드를 살펴보면
val a = "123"
val b = "193"
val c = "125"
val a_a = a.compareTo(a)
val a_b = a.compareTo(b)
val b_a = b.compareTo(a)
val a_c = a.compareTo(c)
println("\"${a}\".compareTo(\"${b}\"): $a_b")
println("\"${b}\".compareTo(\"${a}\"): $b_a")
println("\"${a}\".compareTo(\"${a}\"): $a_a")
println("\"${a}\".compareTo(\"${c}\"): $a_c")
앞서 말한 것처럼 맨 앞 문자끼리 하나 하나씩 아스키코드를 이용해 비교하는 방식으로 진행되는데, 편의상 위치는 0부터 9까지로 조정해서 설명하도록 하겠다.
"123"과 "193"을 비교해보면 첫 번째 문자는 서로 같은 위치에 존재하는 '1'이므로 아직까진 0을 반환하는 상태이다. 그다음 문자 '2'와 '9'를 비교해보면 7이 차이가 나는데 2가 기준값이 되므로 -7이 반환되며 break 시키고 이 값을 반환한다.
그 반대의 경우 당연히 7이 반환된다.
"123"과 "123"은 모든 문자가 같기 때문에 0이 반환된다.
"123"과 "125"의 경우는 앞에서 부터 하나씩 문자 별로 비교하면서 가다가 마지막 문자인 '3'과 '5' 차이 -2를 반환한다.
그냥 간단하게 생각하면 123 - 193 = -70인데 '맨 앞 자리 숫자'와 '부호'를 붙여서 -7을 반환한다고 생각하면 편하다.
이번에는 영문끼리 비교하는 코드를 살펴보자.
val alphaA = "a"
val alphaP = "p"
val alphaUpperA = "A"
val alpahUpperZ = "Z"
val alphaA_alphaP = alphaA.compareTo(alphaP)
val alphaP_alphaA = alphaP.compareTo(alphaA)
val alphaA_alphaUpperA = alphaA.compareTo(alphaUpperA)
val alpahUpperZ_alphaP = alpahUpperZ.compareTo(alphaP)
println("\"${alphaA}\".compareTo(\"${alphaP}\"): $alphaA_alphaP")
println("\"${alphaP}\".compareTo(\"${alphaA}\"): $alphaP_alphaA")
println("\"${alphaA}\".compareTo(\"${alphaUpperA}\"): $alphaA_alphaUpperA")
println("\"${alpahUpperZ}\".compareTo(\"${alphaP}\"): $alpahUpperZ_alphaP")
알파벳 역시 편의상 1부터 26까지로 위치를 변환하여 설명할 예정이고 예외 사항에 따라 26에서 더 추가하여 설명할 예정이다.
위에서부터 하나씩 살펴보면
"a"와 "p"의 각 각의 위치는 "a"는 1이라 할 수 있고 "p"는 16이라 할 수 있다. 당연히 1 - 16 = -15 이기에 -15가 반환이 된다.
그 반대는 15를 반환한다.
"a"와 "A"는 소문자와 대문자로 앞서 말한 것처럼 대문자가 먼저 나와서 대문자 알파벳 사이클을 끝내고 다음 소문자 알파벳이 시작되므로 대략 위치로 생각하면 "A"는 1이고 "a"는 26(알파벳 개수) + 6(특수문자 '[', '\', ']', '^', '_', `) + 1 해서 26 + 6 + 1 = 33이 되며 33 - 1 = 32를 반환한다.
"Z"와 "p"는 각 각 26, 48(26 + 6 + 16)에 위치한다. 26 - 48 = -22 그래서 -22를 반환한다.
결과 값은 같지만 정확한 위치로 계산하고 싶다면 아스키코드를 보고 계산하면 된다.
이번에는 공백, null 값 등 궁금했던 것들에 대한 테스트를 해보려 한다.
null의 경우 아스키 코드 상으로는 0에 위치한다고 한다. 하지만 Kotlin의 null safe 방식 때문에 테스트해볼 수는 없었다. 아마 테스트해보려면 JAVA나 C로 해봐야 할 거 같다.
다음 공백의 경우 32번에 위치한다고 하는데, 결과 값이 도출되는 방식은 이전과는 달랐다.
val blank = ""
val alphaB = "b"
val d = "3"
val e = "142"
val str = "baserdpp"
val alphaB_blank = alphaB.compareTo(blank)
val blank_d = blank.compareTo(d)
val e_blank = e.compareTo(blank)
val blank_str = blank.compareTo(str)
println("\"${alphaB}\".compareTo(\"${blank}\"): $alphaB_blank")
println("\"${blank}\".compareTo(\"${d}\"): $blank_d")
println("\"${e}\".compareTo(\"${blank}\"): $e_blank")
println("\"${blank}\".compareTo(\"${str}\"): $blank_str")
아스키코드상으로 보면 b의 위치는 98이고 공백의 위치는 32로 98 - 32 = 66이 나와야 하는데, 결과 값을 보면 1이 반환된다. 아래의 경우 3 역시 -1이 반환되어 이게 뭔가 고민해보다 문자열의 개수를 다르게 하여 테스트해보다 알게 된 게 공백과 문자열을 비교하면 그 문자열 길이의 차이만큼 이 반환된다는 것이다.
그래서 "142" 와 "blank"의 길이의 차이는 3이고 "blank"와 "baserdpp"의 길이 차이는 8로 0 - 8 이므로 -8을 반환한 것이다. 이제 이쯤에서 한번 테스트해봐야 할 것이 앞의 문자열은 같은데 길이가 다른 경우와 문자열도 다르고 길이도 다른 경우를 테스트해서 어떤 반환 값이 나오는지 살펴보려 한다.
val sameString1 = "090"
val sameString2 = "090090"
val anotherString = "009009"
val sameString1_sameString2 = sameString1.compareTo(sameString2)
val sameString1_anotherString = sameString1.compareTo(anotherString)
val anotherString_sameString2 = anotherString.compareTo(sameString2)
println("\"${sameString1}\".compareTo(\"${sameString2}\"): $sameString1_sameString2")
println("\"${sameString1}\".compareTo(\"${anotherString}\"): $sameString1_anotherString")
println("\"${anotherString}\".compareTo(\"${sameString2}\"): $anotherString_sameString2")
여기서 확인할 수 있는건
"090"의 경우 "090090" 앞 문자열은 같지만 길이가 달라지기 때문에 길이의 차이 값인 -3을 반환하는 것이고
"090"과 "009009"의 경우는 문자열의 차이 때문에 문자열의 길이가 달라도 9와 0의 위치 차이 값인 9를 반환하며
"009009"와 "090090" 역시 마찬가지로 문자열의 차이 값을 반환해준다.
String 타입 간 compareTo함수 정리는 이 정도면 될 거 같고 생각보다 너무 길어져서 포스팅을 이어서 쓰던지 아니면 2개 글로 나눠서 쓰던지 해야 할 거 같다.
'개발' 카테고리의 다른 글
compareTo() 정리(Int) - Kotlin (2) (0) | 2021.03.12 |
---|---|
String 관련 함수 정리 (0) | 2021.03.05 |
개발하면서 자주 썼던 함수 정리 (maxBy, minBy, groupBy, map, reduce, indices, compareTo) (0) | 2021.03.05 |