Browse Source

Parse without using String.split

pull/4519/head
Chanjung Kim 8 months ago
parent
commit
6570cd3e08
  1. 262
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt
  2. 8
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt
  3. 46
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt
  4. 320
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt
  5. 9
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt

262
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt

@ -1,262 +0,0 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
import kotlin.math.absoluteValue
internal sealed class PluralCondition {
abstract fun isFulfilled(n: Int): Boolean
// add isFulfilled(n: Double) or isFulfilled(n: Decimal) as needed
class And(
private val left: PluralCondition,
private val right: PluralCondition,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) and right.isFulfilled(n)
override fun toString(): String = "$left and $right"
}
class Or(
private val left: PluralCondition,
private val right: PluralCondition,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) or right.isFulfilled(n)
override fun toString(): String = "$left or $right"
}
class Relation(
private val operand: PluralOperand,
private val operandDivisor: Int?,
private val comparisonIsNegated: Boolean,
private val ranges: Array<IntRange>,
) : PluralCondition() {
override fun isFulfilled(n: Int): Boolean {
val expressionOperandValue = when (operand) {
PluralOperand.N, PluralOperand.I -> n.absoluteValue
else -> 0
}
val moduloAppliedValue = if (operandDivisor != null) {
expressionOperandValue % operandDivisor
} else {
expressionOperandValue
}
return ranges.any { moduloAppliedValue in it } != comparisonIsNegated
}
override fun toString(): String {
return StringBuilder().run {
append(operand.name.lowercase())
if (operandDivisor != null) {
append(" % ")
append(operand)
}
append(' ')
if (comparisonIsNegated) {
append('!')
}
append("= ")
var first = true
for (range in ranges) {
if (!first) {
append(',')
}
first = false
append(range.first)
if (range.first != range.last) {
append("..")
append(range.last)
}
}
toString()
}
}
private class Parser(private val description: String) {
private var currentIdx = 0
private fun consumeWhitespaces() {
while (currentIdx < description.length && description[currentIdx].isWhitespace()) {
currentIdx += 1
}
}
private fun peekNextOrNull(): Char? {
return description.getOrNull(currentIdx)
}
private fun peekNext(): Char {
return peekNextOrNull() ?: throw PluralConditionParseException(description)
}
private fun consumeNext(): Char {
val next = peekNext()
currentIdx += 1
return next
}
private fun consumeNextInt(): Int {
if (!peekNext().isDigit()) throw PluralConditionParseException(description)
var integerValue = 0
var integerLastIdx = currentIdx
while (integerLastIdx < description.length && description[integerLastIdx].isDigit()) {
integerValue *= 10
integerValue += description[integerLastIdx] - '0'
integerLastIdx += 1
}
currentIdx = integerLastIdx
return integerValue
}
fun nextOperand(): PluralOperand {
consumeWhitespaces()
return when (consumeNext()) {
'n' -> PluralOperand.N
'i' -> PluralOperand.I
'f' -> PluralOperand.F
't' -> PluralOperand.T
'v' -> PluralOperand.V
'w' -> PluralOperand.W
'c', 'e' -> PluralOperand.C
else -> throw PluralConditionParseException(description)
}
}
fun nextModulusDivisor(): Int? {
consumeWhitespaces()
if (peekNext() == '%') {
consumeNext()
consumeWhitespaces()
return consumeNextInt()
}
return null
}
/**
* Returns `true` for `!=`, `false` for `=`.
*/
fun nextComparisonIsNegated(): Boolean {
consumeWhitespaces()
when (peekNext()) {
'!' -> {
consumeNext()
if (consumeNext() != '=') throw PluralConditionParseException(description)
return true
}
'=' -> {
consumeNext()
return false
}
else -> throw PluralConditionParseException(description)
}
}
/**
* Returns `number..number` if the range is actually a value.
*/
fun nextRange(): IntRange {
consumeWhitespaces()
val start = consumeNextInt()
if (peekNextOrNull() != '.') {
return start..start
}
consumeNext()
if (consumeNext() != '.') throw PluralConditionParseException(description)
val endInclusive = consumeNextInt()
return start..endInclusive
}
fun nextCommaOrNull(): Char? {
return when (peekNextOrNull()) {
',' -> {
consumeNext()
','
}
null -> null
else -> throw PluralConditionParseException(description)
}
}
}
companion object {
/**
* Syntax:
* ```
* relation = operand ('%' value)? ('=' | '!=') range_list
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* range_list = (range | value) (',' range_list)*
* range = value'..'value
* value = digit+
* digit = [0-9]
* ```
*/
fun parse(description: String): Relation {
val parser = Parser(description)
val operand = parser.nextOperand()
val divisor = parser.nextModulusDivisor()
val negated = parser.nextComparisonIsNegated()
val ranges = mutableListOf<IntRange>()
while (true) {
ranges.add(parser.nextRange())
if (parser.nextCommaOrNull() == null) break
}
// ranges is not empty here
return Relation(operand, divisor, negated, ranges.toTypedArray())
}
}
}
companion object {
/**
* Parses [description] as defined in the [Unicode Plural rules syntax](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).
* For compact implementation, samples and keywords for backward compatibility are also not handled. You can
* find such keywords in the [Relations Examples](https://unicode.org/reports/tr35/tr35-numbers.html#Relations_Examples) section.
* ```
* condition = and_condition ('or' and_condition)*
* and_condition = relation ('and' relation)*
* relation = operand ('%' value)? ('=' | '!=') range_list
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* range_list = (range | value) (',' range_list)*
* range = value'..'value
* value = digit+
* digit = [0-9]
* ```
* See [Relation.parse] for the remaining part of the syntax.
*/
fun parse(description: String): PluralCondition {
var condition: PluralCondition? = null
val orConditionDescriptions = OR_PATTERN.split(description.trim())
if (orConditionDescriptions.isNotEmpty() && orConditionDescriptions[0].isNotEmpty()) {
for (orConditionDescription in orConditionDescriptions) {
if (orConditionDescription.isEmpty()) throw PluralConditionParseException(description)
var andCondition: PluralCondition? = null
val andConditionDescriptions = AND_PATTERN.split(orConditionDescription.trim())
for (relationDescription in andConditionDescriptions) {
val relation = Relation.parse(relationDescription)
andCondition = if (andCondition == null)
relation
else
And(andCondition, relation)
}
if (andCondition == null) throw PluralConditionParseException(description)
condition = if (condition == null) andCondition else Or(condition, andCondition)
}
}
return condition ?: NoCondition
}
private val AND_PATTERN = Regex("""\s*and\s*""")
private val OR_PATTERN = Regex("""\s*or\s*""")
}
}
internal object NoCondition : PluralCondition() {
override fun isFulfilled(n: Int): Boolean = true
override fun toString(): String = ""
}

