-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] graphemeLength String Extension with LocalBreakIterator #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
chattymin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다. 한번만 더 확인 부탁드릴게요 :)
| fun String.countGraphemes(trim: Boolean = false): Int { | ||
| val target = if (trim) this.trim() else this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trim을 제외한 글자 수를 세는것 까지가 이 함수의 역할일까요?
글자를 Grapheme 단위로 세는것까지가 이 함수의 역할이라고 생각이 됩니다.
그렇기에 trim여부를 외부에서 받아오는 것이 아닌, 외부에서 trim을 하고 해당 함수를 사용하는 것이 이상적이라 생각됩니다.
trim과 관련된 부분은 삭제하면 좋을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
책임이 과한 느낌이 있으니 이 부분은 제거하겠습니다
| } | ||
|
|
||
| // 그래핌 단위로 카운팅된 총 글자수 리턴 | ||
| fun String.countGraphemes(trim: Boolean = false): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
count 라는 네이밍은 String 내부에서 특정 Char의 갯수를 셀 때 주로 사용하는 네이밍입니다.
현재 함수의 목적은 길이를 구하는 것이기에 length라는 네이밍이 더 어울릴 것 같네요
| fun String.countGraphemes(trim: Boolean = false): Int { | |
| fun String.graphemeLength(): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
말씀하신 것처럼.length 처럼 익숙한 네이밍으로 가는게 좋아 보여요. 반영하겠습니다
| // ThreadLocal.get()은 값이 설정되지 않았을 경우 null을 반환할 수 있어서 nullable 타입으로 추론하는듯. | ||
| // 그래서 iterator가 null safety하지 않다고 경고하는디... | ||
| // val iterator = localBreakIterator.get() ?: | ||
| // BreakIterator.getCharacterInstance(Locale.ROOT).also { | ||
| // localBreakIterator.set(it) | ||
| // } | ||
| // 이렇게 null 처리를 하는건 어떨지??? | ||
| val iterator = localBreakIterator.get() | ||
| iterator.setText(target) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nullable한 상태로 사용하는 것은 문제가 있는 것 같습니다.
제안해주신 방향성의 경우 추후 localBreakIterator를 사용할 때 마다 적어줘야 하기에 불편함이 클 것 같습니다.
제가 제안하는 방법은 get 했을 때 nullable하다면 자체적으로 set 하는 변수를 만들어 백킹프로퍼티를 활용하는 방향입니다. 최상단의 선언 부분만 수정해준다면, 아래에서 활용할 때에 기존과 비슷하고 get을 매번 써주지 않아도 되는 방향으로 사용할 수 있을 것 같네요
보신 후 의견 제안해주시면 감사하겠습니다.
private val LocalBreakIterator = ThreadLocal<BreakIterator>().apply {
set(BreakIterator.getCharacterInstance(Locale.ROOT))
}
private val localBreakIterator: BreakIterator = LocalBreakIterator.get() ?: initBreakIterator()
private fun initBreakIterator() = BreakIterator.getCharacterInstance(Locale.ROOT).also {
LocalBreakIterator.set(it)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
자제척으로 set을 하는 방향은 생각을 못했었네요!
제안하신 방법으로 사용한다면 val iterator = localBreakIterator 처럼 사용이 간단해서 좋을 것 같습니다. 추가적으로 찾아본 방법은 아래와 같은데 이걸 사용하는건 어떻게 생각하시나요? 현업의 시선이 궁금합니다ㅎㅎ
private val localBreakIterator = ThreadLocal.withInitial {
BreakIterator.getCharacterInstance(Locale.ROOT)
}withInitial을 사용하면 ThreadLocal에 접근할 때마다 초기값이 자동제공 됩니다. 때문에 별도 초기화 로직이 필요가 없어지지만 아래의 걸림돌이 생기네요.
- API 26이상에서만 지원함
- Java의 ThreadLocal.get() 메서드에는 nullability 어노테이션이 없어서 Kotlin에서는 반환 타입을 nullable로 추론
2.의 여파로localBreakIterator가 실제 실행에서는 항상 non-null임을 보장받지만 IDE상에서 경고로 표시됨
제약이 좀 있는편이라 좋은 방법은 아닌것 같지만 선언이 간단하다? 의 이점이 있을것 같네요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지원하는 api가 너무 높습니다. 현재 프로젝트의 경우 api 21부터 지원하고 있기에 해당 방법을 사용하기엔 무리가 있다고 생각됩니다.
현재는 프로젝트의 규모가 작고, 해당 기능의 컨텍스트를 파악하고 있지만 추후 규모가 커지고 외부에서 작업을 한다면 non-null이 보장받는 다는것을 파악하는데 리소스가 필요할 것 같습니다.
더 간단한 로직도 좋지만, 리스크를 줄이는 방향이 더 좋을 것 같습니다 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의견 감사합니다ㅎ 해당 부분은 제안해주신 방안으로 수정했습니다 👍
| } | ||
|
|
||
| // 아스키 문자열일 경우 얼리리턴 용도 | ||
| private fun String.isLikelyAscii(): Boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isAscii만 해도 의미 전달은 충분할 것 같습니다.
| private fun String.isLikelyAscii(): Boolean { | |
| private fun String.isAscii(): Boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영하겠습니다 :)
| var i = 0 | ||
| val len = length | ||
| while (i < len) { | ||
| if (this[i].code > 0x7F) return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0x7F 와 같은 문자는 상수로 빼서 관리하면 좋을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i 라는 변수를 사용하기 보다는 String 전체를 순회하는 방법을 사용하면 불필요한 변수 할당을 줄일 수 있을 것 같네요
| if (this[i].code > 0x7F) return false | |
| this.forEach { | |
| if (it.code > 0x7F) return false | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 상수로 관리하겠습니다!
forEach같은 경우 내부적으로 람다를 사용하는걸로 알고있습니다. 물론 인라인 함수로 구현되어 람다 오버헤드가 적다는것도 알고있지만, 깡성능에서while또는for루프에 비해 불필요한 람다 호출 가능성이 높다고 생각했어요. 물론 엄~청 미세한 오버헤드 차이라고 생각합니다.forEach사용도 가독성 측면에선 더 좋다고 생각해요. 어떤 방식을 더 선호하시나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 성능에 큰 차이가 있지 않다면 가독성을 더 챙기는 것을 선호합니다 :)
말씀해주신 부분을 더욱 신경쓰고 싶다면 이런 방법은 어떤가요? @angryPodo
for (input in this) {
if (input.code > 0x7F) return false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for문으로 좀더 간결성을 챙기는게 좋아보여 반영하겠습니다!
| } | ||
|
|
||
| // 그래핌 단위의 문자열 뒤집기(이모지 포함) | ||
| fun String.reverseWithGraphemes(): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| fun String.reverseWithGraphemes(): String { | |
| fun String.graphemeReversed(): String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 👍 👍 👍
|
|
||
| // 그래핌 단위의 문자열 뒤집기(이모지 포함) | ||
| fun String.reverseWithGraphemes(): String { | ||
| if (this.isEmpty()) return "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
공백만 있는 경우에도 연산을 수행하기 보다 바로 리턴하는게 더 좋을 것 같습니다.
그리고 임의의 값을 리턴하기 보다는 자기 자신을 리턴하는게 더 안전할 것 같네요
| if (this.isEmpty()) return "" | |
| if (this.isBlank()) return this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영하겠습니다ㅎㅎ
| val graphemes = mutableListOf<String>() | ||
|
|
||
| var start = 0 | ||
| var end = iterator.next() | ||
|
|
||
| while (end != BreakIterator.DONE) { | ||
| graphemes.add(this.substring(start, end)) | ||
| start = end | ||
| end = iterator.next() | ||
| } | ||
|
|
||
| val result = StringBuilder(length) | ||
| for (i in graphemes.size - 1 downTo 0) { | ||
| result.append(graphemes[i]) | ||
| } | ||
|
|
||
| return result.toString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
조금 더 최적화 할 방법이 있을 것 같네요.
반복문 하나로 해결할 수 있을 것 같습니다. 이건 한번 고민해보시면 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요게 처음 버전은 아래와 같았는데요
fun String.reverseWithGraphemes(): String {
if (this.isEmpty()) return ""
if (this.length == 1) return this
if (this.isLikelyAscii()) return this.reversed()
val iterator = localBreakIterator.get()
iterator.setText(this)
val graphemes = mutableListOf<String>()
var start = 0
var end = iterator.next()
while (end != BreakIterator.DONE) {
graphemes.add(this.substring(start, end))
start = end
end = iterator.next()
}
return graphemes.reversed().joinToString("")
}.joinToString("")의 내부로직을 보니까 결합하는 과정에서 중간 컬렉션이 생성되어서 메모리 최적화를 해볼까 싶었어요. 그래서 StringBuilder를 사용해 객체 생성을 줄여봤습니다.
이전 버전이 나을까요? 가독성은 더 좋은 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트해보니 String.reversed 사용해도 잘 뒤집히네요. 해당 함수 삭제 해주시면 감사하겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 함수는 제거했습니다 😢
|
그리고 함수 위에있는 주석은 지우거나, 영어로 작성하는 것으로 변경 부탁드립니다. |
- Rename `countGraphemes` to `graphemeLength`. - Rename `isLikelyAscii` to `isAscii`. - Use `ASCII_MAX` constant for ASCII range check. - Remove trim option in `graphemeLength`. - Improve efficiency by using `isAscii` for early return. - Use `initBreakIterator` for initializing `localBreakIterator`. - Improve comments.
chattymin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
New
최대한 최적화 해봤어요 🙇🏻♂️