diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt deleted file mode 100644 index d4d1a11e23..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralCondition.kt +++ /dev/null @@ -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, - ) : 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() - 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 = "" -} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt deleted file mode 100644 index 05c4d6ce89..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralConditionParseException.kt +++ /dev/null @@ -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") \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt deleted file mode 100644 index 19103e2b0e..0000000000 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralOperand.kt +++ /dev/null @@ -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, -} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt index b8a63fcb3b..06b53ed9d7 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRule.kt +++ b/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, + ) : 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() + } + } } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt index 8e5b04e781..bcde4d8bc6 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/intl/PluralRuleList.kt +++ b/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) { +internal class PluralRuleList(private val rules: Array) { 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