Chanjung Kim
8 months ago
7 changed files with 429 additions and 25 deletions
@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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 categories defined in the [CLDR Language Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules). |
||||
*/ |
||||
internal enum class PluralCategory { |
||||
ZERO, |
||||
ONE, |
||||
TWO, |
||||
FEW, |
||||
MANY, |
||||
OTHER; |
||||
|
||||
companion object { |
||||
fun fromString(name: String): PluralCategory? { |
||||
return entries.firstOrNull { |
||||
it.name.equals(name, true) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,257 @@
|
||||
/* |
||||
* 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 |
||||
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 (peekNext() != '.') throw PluralConditionParseException(description) |
||||
val endInclusive = consumeNextInt() |
||||
return start..endInclusive |
||||
} |
||||
|
||||
fun nextCommaOrNull(): Char? { |
||||
return when (peekNextOrNull()) { |
||||
',' -> ',' |
||||
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 = "" |
||||
} |
@ -0,0 +1,8 @@
|
||||
/* |
||||
* 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") |
@ -0,0 +1,41 @@
|
||||
/* |
||||
* 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, |
||||
} |
@ -0,0 +1,14 @@
|
||||
/* |
||||
* 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 PluralRule(val category: PluralCategory, private val constraint: PluralCondition) { |
||||
fun appliesTo(n: Int): Boolean { |
||||
return constraint.isFulfilled(n) |
||||
} |
||||
|
||||
// add appliesTo(n: Double) or appliesTo(n: Decimal) as needed |
||||
} |
@ -0,0 +1,73 @@
|
||||
/* |
||||
* 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 kotlinx.coroutines.CoroutineStart |
||||
import kotlinx.coroutines.Deferred |
||||
import kotlinx.coroutines.async |
||||
import kotlinx.coroutines.coroutineScope |
||||
import kotlinx.coroutines.sync.Mutex |
||||
import kotlinx.coroutines.sync.withLock |
||||
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>) { |
||||
fun getCategory(quantity: Int): PluralCategory { |
||||
return rules.first { rule -> rule.appliesTo(quantity) }.category |
||||
} |
||||
|
||||
companion object { |
||||
private val cacheMutex = Mutex() |
||||
private val cache = mutableMapOf<String, Deferred<PluralRuleList>>() |
||||
private val emptyList = PluralRuleList(emptyArray()) |
||||
|
||||
@OptIn(InternalResourceApi::class) |
||||
suspend fun getInstance( |
||||
languageQualifier: LanguageQualifier, |
||||
regionQualifier: RegionQualifier, |
||||
): PluralRuleList { |
||||
val cldrLocaleName = buildCldrLocaleName(languageQualifier, regionQualifier) ?: return emptyList |
||||
return coroutineScope { |
||||
val deferred = cacheMutex.withLock { |
||||
cache.getOrPut(cldrLocaleName) { |
||||
async(start = CoroutineStart.LAZY) { |
||||
createInstance(cldrLocaleName) |
||||
} |
||||
} |
||||
} |
||||
deferred.await() |
||||
} |
||||
} |
||||
|
||||
@OptIn(InternalResourceApi::class) |
||||
private fun buildCldrLocaleName( |
||||
languageQualifier: LanguageQualifier, |
||||
regionQualifier: RegionQualifier, |
||||
): String? { |
||||
val localeWithRegion = languageQualifier.language + "_" + regionQualifier.region |
||||
if (cldrPluralRuleListIndexByLocale.containsKey(localeWithRegion)) { |
||||
return localeWithRegion |
||||
} |
||||
if (cldrPluralRuleListIndexByLocale.containsKey(languageQualifier.language)) { |
||||
return languageQualifier.language |
||||
} |
||||
return null |
||||
} |
||||
|
||||
private fun createInstance(cldrLocaleName: String): PluralRuleList { |
||||
val cldrPluralRuleListIndex = cldrPluralRuleListIndexByLocale[cldrLocaleName]!! |
||||
val cldrPluralRuleList = cldrPluralRuleLists[cldrPluralRuleListIndex] |
||||
val pluralRules = cldrPluralRuleList.map { |
||||
PluralRule( |
||||
it.first, |
||||
PluralCondition.parse(it.second), |
||||
) |
||||
} |
||||
return PluralRuleList(pluralRules.toTypedArray()) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue