Skip to content

Commit bcaba8a

Browse files
Wojciech LiberdaWojciech Liberda
authored andcommitted
Enhanced String.replaceWithArgs to support non-positional format specifiers (%s, %d) when only one argument is provided
1 parent 7f012cb commit bcaba8a

File tree

2 files changed

+50
-5
lines changed

2 files changed

+50
-5
lines changed

components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@ import org.jetbrains.compose.resources.plural.PluralCategory
44
import kotlin.io.encoding.Base64
55
import kotlin.io.encoding.ExperimentalEncodingApi
66

7-
private val SimpleStringFormatRegex = Regex("""%(\d+)\$[ds]""")
8-
internal fun String.replaceWithArgs(args: List<String>) = SimpleStringFormatRegex.replace(this) { matchResult ->
9-
args[matchResult.groupValues[1].toInt() - 1]
7+
private val SimpleStringFormatRegex = Regex("""%(?:([1-9]\d*)\$)?[ds]""")
8+
internal fun String.replaceWithArgs(args: List<String>): String {
9+
if (!SimpleStringFormatRegex.containsMatchIn(this)) return this
10+
11+
return SimpleStringFormatRegex.replace(this) { match ->
12+
val placeholderNumber = match.groups[1]?.value?.toIntOrNull()
13+
val index = when {
14+
placeholderNumber != null -> placeholderNumber - 1
15+
args.size == 1 -> 0
16+
else -> {
17+
throw IllegalArgumentException(
18+
"Formatting failed: Non-positional placeholder '${match.value}' is ambiguous when multiple arguments are provided in \"$this\""
19+
)
20+
}
21+
}
22+
args[index]
23+
}
1024
}
1125

1226
internal sealed interface StringItem {
@@ -16,14 +30,15 @@ internal sealed interface StringItem {
1630
}
1731

1832
private val stringItemsCache = AsyncCache<String, StringItem>()
33+
1934
//@TestOnly
2035
internal fun dropStringItemsCache() {
2136
stringItemsCache.clear()
2237
}
2338

2439
internal suspend fun getStringItem(
2540
resourceItem: ResourceItem,
26-
resourceReader: ResourceReader
41+
resourceReader: ResourceReader,
2742
): StringItem = stringItemsCache.getOrLoad(
2843
key = "${resourceItem.path}/${resourceItem.offset}-${resourceItem.size}"
2944
) {

components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/StringFormatTest.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,34 @@ class StringFormatTest {
180180
// Only the first argument should be used, ignoring the rest
181181
assertEquals("Hello Alice!", result)
182182
}
183-
}
183+
184+
@Test
185+
fun `replaceWithArgs handle single argument format`() {
186+
val template = "Hello %s!"
187+
val args = listOf("Alice")
188+
189+
val result = template.replaceWithArgs(args)
190+
191+
assertEquals("Hello Alice!", result)
192+
}
193+
194+
@Test
195+
fun `replaceWithArgs handle multiple placeholders for single argument`() {
196+
val template = "%1\$s and %1\$s are best friends!"
197+
val args = listOf("Alice")
198+
199+
val result = template.replaceWithArgs(args)
200+
201+
assertEquals("Alice and Alice are best friends!", result)
202+
}
203+
204+
@Test
205+
fun `replaceWithArgs throw exception when multiple arguments with single placeholder format`() {
206+
val template = "Hello %s, you have %d new messages!"
207+
val args = listOf("Alice", "15")
208+
209+
assertFailsWith<IllegalArgumentException> {
210+
template.replaceWithArgs(args)
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)