diff --git a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/TextFormatter.kt b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/TextFormatter.kt index 0278fc4f9c38507de68b08d3668638e95b148cee..3d605faa9253633e69852f8b60b85ebec3cf0384 100644 --- a/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/TextFormatter.kt +++ b/app/src/main/kotlin/de/justjanne/quasseldroid/util/format/TextFormatter.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.core.text.buildSpannedString import de.justjanne.quasseldroid.util.AnnotatedStringAppender +import de.justjanne.quasseldroid.util.extensions.component6 +import de.justjanne.quasseldroid.util.extensions.component7 import java.util.* import java.util.regex.Pattern @@ -66,7 +68,7 @@ object TextFormatter { is FormatString.FixedValue -> target.append(block.content) is FormatString.FormatSpecifier -> { val arg = when { - block.index != null -> args[block.index - 1] + block.argumentIndex != null -> args[block.argumentIndex - 1] block.flags.orEmpty().contains(FLAG_REUSE_ARGUMENT) -> args[argIndex] else -> args[argIndex++] } @@ -120,23 +122,23 @@ object TextFormatter { } index = match.range.last + 1 - val conversionGroup = match.groups["conversion"]?.value - require(conversionGroup != null) { - "Invalid format string '$match', missing conversion" + val groupValues = match.groupValues + require(groupValues.size == 7) { + "Invalid match '$match', should return 6 groups, returned ${groupValues.size}" } - require(conversionGroup.length == 1) { + val (_, argumentIndex, flags, width, precision, time, conversion) = groupValues + require(conversion.length == 1) { "Invalid format string '$match', conversion too long" } - val conversion = conversionGroup.first() yield( FormatString.FormatSpecifier( - index = match.groups["index"]?.value?.toIntOrNull(), - flags = match.groups["flags"]?.value, - width = match.groups["width"]?.value?.toIntOrNull(), - precision = match.groups["precision"]?.value?.toIntOrNull(), - time = match.groups["time"] != null, - conversion = conversion + argumentIndex = argumentIndex.takeIf(String::isNotEmpty)?.toIntOrNull(), + flags = flags.takeIf(String::isNotEmpty), + width = width.takeIf(String::isNotEmpty)?.toIntOrNull(), + precision = precision.takeIf(String::isNotEmpty)?.toIntOrNull(), + time = time.takeIf(String::isNotEmpty) != null, + conversion = conversion.first() ) ) } @@ -148,5 +150,5 @@ object TextFormatter { private const val FLAG_REUSE_ARGUMENT = '<' private val FORMAT_SEQUENCE = - Pattern.compile("%(?:(?<index>[0-9]+)\\$)?(?<flags>[,\\-(+# 0<]+)?(?<width>[0-9]+)?(?:\\.(?<precision>[0-9]+))?(?<time>[tT])?(?<conversion>[a-zA-Z])") + Pattern.compile("%(?:([0-9]+)\\\$)?([,\\-(+# 0<]*)([0-9]*)(?:\\.([0-9]*))?([tT]?)([a-zA-Z])") } diff --git a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/TextFormatterTest.kt b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/TextFormatterTest.kt index 3a844e9dec080cbdb18ab4838b2d4b7423a481cf..23fcf9e53803036c341d09d5dbb94dc59619d202 100644 --- a/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/TextFormatterTest.kt +++ b/app/src/test/kotlin/de/kuschku/justjanne/quasseldroid/util/irc/TextFormatterTest.kt @@ -1,21 +1,31 @@ package de.kuschku.justjanne.quasseldroid.util.irc -import android.text.Spanned -import android.text.style.ForegroundColorSpan import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.core.text.buildSpannedString +import de.justjanne.quasseldroid.util.format.FormatString import de.justjanne.quasseldroid.util.format.TextFormatter import org.junit.jupiter.api.Test import java.util.* import kotlin.test.assertEquals class TextFormatterTest { - @Test - fun testBasicFormatting() { - val calendar = GregorianCalendar(1995, Calendar.MAY, 23, 13, 34, 18) + private val calendar = GregorianCalendar(1995, Calendar.MAY, 23, 13, 34, 18) + @Test + fun testNumberedArgumentIndex() { + assertEquals( + listOf( + FormatString.FormatSpecifier(argumentIndex = 4, width = 2, conversion = 's'), + FormatString.FixedValue(" "), + FormatString.FormatSpecifier(argumentIndex = 3, width = 2, conversion = 's'), + FormatString.FixedValue(" "), + FormatString.FormatSpecifier(argumentIndex = 2, width = 2, conversion = 's'), + FormatString.FixedValue(" "), + FormatString.FormatSpecifier(argumentIndex = 1, width = 2, conversion = 's') + ), + TextFormatter.parseBlocks("%4\$2s %3\$2s %2\$2s %1\$2s").toList() + ) assertEquals( " d c b a", TextFormatter.format( @@ -23,10 +33,55 @@ class TextFormatterTest { locale = Locale.ENGLISH ) ) + } + + @Test + fun testReuseFlag() { + assertEquals( + listOf( + FormatString.FixedValue("Duke's Birthday: "), + FormatString.FormatSpecifier(argumentIndex = 1, time = true, conversion = 'b'), + FormatString.FixedValue(" "), + FormatString.FormatSpecifier(flags = "<", time = true, conversion = 'e'), + FormatString.FixedValue(", "), + FormatString.FormatSpecifier(flags = "<", time = true, conversion = 'Y'), + ), + TextFormatter.parseBlocks("Duke's Birthday: %1\$tb %<te, %<tY").toList() + ) + assertEquals( + "Duke's Birthday: May 23, 1995", + TextFormatter.format( + "Duke's Birthday: %1\$tb %<te, %<tY", + calendar, + locale = Locale.ENGLISH, + ) + ) + } + + @Test + fun testFloatFormatting() { + assertEquals( + listOf( + FormatString.FixedValue("e = "), + FormatString.FormatSpecifier(flags = "+", width = 10, precision = 4, conversion = 'f'), + ), + TextFormatter.parseBlocks("e = %+10.4f").toList() + ) assertEquals( "e = +2,7183", TextFormatter.format("e = %+10.4f", Math.E, locale = Locale.FRANCE) ) + } + + @Test + fun testAccountingFormatting() { + assertEquals( + listOf( + FormatString.FixedValue("Amount gained or lost since last statement: \$ "), + FormatString.FormatSpecifier(flags = "(,", precision = 2, conversion = 'f'), + ), + TextFormatter.parseBlocks("Amount gained or lost since last statement: \$ %(,.2f").toList() + ) assertEquals( "Amount gained or lost since last statement: $ (6,217.58)", TextFormatter.format( @@ -34,6 +89,17 @@ class TextFormatterTest { locale = Locale.ENGLISH ) ) + } + + @Test + fun testDateTimeFormatting() { + assertEquals( + listOf( + FormatString.FixedValue("Local time: "), + FormatString.FormatSpecifier(time = true, conversion = 'T'), + ), + TextFormatter.parseBlocks("Local time: %tT").toList() + ) assertEquals( "Local time: 13:34:18", TextFormatter.format( @@ -42,14 +108,17 @@ class TextFormatterTest { locale = Locale.ENGLISH ) ) + assertEquals( - "Unable to open file 'food': No such file or directory", - TextFormatter.format( - "Unable to open file '%1\$s': %2\$s", - "food", - "No such file or directory", - locale = Locale.ENGLISH - ) + listOf( + FormatString.FixedValue("Duke's Birthday: "), + FormatString.FormatSpecifier(argumentIndex = 1, time = true, conversion = 'b'), + FormatString.FixedValue(" "), + FormatString.FormatSpecifier(argumentIndex = 1, time = true, conversion = 'e'), + FormatString.FixedValue(", "), + FormatString.FormatSpecifier(argumentIndex = 1, time = true, conversion = 'Y'), + ), + TextFormatter.parseBlocks("Duke's Birthday: %1\$tb %1\$te, %1\$tY").toList() ) assertEquals( "Duke's Birthday: May 23, 1995", @@ -59,14 +128,6 @@ class TextFormatterTest { locale = Locale.ENGLISH ) ) - assertEquals( - "Duke's Birthday: May 23, 1995", - TextFormatter.format( - "Duke's Birthday: %1\$tb %<te, %<tY", - calendar, - locale = Locale.ENGLISH, - ) - ) } @Test