1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.juneau.commons.lang;
18
19 import static org.apache.juneau.commons.utils.Utils.*;
20 import static org.junit.jupiter.api.Assertions.*;
21 import static java.util.stream.Collectors.*;
22
23 import java.math.*;
24 import java.text.*;
25 import java.util.*;
26 import java.util.stream.*;
27
28 import org.apache.juneau.*;
29 import org.apache.juneau.commons.function.*;
30 import org.junit.jupiter.api.*;
31
32 class StringFormat_Test extends TestBase {
33
34 private static StringFormat fs(String pattern) {
35 return StringFormat.of(pattern);
36 }
37
38 private static String stringify(ThrowingSupplier<String> supplier) {
39 try {
40 return supplier.get();
41 } catch (Throwable t) {
42 return t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
43 }
44 }
45
46 private static void assertStringFormat(String pattern, Locale locale, Object... args) {
47 var expected = stringify(()->String.format(locale, pattern, args));
48 var actual = "";
49 var fmt = (StringFormat)null;
50 try {
51 var fmt2 = fs(pattern);
52 fmt = fmt2;
53 actual = stringify(()->fmt2.format(locale, args));
54 } catch (Throwable t) {
55 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
56 }
57 if (!expected.equals(actual)) {
58 System.out.println("Pattern: " + pattern);
59 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
60 System.out.println("toPattern(): " + toPattern);
61 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
62 }
63 }
64
65 private static void assertStringFormat(String pattern, Object... args) {
66 var expected = stringify(()->String.format(pattern, args));
67 var actual = "";
68 var fmt = (StringFormat)null;
69 try {
70 var fmt2 = fs(pattern);
71 fmt = fmt2;
72 actual = stringify(()->fmt2.format(args));
73 } catch (Throwable t) {
74 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
75 }
76 if (!expected.equals(actual)) {
77 System.out.println("Pattern: " + pattern);
78 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
79 System.out.println("toPattern(): " + toPattern);
80 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
81 }
82 }
83
84 private static void assertMessageFormat(String pattern, Locale locale, Object... args) {
85 var expected = stringify(()->new MessageFormat(pattern, locale).format(args));
86 var actual = "";
87 var fmt = (StringFormat)null;
88 try {
89 var fmt2 = fs(pattern);
90 fmt = fmt2;
91 actual = stringify(()->fmt2.format(locale, args));
92 } catch (Throwable t) {
93 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
94 }
95 if (!expected.equals(actual)) {
96 System.out.println("Pattern: " + pattern);
97 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
98 System.out.println("toPattern(): " + toPattern);
99 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
100 }
101 }
102
103 private static void assertMessageFormat(String pattern, Object... args) {
104 var expected = stringify(()->MessageFormat.format(pattern, args));
105 var actual = "";
106 var fmt = (StringFormat)null;
107 try {
108 var fmt2 = fs(pattern);
109 fmt = fmt2;
110 actual = stringify(()->fmt2.format(args));
111 } catch (Throwable t) {
112 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
113 }
114 if (!expected.equals(actual)) {
115 System.out.println("Pattern: " + pattern);
116 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
117 System.out.println("toPattern(): " + toPattern);
118 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
119 }
120 }
121
122 private static void assertMixedFormat(String expected, String pattern, Locale locale, Object... args) {
123 var actual = "";
124 var fmt = (StringFormat)null;
125 try {
126 var fmt2 = fs(pattern);
127 fmt = fmt2;
128 actual = stringify(()->fmt2.format(locale, args));
129 } catch (Throwable t) {
130 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
131 }
132 if (!expected.equals(actual)) {
133 System.out.println("Pattern: " + pattern);
134 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
135 System.out.println("toPattern(): " + toPattern);
136 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
137 }
138 }
139
140 private static void assertMixedFormat(String expected, String pattern, Object... args) {
141 var actual = "";
142 var fmt = (StringFormat)null;
143 try {
144 var fmt2 = fs(pattern);
145 fmt = fmt2;
146 actual = stringify(()->fmt2.format(args));
147 } catch (Throwable t) {
148 actual = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
149 }
150 if (!expected.equals(actual)) {
151 System.out.println("Pattern: " + pattern);
152 var toPattern = opt(fmt).map(x -> x.toPattern()).orElse(null);
153 System.out.println("toPattern(): " + toPattern);
154 fail("Pattern: " + pattern + ", toPattern(): " + toPattern + ", expected: <" + expected + "> but was: <" + actual + ">");
155 }
156 }
157
158
159
160
161 @Test void a01_messageFormat() {
162 assertMessageFormat("Hello {0}", "John");
163 assertMessageFormat("Price: {0,number,currency}", 19.99);
164 assertMessageFormat("{0} has {1} items and {2} friends", "John", 5, 3);
165 assertMessageFormat("Hello {0} world", "John");
166 assertMessageFormat("Count: {0,number,integer}", 1234);
167 assertMessageFormat("Date: {0,date,short}", new Date(0));
168 assertMessageFormat("Time: {0,time,short}", new Date(0));
169
170 assertMessageFormat("Date: {0}", new Date(0));
171 assertMessageFormat("Value: {0}", (String)null);
172 assertMessageFormat("Name: {0}", "");
173 assertMessageFormat("Text: {0}\nNewline\tTab", "Hello");
174 assertMessageFormat("Unicode: {0} 中文", "Test");
175 assertMessageFormat("{0}{1}", "A", "B");
176 assertMessageFormat("{0} and {0} again", "Hello");
177 assertMessageFormat("Price: {0,number,currency}, Count: {1,number,integer}, Date: {2,date,short}", 19.99, 42, new java.util.Date());
178 assertMessageFormat("Price: {0,number,currency}", Locale.US, 19.99);
179 assertMessageFormat("Price: {0,number,currency}", Locale.FRANCE, 19.99);
180 assertMessageFormat("a '{0}' b");
181 assertMessageFormat("a ''{0}'' b", 1);
182 assertMessageFormat("'{0}'");
183 assertMessageFormat("''{0}''", 1);
184
185
186 assertMessageFormat("Set: {{0}}", 50);
187 assertMessageFormat("Set: {{0}} and {{1}}", "A", "B");
188 assertMessageFormat("Hello {0}");
189 assertMessageFormat("{0} has {1} items and {2} friends", "John", 5);
190 assertMessageFormat("Hello {");
191 assertMessageFormat("Hello {0");
192 assertMessageFormat("Hello '");
193 assertMessageFormat("Hello 'x");
194 }
195
196
197
198
199 @Test void a02_stringFormat() {
200 assertStringFormat("Hello %s", "John");
201 assertStringFormat("Price: $%.2f", 19.99);
202 assertStringFormat("Name: %-10s Age: %3d", "John", 25);
203 assertStringFormat("Color: #%06X", 0xFF5733);
204 assertStringFormat("Hello world");
205 assertStringFormat("Progress: %d%%", 50);
206 assertStringFormat("");
207 assertStringFormat("%1$s loves %2$s, and %1$s also loves %3$s", "Alice", "Bob", "Charlie");
208 assertStringFormat("Hello %1$s", "John");
209 assertStringFormat("Price: %1$.2f", 19.99);
210 assertStringFormat("Octal: %o", 64);
211 assertStringFormat("Octal: %o", 255);
212 assertStringFormat("Octal: %o", (Number)null);
213 assertStringFormat("Flag: %b", true);
214 assertStringFormat("Flag: %b", false);
215 assertStringFormat("Flag: %b", (Boolean)null);
216
217 assertStringFormat("Flag: %B", true);
218 assertStringFormat("Flag: %B", false);
219 assertStringFormat("Flag: %B", (Boolean)null);
220 assertStringFormat("Flag: %B", "hello");
221 assertStringFormat("Flag: %B", 42);
222 assertStringFormat("Char: %c", 'A');
223 assertStringFormat("Char: %c", "A");
224 assertStringFormat("Char: %c", (String)null);
225 assertStringFormat("Value: %.2e", 1234567.0);
226 assertStringFormat("Value: %.2e", (Number)null);
227 assertStringFormat("Number: %+10.2f", 19.99);
228 assertStringFormat("ID: %05d", 42);
229 assertStringFormat("Value: %d", (Number)null);
230 assertStringFormat("Value: %s", (String)null);
231 assertStringFormat("Name: %s", "");
232 assertStringFormat("%s%s", "A", "B");
233 assertStringFormat("Progress: %d%% Complete: %d%%", 50, 75);
234 assertStringFormat("%1$s and %1$s again", "Hello");
235 assertStringFormat("Hex: 0x%08X, Decimal: %+d, Float: %10.3f", 255, 42, 3.14159);
236 assertStringFormat("Price: %.2f", Locale.US, 19.99);
237 assertStringFormat("Price: %.2f", Locale.FRANCE, 19.99);
238 assertStringFormat("Value: %s", (Object)null);
239 assertStringFormat("Int: %d", 42);
240 assertStringFormat("Long: %d", 1234567890L);
241 assertStringFormat("Byte: %d", (byte)127);
242 assertStringFormat("Short: %d", (short)32767);
243 assertStringFormat("Int: %d", Locale.FRANCE, 1234);
244 assertStringFormat("Long: %d", Locale.GERMANY, 1234567L);
245 assertStringFormat("Value: %d", "not-a-number");
246 assertStringFormat("Hex: %x", 255);
247 assertStringFormat("Hex: %x", 255L);
248 assertStringFormat("Hex: %x", (byte)255);
249 assertStringFormat("Hex: %x", (Number)null);
250 assertStringFormat("Hex: %X", 255);
251 assertStringFormat("Hex: %X", 0xABCL);
252 assertStringFormat("Hex: %X", (short)255);
253 assertStringFormat("Hex: %X", (Number)null);
254 assertStringFormat("Octal: %o", 255L);
255 assertStringFormat("Octal: %o", (byte)64);
256 assertStringFormat("Value: %b", "hello");
257 assertStringFormat("Value: %b", 42);
258 assertStringFormat("Char: %c", 65);
259 assertStringFormat("Char: %c", 65L);
260 assertStringFormat("Char: %c", "X");
261 assertStringFormat("Char: %C", (Character)null);
262 assertStringFormat("Char: %C", 66);
263 assertStringFormat("Char: %C", 66L);
264 assertStringFormat("Float: %f", 3.14f);
265 assertStringFormat("Double: %f", 3.14159);
266 assertStringFormat("Float: %f", Locale.FRANCE, 3.14f);
267 assertStringFormat("Double: %f", Locale.GERMANY, 1234.56);
268 assertStringFormat("Value: %f", (Number)null);
269 assertStringFormat("Value: %f", "not-a-number");
270 assertStringFormat("Value: %.2e", 1234.56);
271 assertStringFormat("Value: %S", "hello");
272 assertStringFormat("Value: %S", (String)null);
273 assertStringFormat("Value: %B", true);
274 assertStringFormat("Char: %C", 'a');
275 assertStringFormat("Float: %F", 3.14);
276
277 assertStringFormat("Line 1%nLine 2");
278 assertStringFormat("First: %s%nSecond: %s", "one", "two");
279 assertStringFormat("%s %n %s", "first", "second");
280
281
282 assertStringFormat("Hello %s");
283 assertStringFormat("Hello %s and %s", "John");
284 assertStringFormat("Hello %");
285 assertStringFormat("Hello %s and %", "John");
286 assertStringFormat("Hello %x$s", "John");
287 }
288
289
290
291
292 @Test void a03_mixedFormat() {
293 assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
294 assertMixedFormat("User Alice has admin and 10 items", "User {0} has %s and {2} items", "Alice", "admin", 10);
295 assertMixedFormat("Alice loves Bob, and Alice also loves Charlie", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob", "Charlie");
296 assertMixedFormat("Alice has 5 items, Bob has 3 items, total: 8", "{0} has %d items, {2} has %d items, total: %d", "Alice", 5, "Bob", 3, 8);
297 assertMixedFormat("Alice Bob Charlie", "{0} %2$s {2}", "Alice", "Bob", "Charlie");
298 assertMixedFormat("Hello John, you have 5 items", "Hello {0}, you have %d items", "John", 5);
299 assertMixedFormat("A B B D C", "{0} %s {1} %s {2}", "A", "B", "C", "D");
300 assertMixedFormat("ABB", "{0}%s{1}", "A", "B", "C");
301 assertMixedFormat("Hello and Hello are the same", "{0} and %1$s are the same", "Hello");
302
303
304 assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "Hello {0} and %s", "John");
305 assertMixedFormat("John has 5 items and {2} friends", "{0} has %d items and {2} friends", "John", 5);
306 assertMixedFormat("MissingFormatArgumentException: Format specifier '%s'", "%1$s loves %2$s, and {0} also loves %3$s", "Alice", "Bob");
307 }
308
309
310
311
312 @Test void a04_supportedButDeviatesFromMessageFormat() {
313
314 assertMixedFormat("Hello John world", "Hello {} world", "John");
315 assertMixedFormat("A B C", "{} {} {}", "A", "B", "C");
316
317 assertMixedFormat("Number: 42", "Number: %d", new BigDecimal("42"));
318
319
320
321
322 var fmt = fs("Price: {0,number,currency}");
323 var result = stringify(()->fmt.format(Locale.US, 19.99));
324 assertTrue(result.contains("19.99"), "Result should contain '19.99', but was: " + result);
325 assertTrue(result.contains("Price: "), "Result should contain 'Price: ', but was: " + result);
326
327 assertTrue(result.contains("$") || result.contains("USD"),
328 "Result should contain '$' or 'USD' for currency, but was: " + result);
329 }
330
331
332
333
334 @Test void a05_errors() {
335 assertThrows(IllegalArgumentException.class, () -> new StringFormat(null));
336 assertThrows(IllegalArgumentException.class, () -> fs(null));
337 }
338
339 @Test void a06_caching() {
340
341 assertSame(fs("Hello {0}"), fs("Hello {0}"));
342
343
344 assertNotSame(fs("Hello {0}"), fs("Hello %s"));
345
346
347 var fmt1 = new StringFormat("Hello {0}");
348 var fmt2 = new StringFormat("Hello {0}");
349 assertNotSame(fmt1, fmt2);
350 assertEquals(fmt1, fmt2);
351 }
352
353 @Test void a07_equalsAndHashCode() {
354 var fmt1 = StringFormat.of("Hello {0}");
355 var fmt2 = StringFormat.of("Hello {0}");
356 var fmt3 = StringFormat.of("Hello %s");
357
358
359 assertEquals(fmt1, fmt2);
360 assertNotEquals(fmt1, fmt3);
361
362
363 assertNotEquals(fmt1, null);
364
365
366 assertNotEquals(fmt1, "Hello {0}");
367 assertNotEquals(fmt1, new Object());
368
369
370 var fmt4 = StringFormat.of("Different pattern");
371 assertNotEquals(fmt1, fmt4);
372
373
374 assertEquals(fmt1.hashCode(), fmt2.hashCode());
375 }
376
377 @Test void a08_toString() {
378 assertEquals("Hello {0}", fs("Hello {0}").toString());
379 }
380
381 @Test void a09_toPattern() {
382
383 assertEquals("[L:Hello ]", fs("Hello ").toPattern());
384 assertEquals("[L:a ][L:{0}][L: b]", fs("a '{0}' b").toPattern());
385
386
387 assertEquals("[L:Hello ][M:s0]", fs("Hello {0}").toPattern());
388 assertEquals("[L:Hello ][M:s0][L: ][M:s1]", fs("Hello {0} {1}").toPattern());
389
390
391 assertEquals("[L:Price: ][M:o0:{0,number,currency}]", fs("Price: {0,number,currency}").toPattern());
392 assertEquals("[L:Count: ][M:o0:{0,number,integer}]", fs("Count: {0,number,integer}").toPattern());
393 assertEquals("[L:Date: ][M:o0:{0,date,short}]", fs("Date: {0,date,short}").toPattern());
394
395
396 assertEquals("[L:Hello ][S:s0:%s]", fs("Hello %s").toPattern());
397 assertEquals("[L:Number: ][S:d0:%d]", fs("Number: %d").toPattern());
398 assertEquals("[L:Hex: ][S:x0:%x]", fs("Hex: %x").toPattern());
399 assertEquals("[L:Float: ][S:z0:%.2f]", fs("Float: %.2f").toPattern());
400 assertEquals("[L:ID: ][S:z0:%05d]", fs("ID: %05d").toPattern());
401
402
403 assertEquals("[L:Hello ][M:s0][L:, you have ][S:d1:%d][L: items]", fs("Hello {0}, you have %d items").toPattern());
404 assertEquals("[L:Price: ][M:o0:{0,number,currency}][L: and ][S:s1:%s]", fs("Price: {0,number,currency} and %s").toPattern());
405
406
407 assertEquals("[L:Month: ][S:z0:%tm]", fs("Month: %tm").toPattern());
408 assertEquals("[L:Year: ][S:z0:%tY]", fs("Year: %tY").toPattern());
409 assertEquals("[L:Date: ][S:z0:%TD]", fs("Date: %TD").toPattern());
410
411
412 var lineSep = System.lineSeparator();
413 assertEquals("[L:Line 1][L:" + lineSep + "][L:Line 2]", fs("Line 1%nLine 2").toPattern());
414 assertEquals("[S:s0:%s][L: ][L:" + lineSep + "][L: ][S:s1:%s]", fs("%s %n %s").toPattern());
415 }
416
417 @Test void a10_veryLongPattern() {
418 var pattern = "Start: " + IntStream.range(0, 10).mapToObj(i -> "{" + i + "}").collect(joining(" ")) + " ";
419 var args = IntStream.range(0, 10).boxed().toArray();
420 assertMessageFormat(pattern, args);
421 }
422
423 @Test void a11_parseIndexErrors() {
424 assertThrows(IllegalArgumentException.class, () -> fs("Hello {abc}"));
425 }
426
427
428
429
430
431 @Test void a14_messageFormatTokenBranches() {
432
433 var fmt1 = StringFormat.of("Hello {0}");
434 var result1 = fmt1.format((Object[])null);
435 assertEquals("Hello {0}", result1);
436
437
438 var fmt2 = StringFormat.of("Price: {0,number,currency}");
439 var result2 = fmt2.format((Object[])null);
440 assertEquals("Price: {0,number,currency}", result2);
441
442
443
444 var fmt3 = StringFormat.of("Hello {0}");
445 var result3 = fmt3.format((Locale)null, "World");
446
447 assertEquals("Hello World", result3);
448
449
450
451 var fmt4 = StringFormat.of("Price: {0,number,currency}");
452 var result4 = fmt4.format(Locale.US, 19.99);
453
454 assertTrue(result4.contains("19.99") || result4.contains("$19.99"));
455
456 var result5 = fmt4.format(Locale.FRANCE, 19.99);
457
458 assertTrue(result5.contains("19.99") || result5.contains("19,99"));
459
460
461
462
463
464
465
466
467
468
469
470
471 }
472
473
474
475
476
477 @Test void a15_stringFormatTokenBranches() {
478
479 var fmt1 = StringFormat.of("Hello %s");
480 assertThrows(java.util.MissingFormatArgumentException.class, () -> fmt1.format((Object[])null));
481
482
483 var fmt2 = StringFormat.of("Price: %.2f");
484 assertThrows(java.util.MissingFormatArgumentException.class, () -> fmt2.format((Object[])null));
485
486
487 var fmt3 = StringFormat.of("First: %1$s, Second: %2$s");
488 assertThrows(java.util.MissingFormatArgumentException.class, () -> fmt3.format((Object[])null));
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505 }
506
507 @Test void a12_localeHandling() {
508
509
510
511
512
513 assertStringFormat("Hello %s", (Locale)null, "John");
514 assertStringFormat("Number: %d", (Locale)null, 42);
515 assertStringFormat("Float: %.2f", (Locale)null, 3.14);
516
517
518 assertStringFormat("Hello %s", Locale.getDefault(), "John");
519 assertStringFormat("Number: %d", Locale.getDefault(), 42);
520 assertStringFormat("Float: %.2f", Locale.getDefault(), 3.14);
521
522
523 assertStringFormat("Hello %s", Locale.FRANCE, "John");
524 assertStringFormat("Number: %d", Locale.GERMANY, 42);
525 assertStringFormat("Float: %.2f", Locale.JAPAN, 3.14);
526 }
527
528
529
530
531 @Test void a13_format_withLocale() {
532
533 String result1 = StringFormat.format("Hello", Locale.US);
534 assertEquals("Hello", result1);
535
536 String result2 = StringFormat.format("Test pattern", Locale.FRANCE);
537 assertEquals("Test pattern", result2);
538
539
540 String result3 = StringFormat.format("Hello %s", Locale.US, "World");
541 assertEquals("Hello World", result3);
542
543 String result4 = StringFormat.format("Price: {0,number,currency}", Locale.US, 19.99);
544 assertTrue(result4.contains("19.99") || result4.contains("$19.99"));
545
546
547 String result5 = StringFormat.format("Test", (Locale)null);
548 assertEquals("Test", result5);
549 }
550 }