-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
...src/commonMain/kotlin/de/comahe/i18n4k/messages/formatter/types/MessageSelectFormatter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package de.comahe.i18n4k.messages.formatter.types | ||
|
||
import de.comahe.i18n4k.Locale | ||
import de.comahe.i18n4k.messages.formatter.MessageValueFormatter | ||
import de.comahe.i18n4k.messages.formatter.parsing.MessageFormatContext | ||
import de.comahe.i18n4k.messages.formatter.parsing.MessagePart | ||
import de.comahe.i18n4k.messages.formatter.parsing.StylePart | ||
import de.comahe.i18n4k.messages.formatter.parsing.StylePartList | ||
import de.comahe.i18n4k.messages.formatter.parsing.StylePartNamed | ||
import de.comahe.i18n4k.messages.formatter.parsing.StylePartSimple | ||
import kotlinx.atomicfu.atomic | ||
import kotlinx.atomicfu.update | ||
import kotlinx.collections.immutable.persistentMapOf | ||
|
||
/** | ||
* Select a text value based on the value of the parameter. | ||
* | ||
* Format: | ||
* | ||
* { PARAMETER_NUMBER, select, VALUE1: TEXT1 | VALUE2 / VALUE3: TEXT2 | regex#VALUE_REGEX : TEXT_REGEX | OTHERWISE_TEXT} | ||
* - PARAMETER_NUMBER: | ||
* - Number of the parameter which value is matched against the values | ||
* of the select list | ||
* - VALUE*: | ||
* - If a values matches the value of the parameter, the corresponding | ||
* text (TEXT*) is selected | ||
* - List of several values for the same text can be separated by slash | ||
* "/" | ||
* - VALUE_REGEX | ||
* - When the value is prefixed by "regex#", it is seen as regular | ||
* repression, e.g. `regex#([A-Z])\w+` | ||
* - If the regex matches the value of the parameter, the corresponding | ||
* text (TEXT_REGEX) is selected | ||
* - TEXT*: | ||
* - Text that is returned by the pattern if the value matches the value | ||
* of the parameter | ||
* - Texts themselves can also contain patterns. So, the patterns can be | ||
* nested. | ||
* - TEXT_REGEX | ||
* - Like TEXT* but regex-groups can be refereed by <GROUP_NUMBER> | ||
* - OTHERWISE_TEXT | ||
* - If non of the values before matched, this text will be selected | ||
* - If there is no OTHERWISE_TEXT specified and no value matches, an | ||
* empty string is returned | ||
* | ||
* Values and texts are trimmed (leading and tailing whitespaces are | ||
* removed) | ||
* | ||
* Example: | ||
* | ||
* {0} has forgotten {1, select, female: her | his } {3, select, one: bag | {2} bags}. | ||
* | ||
* Usage: | ||
* | ||
* FORGOTTEN_BAG("Peter", "male", 1, "one") | ||
* -> Peter has forgotten his bag. | ||
* FORGOTTEN_BAG("Mary", "female", 2, "few") | ||
* -> Mary has forgotten her 2 bags. | ||
*/ | ||
object MessageSelectFormatter : MessageValueFormatter { | ||
|
||
private const val REGEX_PREFIX = "regex#" | ||
|
||
private val regexCache = atomic(persistentMapOf<CharSequence, Regex>()) | ||
|
||
override val typeId: String | ||
get() = "select" | ||
|
||
override fun format( | ||
result: StringBuilder, | ||
value: Any?, | ||
style: StylePart?, | ||
parameters: List<Any>, | ||
locale: Locale, | ||
context: MessageFormatContext | ||
) { | ||
if (style == null) | ||
return | ||
|
||
val messagePart: MessagePart? = getMessagePartForMatchingStyle(value, style) | ||
|
||
messagePart?.format(result, parameters, locale, context) | ||
} | ||
|
||
private fun getMessagePartForMatchingStyle(value: Any?, style: StylePart): MessagePart? { | ||
if (style is StylePartSimple) | ||
return style.data | ||
|
||
if (style is StylePartNamed) { | ||
if (valueMatches(value, style.names)) | ||
return style.data | ||
} | ||
|
||
if (style is StylePartList) { | ||
for (subStyle in style.list) { | ||
val p = getMessagePartForMatchingStyle(value, subStyle) | ||
if (p != null) | ||
return p | ||
} | ||
} | ||
return null | ||
} | ||
|
||
private fun valueMatches(value: Any?, names: Collection<CharSequence>): Boolean { | ||
for (name in names) { | ||
if (valueMatches(value, name)) | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
private fun valueMatches(value: Any?, name: CharSequence): Boolean { | ||
val valueString: CharSequence = value as? CharSequence | ||
?: value?.toString() | ||
?: return false | ||
|
||
if (name.startsWith(REGEX_PREFIX)) { | ||
val regex = getRegex(name.subSequence(REGEX_PREFIX.length, name.length)) | ||
return regex.matches(valueString) | ||
} | ||
return valueString == name | ||
} | ||
|
||
private fun getRegex(text: CharSequence): Regex { | ||
var regex = regexCache.value[text] | ||
if (regex != null) | ||
return regex | ||
regexCache.update { | ||
var regexLocal = it[text] | ||
if (regexLocal != null) { | ||
regex = regexLocal | ||
return@update it | ||
} | ||
regexLocal = Regex(text.toString()) | ||
regex = regexLocal | ||
return@update it.put(text, regexLocal) | ||
} | ||
return regex!! | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
i18n4k-core/src/commonTest/kotlin/da/comahe/i18n4k/MessageSelectFormatterTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package da.comahe.i18n4k | ||
|
||
import de.comahe.i18n4k.Locale | ||
import de.comahe.i18n4k.config.I18n4kConfigDefault | ||
import de.comahe.i18n4k.i18n4k | ||
import de.comahe.i18n4k.messages.formatter.MessageFormatterDefault.format | ||
import kotlin.test.BeforeTest | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
|
||
class MessageSelectFormatterTest { | ||
|
||
private var i18n4kConfig = I18n4kConfigDefault() | ||
|
||
@BeforeTest | ||
fun init() { | ||
i18n4k = i18n4kConfig | ||
i18n4kConfig.restoreDefaultSettings() | ||
} | ||
|
||
|
||
@Test | ||
fun test_select_simple() { | ||
val locale = Locale("en") | ||
val pattern = "{0, select, 0:zero|1/2:few|3/4/5/6:many|too much }"; | ||
|
||
assertEquals("zero", format(pattern, listOf(0), locale)) | ||
assertEquals("few", format(pattern, listOf(1), locale)) | ||
assertEquals("few", format(pattern, listOf("2"), locale)) | ||
assertEquals("many", format(pattern, listOf(3), locale)) | ||
assertEquals("many", format(pattern, listOf(4), locale)) | ||
assertEquals("many", format(pattern, listOf("5"), locale)) | ||
assertEquals("many", format(pattern, listOf("6"), locale)) | ||
assertEquals("too much", format(pattern, listOf(7), locale)) | ||
assertEquals("too much", format(pattern, listOf("some thing"), locale)) | ||
} | ||
|
||
@Test | ||
fun test_select_nested() { | ||
val locale = Locale("en") | ||
val pattern = "{0, select, 0:{1}|1/2:{2}|3/4/5/6:{3}|{4} }"; | ||
val extraParams = listOf("zero", "few", "many", "too much") | ||
|
||
assertEquals("zero", format(pattern, listOf(0) + extraParams, locale)) | ||
assertEquals("few", format(pattern, listOf(1) + extraParams, locale)) | ||
assertEquals("few", format(pattern, listOf("2") + extraParams, locale)) | ||
assertEquals("many", format(pattern, listOf(3) + extraParams, locale)) | ||
assertEquals("many", format(pattern, listOf(4) + extraParams, locale)) | ||
assertEquals("many", format(pattern, listOf("5") + extraParams, locale)) | ||
assertEquals("many", format(pattern, listOf("6") + extraParams, locale)) | ||
assertEquals("too much", format(pattern, listOf(7) + extraParams, locale)) | ||
assertEquals("too much", format(pattern, listOf("some thing") + extraParams, locale)) | ||
} | ||
|
||
@Test | ||
fun test_select_regex() { | ||
val locale = Locale("en") | ||
val pattern = | ||
"{0, select, 0:zero | regex#\\d+ : digits | regex#\\w+ : word | regex#[abc<>-]+ : mix | else }" | ||
|
||
assertEquals("zero", format(pattern, listOf(0), locale)) | ||
assertEquals("digits", format(pattern, listOf(1), locale)) | ||
assertEquals("digits", format(pattern, listOf(12), locale)) | ||
assertEquals("digits", format(pattern, listOf("001200"), locale)) | ||
assertEquals("word", format(pattern, listOf('a'), locale)) | ||
assertEquals("word", format(pattern, listOf("b"), locale)) | ||
assertEquals("word", format(pattern, listOf("abc"), locale)) | ||
assertEquals("word", format(pattern, listOf("abc".subSequence(0, 1)), locale)) | ||
assertEquals("word", format(pattern, listOf("a1"), locale)) | ||
assertEquals("word", format(pattern, listOf("b2"), locale)) | ||
assertEquals("word", format(pattern, listOf("1a"), locale)) | ||
assertEquals("word", format(pattern, listOf("2b"), locale)) | ||
assertEquals("mix", format(pattern, listOf("<a>"), locale)) | ||
assertEquals("mix", format(pattern, listOf(">"), locale)) | ||
assertEquals("mix", format(pattern, listOf("<"), locale)) | ||
assertEquals("mix", format(pattern, listOf("----"), locale)) | ||
assertEquals("else", format(pattern, listOf("#"), locale)) | ||
assertEquals("else", format(pattern, listOf(";;;"), locale)) | ||
} | ||
|
||
@Test | ||
fun test_invalid() { | ||
val locale = Locale("en") | ||
|
||
assertEquals("!", format("{~, select }!", listOf('a'), locale)) | ||
assertEquals("!", format("{~, select , }!", listOf('a'), locale)) | ||
assertEquals("!", format("{~, select , a: x }!", listOf('a'), locale)) | ||
|
||
assertEquals("!", format("{0, select }!", listOf('a'), locale)) | ||
assertEquals("!", format("{0, select , }!", listOf('a'), locale)) | ||
assertEquals("!", format("{0, select , b: y }!", listOf('a'), locale)) | ||
assertEquals("!", format("{0, select , | x }!", listOf('a'), locale)) | ||
assertEquals("!", format("{0, select , | a: x }!", listOf('a'), locale)) | ||
|
||
assertEquals("{1}!", format("{1, select , x }!", listOf('a'), locale)) | ||
assertEquals("{1}!", format("{1, select , a: x }!", listOf('a'), locale)) | ||
} | ||
} |