You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
15 KiB
406 lines
15 KiB
/* |
|
* 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.plural |
|
|
|
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 condition.isFulfilled(n) |
|
} |
|
|
|
private sealed class Condition { |
|
abstract fun isFulfilled(n: Int): Boolean |
|
|
|
abstract fun simplifyForInteger(): Condition |
|
|
|
abstract fun equivalentForInteger(other: Condition): Boolean |
|
|
|
/** |
|
* 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) && right.isFulfilled(n) |
|
|
|
override fun simplifyForInteger(): Condition { |
|
val leftSimplified = left.simplifyForInteger() |
|
if (leftSimplified == False) return False |
|
|
|
val rightSimplified = right.simplifyForInteger() |
|
when { |
|
leftSimplified == True -> return rightSimplified |
|
rightSimplified == False -> return False |
|
rightSimplified == True -> return leftSimplified |
|
} |
|
|
|
if (leftSimplified.equivalentForInteger(rightSimplified)) return leftSimplified |
|
return And(leftSimplified, rightSimplified) |
|
} |
|
|
|
override fun equivalentForInteger(other: Condition): Boolean { |
|
if (this === other) return true |
|
if (other !is And) return false |
|
return left.equivalentForInteger(other.left) && right.equivalentForInteger(other.right) |
|
} |
|
|
|
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) || right.isFulfilled(n) |
|
|
|
override fun simplifyForInteger(): Condition { |
|
val leftSimplified = left.simplifyForInteger() |
|
if (leftSimplified == True) return True |
|
|
|
val rightSimplified = right.simplifyForInteger() |
|
when { |
|
leftSimplified == False -> return rightSimplified |
|
rightSimplified == True -> return True |
|
rightSimplified == False -> return leftSimplified |
|
} |
|
|
|
if (leftSimplified.equivalentForInteger(rightSimplified)) return leftSimplified |
|
return Or(leftSimplified, rightSimplified) |
|
} |
|
|
|
override fun equivalentForInteger(other: Condition): Boolean { |
|
if (this === other) return true |
|
if (other !is Or) return false |
|
return left.equivalentForInteger(other.left) && right.equivalentForInteger(other.right) |
|
} |
|
|
|
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 simplifyForInteger(): Condition { |
|
return when (operand) { |
|
Operand.N, Operand.I -> Relation( |
|
Operand.N, |
|
operandDivisor, |
|
comparisonIsNegated, |
|
ranges, |
|
) |
|
|
|
else -> if (ranges.any { 0 in it } != comparisonIsNegated) True else False |
|
} |
|
} |
|
|
|
override fun equivalentForInteger(other: Condition): Boolean { |
|
if (this === other) return true |
|
if (other !is Relation) return false |
|
if ((operand == Operand.N || operand == Operand.I) != (other.operand == Operand.N || other.operand == Operand.I)) return false |
|
if (operandDivisor != other.operandDivisor) return false |
|
if (comparisonIsNegated != other.comparisonIsNegated) return false |
|
if (!ranges.contentEquals(other.ranges)) return false |
|
return true |
|
} |
|
|
|
override fun toString(): String { |
|
return StringBuilder().run { |
|
append(operand.name.lowercase()) |
|
if (operandDivisor != null) { |
|
append(" % ") |
|
append(operandDivisor) |
|
} |
|
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 True : Condition() { |
|
override fun isFulfilled(n: Int) = true |
|
override fun simplifyForInteger() = this |
|
override fun equivalentForInteger(other: Condition) = this == other |
|
override fun toString(): String = "" |
|
} |
|
|
|
private object False : Condition() { |
|
override fun isFulfilled(n: Int) = false |
|
override fun simplifyForInteger() = this |
|
override fun equivalentForInteger(other: Condition) = this == other |
|
override fun toString(): String = "(false)" |
|
} |
|
|
|
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 True |
|
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().simplifyForInteger() |
|
} |
|
} |
|
} |