8
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt

@ -1,8 +0,0 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
internal class PluralConditionParseException(description: String) : Exception("Invalid LDML: $description")

46
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt

@ -1,46 +0,0 @@
/*
* Copyright 2020-2024 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.resources.intl
/**
* Plural operands defined in the [Unicode Locale Data Markup Language](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_Operand_Meanings).
*/
internal enum class PluralOperand {
/**
* The absolute value of the source number.
*/
N,
/**
* The integer digits of the source number.
*/
I,
/**
* The number of visible fraction digits in the source number, *with* trailing zeros.
*/
V,
/**
* The number of visible fraction digits in the source number, *without* trailing zeros.
*/
W,
/**
* The visible fraction digits in the source number, *with* trailing zeros, expressed as an integer.
*/
F,
/**
* The visible fraction digits in the source number, *without* trailing zeros, expressed as an integer.
*/
T,
/**
* Compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
*/
C,
}

320
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt

@ -5,10 +5,326 @@
package org.jetbrains.compose.resources.intl
internal class PluralRule(val category: PluralCategory, private val constraint: PluralCondition) {
import kotlin.math.absoluteValue
internal class PluralRuleParseException(description: String, position: Int) :
Exception("Invalid syntax at position $position: $description")
internal class PluralRule private constructor(val category: PluralCategory, private val condition: Condition) {
constructor(category: PluralCategory, condition: String) : this(category, Condition.parse(condition))
fun appliesTo(n: Int): Boolean {
return constraint.isFulfilled(n)
return condition.isFulfilled(n)
}
// add appliesTo(n: Double) or appliesTo(n: Decimal) as needed
private sealed class Condition {
abstract fun isFulfilled(n: Int): Boolean
// add isFulfilled(n: Double) or isFulfilled(n: Decimal) as needed
/**
* Plural operands defined in the [Unicode Locale Data Markup Language](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_Operand_Meanings).
*/
enum class Operand {
/**
* The absolute value of the source number.
*/
N,
/**
* The integer digits of the source number.
*/
I,
/**
* The number of visible fraction digits in the source number, *with* trailing zeros.
*/
V,
/**
* The number of visible fraction digits in the source number, *without* trailing zeros.
*/
W,
/**
* The visible fraction digits in the source number, *with* trailing zeros, expressed as an integer.
*/
F,
/**
* The visible fraction digits in the source number, *without* trailing zeros, expressed as an integer.
*/
T,
/**
* Compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting.
*/
C,
}
class And(
private val left: Condition,
private val right: Condition,
) : Condition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) and right.isFulfilled(n)
override fun toString(): String = "$left and $right"
}
class Or(
private val left: Condition,
private val right: Condition,
) : Condition() {
override fun isFulfilled(n: Int): Boolean = left.isFulfilled(n) or right.isFulfilled(n)
override fun toString(): String = "$left or $right"
}
class Relation(
private val operand: Operand,
private val operandDivisor: Int?,
private val comparisonIsNegated: Boolean,
private val ranges: Array<IntRange>,
) : Condition() {
override fun isFulfilled(n: Int): Boolean {
val expressionOperandValue = when (operand) {
Operand.N, Operand.I -> n.absoluteValue
else -> 0
}
val moduloAppliedValue = if (operandDivisor != null) {
expressionOperandValue % operandDivisor
} else {
expressionOperandValue
}
return ranges.any { moduloAppliedValue in it } != comparisonIsNegated
}
override fun toString(): String {
return StringBuilder().run {
append(operand.name.lowercase())
if (operandDivisor != null) {
append(" % ")
append(operand)
}
append(' ')
if (comparisonIsNegated) {
append('!')
}
append("= ")
var first = true
for (range in ranges) {
if (!first) {
append(',')
}
first = false
append(range.first)
if (range.first != range.last) {
append("..")
append(range.last)
}
}
toString()
}
}
}
private object NoCondition : Condition() {
override fun isFulfilled(n: Int): Boolean = true
override fun toString(): String = ""
}
private class Parser(private val description: String) {
private var currentIdx = 0
private fun eof() = currentIdx >= description.length
private fun nextUnchecked() = description[currentIdx]
private fun consumeWhitespaces() {
while (!eof() && nextUnchecked().isWhitespace()) {
currentIdx += 1
}
}
private fun raise(): Nothing = throw PluralRuleParseException(description, currentIdx + 1)
private fun assert(condition: Boolean) {
if (!condition) raise()
}
private fun peekNextOrNull() = description.getOrNull(currentIdx)
private fun peekNext() = peekNextOrNull() ?: raise()
private fun consumeNext(): Char {
val next = peekNext()
currentIdx += 1
return next
}
private fun consumeNextInt(): Int {
assert(peekNext().isDigit())
var integerValue = 0
var integerLastIdx = currentIdx
while (integerLastIdx < description.length && description[integerLastIdx].isDigit()) {
integerValue *= 10
integerValue += description[integerLastIdx] - '0'
integerLastIdx += 1
}
currentIdx = integerLastIdx
return integerValue
}
fun parse(): Condition {
consumeWhitespaces()
if (eof()) return NoCondition
val condition = nextCondition()
consumeWhitespaces()
assert(eof())
return condition
}
/**
* Syntax:
* ```
* condition = and_condition ('or' and_condition)*
* ```
*/
private fun nextCondition(): Condition {
var condition: Condition = nextAndCondition()
while (true) {
consumeWhitespaces()
if (peekNextOrNull() != 'o') break
consumeNext()
assert(consumeNext() == 'r')
condition = Or(condition, nextAndCondition())
}
return condition
}
/**
* Syntax:
* ```
* and_condition = relation ('and' relation)*
* ```
*/
private fun nextAndCondition(): Condition {
var condition: Condition = nextRelation()
while (true) {
consumeWhitespaces()
if (peekNextOrNull() != 'a') break
consumeNext()
assert(consumeNext() == 'n')
assert(consumeNext() == 'd')
condition = And(condition, nextRelation())
}
return condition
}
/**
* Syntax:
* ```
* relation = operand ('%' value)? ('=' | '!=') range_list
* ```
*/
fun nextRelation(): Relation {
val operand = nextOperand()
val divisor = nextModulusDivisor()
val negated = nextComparisonIsNegated()
val ranges = mutableListOf(nextRange())
while (peekNextOrNull() == ',') {
consumeNext()
ranges.add(nextRange())
}
// ranges is not empty here
return Relation(operand, divisor, negated, ranges.toTypedArray())
}
/**
* Syntax:
* ```
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* ```
*/
fun nextOperand(): Operand {
consumeWhitespaces()
return when (consumeNext()) {
'n' -> Operand.N
'i' -> Operand.I
'f' -> Operand.F
't' -> Operand.T
'v' -> Operand.V
'w' -> Operand.W
'c', 'e' -> Operand.C
else -> raise()
}
}
fun nextModulusDivisor(): Int? {
consumeWhitespaces()
if (peekNext() == '%') {
consumeNext()
consumeWhitespaces()
return consumeNextInt()
}
return null
}
/**
* Returns `true` for `!=`, `false` for `=`.
*/
fun nextComparisonIsNegated(): Boolean {
consumeWhitespaces()
when (peekNext()) {
'!' -> {
consumeNext()
assert(consumeNext() == '=')
return true
}
'=' -> {
consumeNext()
return false
}
else -> raise()
}
}
/**
* Returns `number..number` if the range is actually a value.
*/
fun nextRange(): IntRange {
consumeWhitespaces()
val start = consumeNextInt()
if (peekNextOrNull() != '.') {
return start..start
}
consumeNext()
assert(consumeNext() == '.')
val endInclusive = consumeNextInt()
return start..endInclusive
}
}
companion object {
/**
* Parses [description] as defined in the [Unicode Plural rules syntax](https://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax).
* For compact implementation, samples and keywords for backward compatibility are also not handled. You can
* find such keywords in the [Relations Examples](https://unicode.org/reports/tr35/tr35-numbers.html#Relations_Examples) section.
* ```
* condition = and_condition ('or' and_condition)*
* and_condition = relation ('and' relation)*
* relation = operand ('%' value)? ('=' | '!=') range_list
* operand = 'n' | 'i' | 'f' | 't' | 'v' | 'w'
* range_list = (range | value) (',' range_list)*
* range = value'..'value
* value = digit+
* digit = [0-9]
* ```
*/
fun parse(description: String): Condition = Parser(description).parse()
}
}
}

9
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt

@ -15,7 +15,7 @@ import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.LanguageQualifier
import org.jetbrains.compose.resources.RegionQualifier
internal class PluralRuleList private constructor(private val rules: Array<PluralRule>) {
internal class PluralRuleList(private val rules: Array<PluralRule>) {
fun getCategory(quantity: Int): PluralCategory {
return rules.first { rule -> rule.appliesTo(quantity) }.category
}
@ -61,12 +61,7 @@ internal class PluralRuleList private constructor(private val rules: Array<Plura
internal fun createInstance(cldrLocaleName: String): PluralRuleList {
val cldrPluralRuleListIndex = cldrPluralRuleListIndexByLocale[cldrLocaleName]!!
val cldrPluralRuleList = cldrPluralRuleLists[cldrPluralRuleListIndex]
val pluralRules = cldrPluralRuleList.map {
PluralRule(
it.first,
PluralCondition.parse(it.second),
)
}
val pluralRules = cldrPluralRuleList.map { PluralRule(it.first, it.second) }
return PluralRuleList(pluralRules.toTypedArray())
}
}

Loading…
Cancel
Save