Skip to content

Commit

Permalink
[#42] Added select pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
comahe-de committed Nov 16, 2023
1 parent 945eee8 commit 1dd33bc
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import de.comahe.i18n4k.messages.formatter.parsing.MessageFormatContext
import de.comahe.i18n4k.messages.formatter.parsing.MessageParser
import de.comahe.i18n4k.messages.formatter.parsing.MessagePart
import de.comahe.i18n4k.messages.formatter.types.MessageNumberFormatters
import de.comahe.i18n4k.messages.formatter.types.MessageSelectFormatter
import de.comahe.i18n4k.messages.formatter.types.MessageTransformFormatters
import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
Expand Down Expand Up @@ -79,7 +80,8 @@ object MessageFormatterDefault : MessageFormatter {
private val messageFormatContext = atomic(
MessageFormatContext(
(MessageNumberFormatters.all
+ MessageTransformFormatters.all)
+ MessageTransformFormatters.all
+ MessageSelectFormatter)
.associateBy({ it.typeId }, { it }).toPersistentMap()
)
)
Expand Down
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!!
}
}
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))
}
}

0 comments on commit 1dd33bc

Please sign in to comment.