string-format: String.format/formatted/printf cheat sheet module (specifier anatomy, rounding vs C printf, locales, Indian lakh/crore grouping Formatter cannot do, format exceptions, %t dates, concat cost), 11 contract tests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7Sr2zcoRD2watXv1G
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
/**
|
||||
* The anatomy of a format specifier: %[argument_index$][flags][width][.precision]conversion.
|
||||
* Each line below changes exactly one part of the specifier so the effect is visible.
|
||||
*/
|
||||
public class BasicsDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("== conversions: what kind of value ==");
|
||||
System.out.println(String.format("%s | %d | %f | %x | %c | %b", "text", 42, 3.14159, 255, 'A', true));
|
||||
|
||||
System.out.println("== width: minimum characters ==");
|
||||
System.out.println(String.format("[%8s] [%-8s] [%8d] [%-8d]", "ab", "ab", 42, 42));
|
||||
|
||||
System.out.println("== precision: digits after the point (f), or max chars (s) ==");
|
||||
System.out.println(String.format("[%.2f] [%.0f] [%8.3f] [%.3s]", 3.14159, 3.5, 3.14159, "abcdef"));
|
||||
|
||||
System.out.println("== flags ==");
|
||||
System.out.println(String.format("[%05d] [%+d] [%+d] [% d] [%(d] [%,d]", 42, 42, -42, 42, -42, 1234567));
|
||||
System.out.println(String.format("[%#x] [%#o] [%X] [%08.3f]", 255, 8, 255, -3.14159));
|
||||
|
||||
System.out.println("== argument index: reuse and reorder ==");
|
||||
System.out.println(String.format("%2$s %1$s", "world", "hello"));
|
||||
System.out.println(String.format("%1$s, %1$s and again %<s", "echo"));
|
||||
|
||||
System.out.println("== %n and %% ==");
|
||||
System.out.println(String.format("100%% sure%nnext line"));
|
||||
|
||||
System.out.println("== %s accepts anything (calls String.valueOf / toString) ==");
|
||||
System.out.println(String.format("[%s] [%s] [%s] [%S]", null, 7, java.util.List.of(1, 2), "shout"));
|
||||
|
||||
System.out.println("== %b is not 'is this the string true' ==");
|
||||
System.out.println(String.format("%b %b %b %b", "false", 0, null, Boolean.FALSE));
|
||||
|
||||
System.out.println("== %c takes a char or a code point ==");
|
||||
System.out.println(String.format("%c %c %c", 'A', 65, 0x1F600));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
/**
|
||||
* Indicative cost of String.format versus + concatenation versus StringBuilder. This is a
|
||||
* warmed-up plain loop with a sink to defeat dead-code elimination - good enough to show an
|
||||
* order of magnitude, NOT a JMH result. Numbers drift between runs and machines.
|
||||
*/
|
||||
public class ConcatBenchmarkDemo {
|
||||
|
||||
static volatile int sink;
|
||||
|
||||
public static void main(String[] args) {
|
||||
int n = 2_000_000;
|
||||
System.out.println("JDK " + System.getProperty("java.version") + ", " + n + " iterations per variant, best of 5 rounds");
|
||||
|
||||
for (int warm = 0; warm < 3; warm++) {
|
||||
run(Variant.FORMAT, 200_000);
|
||||
run(Variant.FORMATTED, 200_000);
|
||||
run(Variant.PLUS, 200_000);
|
||||
run(Variant.BUILDER, 200_000);
|
||||
}
|
||||
for (Variant v : Variant.values()) {
|
||||
long best = Long.MAX_VALUE;
|
||||
for (int round = 0; round < 5; round++) {
|
||||
best = Math.min(best, run(v, n));
|
||||
}
|
||||
System.out.println(String.format("%-10s %8.1f ns/op", v, best / (double) n));
|
||||
}
|
||||
}
|
||||
|
||||
enum Variant { PLUS, BUILDER, FORMATTED, FORMAT }
|
||||
|
||||
static long run(Variant v, int n) {
|
||||
long t0 = System.nanoTime();
|
||||
int acc = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
String s = switch (v) {
|
||||
case PLUS -> "user=" + i + " score=" + (i * 0.5);
|
||||
case BUILDER -> new StringBuilder().append("user=").append(i).append(" score=").append(i * 0.5).toString();
|
||||
case FORMATTED -> "user=%d score=%.1f".formatted(i, i * 0.5);
|
||||
case FORMAT -> String.format("user=%d score=%.1f", i, i * 0.5);
|
||||
};
|
||||
acc += s.length();
|
||||
}
|
||||
sink = acc;
|
||||
return System.nanoTime() - t0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The %t conversions format java.time values directly. They work, but they are locale-sensitive
|
||||
* and a LocalDate has no time fields, so asking it for an hour is a runtime error.
|
||||
*/
|
||||
public class DateTimeDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
LocalDateTime when = LocalDateTime.of(2026, 11, 12, 9, 5, 7);
|
||||
LocalDate day = when.toLocalDate();
|
||||
|
||||
System.out.println("== %t suffixes on a LocalDateTime ==");
|
||||
System.out.println(String.format(Locale.US, "%tF | %tT | %tR | %tD", when, when, when, when));
|
||||
System.out.println(String.format(Locale.US, "%tA, %<te %<tB %<tY", when));
|
||||
System.out.println(String.format(Locale.US, "%tI:%<tM %<Tp", when));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== the same suffixes, other locales (names are localized, numbers are not) ==");
|
||||
System.out.println(String.format(Locale.GERMANY, "%tA, %<te. %<tB %<tY", when));
|
||||
System.out.println(String.format(Locale.FRANCE, "%tA %<te %<tB %<tY", when));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== a LocalDate has no hour ==");
|
||||
try {
|
||||
System.out.println(String.format("%tH", day));
|
||||
} catch (RuntimeException e) {
|
||||
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== DateTimeFormatter does the same job with a reusable, immutable object ==");
|
||||
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd MMM uuuu HH:mm", Locale.US);
|
||||
System.out.println(f.format(when));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Format strings are checked at runtime, not compile time. Every line here compiles and then throws.
|
||||
* This prints the real exception class and message so the symptom is recognisable later.
|
||||
*/
|
||||
public class FormatErrorsDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
run("wrong type for %d", () -> String.format("%d", "42"));
|
||||
run("too few arguments", () -> String.format("%s and %s", "one"));
|
||||
run("unknown conversion", () -> String.format("%q", 1));
|
||||
run("precision on an integer", () -> String.format("%.2d", 5));
|
||||
run("flag not valid for %x ,", () -> String.format("%,x", 255));
|
||||
run("left-justify without width", () -> String.format("%-d", 5));
|
||||
run("zero pad AND left-justify", () -> String.format("%-08d", 5));
|
||||
run("width on %n", () -> String.format("%5n"));
|
||||
run("double passed to %d", () -> String.format("%d", 3.0));
|
||||
run("int passed to %f", () -> String.format("%f", 3));
|
||||
run("a stray % in user text", () -> String.format("Discount 50% off"));
|
||||
run("extra arguments (no error)", () -> String.format("%s", "a", "b", "c"));
|
||||
}
|
||||
|
||||
private static void run(String label, Supplier<String> call) {
|
||||
try {
|
||||
System.out.println(String.format("%-28s -> OK: %s", label, call.get()));
|
||||
} catch (RuntimeException e) {
|
||||
System.out.println(String.format("%-28s -> %s: %s", label, e.getClass().getSimpleName(), e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.util.Formatter;
|
||||
|
||||
/**
|
||||
* The four ways to reach the same engine: String.format, String.formatted, PrintStream.printf,
|
||||
* and java.util.Formatter. Plus the text-block pairing that makes formatted() worth using.
|
||||
*/
|
||||
public class FormattedAndPrintfDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String name = "Asha";
|
||||
int items = 3;
|
||||
double total = 1499.5;
|
||||
|
||||
System.out.println("== four spellings, one result ==");
|
||||
String a = String.format("Hi %s, %d items, Rs %.2f", name, items, total);
|
||||
String b = "Hi %s, %d items, Rs %.2f".formatted(name, items, total);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
new Formatter(sb).format("Hi %s, %d items, Rs %.2f", name, items, total);
|
||||
System.out.println(a);
|
||||
System.out.println(b);
|
||||
System.out.println(sb);
|
||||
System.out.printf("Hi %s, %d items, Rs %.2f%n", name, items, total);
|
||||
System.out.println("format == formatted == Formatter: " + (a.equals(b) && b.equals(sb.toString())));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== formatted() on a text block ==");
|
||||
String invoice = """
|
||||
Invoice for %s
|
||||
items : %5d
|
||||
total : %,10.2f
|
||||
""".formatted(name, items, total);
|
||||
System.out.print(invoice);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== printf returns the PrintStream, so calls chain ==");
|
||||
System.out.printf("%s ", "one").printf("%s ", "two").printf("%s%n", "three");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== %n vs \\n ==");
|
||||
System.out.println(String.format("a%nb").equals("a\nb") + " on this platform (line.separator is "
|
||||
+ (System.lineSeparator().equals("\n") ? "\\n" : "not \\n") + ")");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== an array argument: spread as the varargs, unless you cast it to Object ==");
|
||||
Object[] row = {"x", 1};
|
||||
String[] strings = {"p", "q"};
|
||||
System.out.println(String.format("%s-%s", row));
|
||||
System.out.println(String.format("%s-%s", strings));
|
||||
System.out.println(String.format("%s", (Object) strings).startsWith("[Ljava.lang.String;@"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Tries every built-in route to lakh/crore grouping, shows that none of them produce it,
|
||||
* then uses IndianNumberFormat.
|
||||
*/
|
||||
public class IndianGroupingDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Locale enIN = Locale.of("en", "IN");
|
||||
Locale hiIN = Locale.of("hi", "IN");
|
||||
long amount = 12_34_56_789L; // 12 crore 34 lakh 56 thousand 789
|
||||
|
||||
System.out.println("== what the JDK gives for 123456789 ==");
|
||||
System.out.println("String.format(en_IN, \"%,d\") : " + String.format(enIN, "%,d", amount));
|
||||
System.out.println("String.format(hi_IN, \"%,d\") : " + String.format(hiIN, "%,d", amount));
|
||||
System.out.println("NumberFormat.getInstance(en_IN) : " + NumberFormat.getInstance(enIN).format(amount));
|
||||
System.out.println("NumberFormat currency (en_IN) : " + NumberFormat.getCurrencyInstance(enIN).format(amount + 0.5));
|
||||
System.out.println("CLDR pattern for en_IN : " + ((DecimalFormat) NumberFormat.getInstance(enIN)).toPattern());
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== a DecimalFormat pattern that LOOKS like it should work ==");
|
||||
DecimalFormat df = new DecimalFormat("#,##,##0");
|
||||
System.out.println("pattern #,##,##0 -> grouping size : " + df.getGroupingSize());
|
||||
System.out.println("pattern #,##,##0 -> output : " + df.format(amount));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== compact formatting knows crore but drops the digits ==");
|
||||
NumberFormat compact = NumberFormat.getCompactNumberInstance(enIN, NumberFormat.Style.SHORT);
|
||||
System.out.println("compact (default rounding) : " + compact.format(amount));
|
||||
compact.setMaximumFractionDigits(2);
|
||||
System.out.println("compact (2 fraction digits) : " + compact.format(amount));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== IndianNumberFormat ==");
|
||||
for (long v : new long[]{0, 999, 1_000, 12_345, 1_23_456, 12_34_567, 12_34_56_789L, -12_34_56_789L}) {
|
||||
System.out.println(String.format("%,15d -> %s", v, IndianNumberFormat.format(v)));
|
||||
}
|
||||
System.out.println(IndianNumberFormat.format(new BigDecimal("1234567.891"), 2));
|
||||
System.out.println(IndianNumberFormat.format(new BigDecimal("-99999.995"), 2));
|
||||
System.out.println("Rs. " + IndianNumberFormat.format(1234567.5, 2));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== plugging it into a format string through %s ==");
|
||||
System.out.println(String.format("%-12s %20s", "Balance", IndianNumberFormat.format(new BigDecimal("98765432.10"), 2)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* Lakh/crore digit grouping (12,34,56,789) - the Indian system: the last three digits, then groups
|
||||
* of two. java.util.Formatter cannot produce it because its ',' flag takes ONE grouping size from
|
||||
* DecimalFormat.getGroupingSize(), and DecimalFormat keeps only the last group length of a pattern.
|
||||
* See IndianGroupingDemo for the output that proves it.
|
||||
*/
|
||||
public final class IndianNumberFormat {
|
||||
|
||||
private IndianNumberFormat() {}
|
||||
|
||||
/** Groups a whole number: 12345678 -> "1,23,45,678". */
|
||||
public static String format(long value) {
|
||||
String digits = Long.toString(value);
|
||||
boolean negative = digits.startsWith("-");
|
||||
if (negative) {
|
||||
digits = digits.substring(1);
|
||||
}
|
||||
return (negative ? "-" : "") + group(digits);
|
||||
}
|
||||
|
||||
/** Groups the integer part of a decimal and rounds the fraction HALF_UP: 1234567.891, 2 -> "12,34,567.89". */
|
||||
public static String format(BigDecimal value, int fractionDigits) {
|
||||
BigDecimal rounded = value.setScale(fractionDigits, RoundingMode.HALF_UP);
|
||||
String plain = rounded.abs().toPlainString();
|
||||
int dot = plain.indexOf('.');
|
||||
String integerPart = dot < 0 ? plain : plain.substring(0, dot);
|
||||
String fraction = dot < 0 ? "" : plain.substring(dot);
|
||||
String sign = rounded.signum() < 0 ? "-" : "";
|
||||
return sign + group(integerPart) + fraction;
|
||||
}
|
||||
|
||||
/** Convenience for doubles - goes through BigDecimal.valueOf so it rounds the way %f does. */
|
||||
public static String format(double value, int fractionDigits) {
|
||||
return format(BigDecimal.valueOf(value), fractionDigits);
|
||||
}
|
||||
|
||||
private static String group(String digits) {
|
||||
int n = digits.length();
|
||||
if (n <= 3) {
|
||||
return digits;
|
||||
}
|
||||
String head = digits.substring(0, n - 3);
|
||||
String tail = digits.substring(n - 3);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int first = head.length() % 2; // leading group of 1 or 2 digits
|
||||
if (first == 1) {
|
||||
sb.append(head.charAt(0));
|
||||
}
|
||||
for (int i = first; i < head.length(); i += 2) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
sb.append(head, i, i + 2);
|
||||
}
|
||||
return sb.append(',').append(tail).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The same format string produces different text under different locales. String.format(fmt, ...)
|
||||
* silently uses the default FORMAT locale; String.format(Locale, fmt, ...) does not.
|
||||
*/
|
||||
public class LocaleDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
double v = 1234567.891;
|
||||
System.out.println("default FORMAT locale: " + Locale.getDefault(Locale.Category.FORMAT));
|
||||
System.out.println();
|
||||
System.out.println("== %,.2f of 1234567.891 ==");
|
||||
for (Locale l : new Locale[]{Locale.US, Locale.GERMANY, Locale.FRANCE, Locale.of("en", "IN"), Locale.of("de", "CH"), Locale.ROOT}) {
|
||||
String s = String.format(l, "%,.2f", v);
|
||||
System.out.println(String.format("%-8s %s", l.toLanguageTag().isEmpty() ? "ROOT" : l.toLanguageTag(), visible(s)));
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== the bug: changing the default locale changes %.2f ==");
|
||||
Locale saved = Locale.getDefault(Locale.Category.FORMAT);
|
||||
System.out.println("default (" + saved + ") : " + String.format("%.2f", 1234.5));
|
||||
Locale.setDefault(Locale.Category.FORMAT, Locale.GERMANY);
|
||||
System.out.println("default (de_DE) : " + String.format("%.2f", 1234.5));
|
||||
System.out.println("explicit ROOT : " + String.format(Locale.ROOT, "%.2f", 1234.5));
|
||||
Locale.setDefault(Locale.Category.FORMAT, saved);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== not localized: hex, NaN, Infinity ==");
|
||||
System.out.println(String.format(Locale.GERMANY, "%x %f %f", 255, Double.NaN, Double.POSITIVE_INFINITY));
|
||||
}
|
||||
|
||||
/** Makes non-breaking and narrow no-break spaces visible so they are not mistaken for plain spaces. */
|
||||
private static String visible(String s) {
|
||||
return s.replace(' ', '~').replace(' ', '^');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
/**
|
||||
* Padding and alignment in practice: a fixed-width table. Text is left-aligned with a negative
|
||||
* width, numbers are right-aligned so the decimal points line up.
|
||||
*/
|
||||
public class ReportTableDemo {
|
||||
|
||||
record Line(String item, int qty, double price) {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
var lines = java.util.List.of(
|
||||
new Line("Notebook", 12, 45.5),
|
||||
new Line("Pen (box of 10)", 3, 120.0),
|
||||
new Line("Stapler", 1, 349.999));
|
||||
|
||||
String header = String.format("%-18s %5s %10s %12s", "Item", "Qty", "Price", "Total");
|
||||
System.out.println(header);
|
||||
System.out.println("-".repeat(header.length()));
|
||||
double grand = 0;
|
||||
for (Line l : lines) {
|
||||
double total = l.qty() * l.price();
|
||||
grand += total;
|
||||
System.out.println(String.format("%-18s %5d %10.2f %,12.2f", l.item(), l.qty(), l.price(), total));
|
||||
}
|
||||
System.out.println("-".repeat(header.length()));
|
||||
System.out.println(String.format("%-18s %5s %10s %,12.2f", "Grand total", "", "", grand));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== the same table with printf: it prints, it does not return a String ==");
|
||||
System.out.printf("%-18s|%5d|%n", "Notebook", 12);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== truncation: %.Ns cuts, width pads ==");
|
||||
System.out.println(String.format("[%-10.6s] [%10.6s]", "Pen (box of 10)", "Pen (box of 10)"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* %f rounds HALF_UP on the decimal string Double.toString produces, not on the exact binary value.
|
||||
* That is why it can disagree with C's printf and with BigDecimal(double).
|
||||
*/
|
||||
public class RoundingDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
double[] values = {0.125, 0.25, 0.35, 2.675, 1.005, 0.5, 1.5, 2.5};
|
||||
System.out.println(String.format("%-8s %-8s %-8s %-22s %s", "value", "%.1f", "%.2f", "exact binary value", "BigDecimal(HALF_EVEN,2)"));
|
||||
for (double v : values) {
|
||||
System.out.println(String.format("%-8s %-8s %-8s %-22s %s",
|
||||
v,
|
||||
String.format("%.1f", v),
|
||||
String.format("%.2f", v),
|
||||
new BigDecimal(v).toPlainString().substring(0, Math.min(22, new BigDecimal(v).toPlainString().length())),
|
||||
new BigDecimal(v).setScale(2, RoundingMode.HALF_EVEN)));
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== %.0f on halves: HALF_UP, away from zero ==");
|
||||
System.out.println(String.format("%.0f %.0f %.0f %.0f", 0.5, 1.5, 2.5, -2.5));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("== same number, three routes: %f rounds what it is given ==");
|
||||
System.out.println(String.format("double 2.675 -> %.2f", 2.675));
|
||||
System.out.println(String.format("new BigDecimal(\"2.675\") -> %.2f", new BigDecimal("2.675")));
|
||||
System.out.println(String.format("new BigDecimal(2.675) -> %.2f", new BigDecimal(2.675)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.ankurm.stringformat;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Pins every claim the post makes as an assertion, so a JDK change that breaks one fails the build. */
|
||||
class StringFormatTest {
|
||||
|
||||
@Test
|
||||
void widthFlagsAndPrecision() {
|
||||
assertEquals("[ ab] [ab ]", String.format("[%8s] [%-8s]", "ab", "ab"));
|
||||
assertEquals("00042", String.format("%05d", 42));
|
||||
assertEquals("3.14", String.format("%.2f", 3.14159));
|
||||
assertEquals("abc", String.format("%.3s", "abcdef"));
|
||||
assertEquals("world hello", String.format("%2$s %1$s", "hello", "world"));
|
||||
assertEquals("a a a", String.format("%1$s %1$s %<s", "a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formattedEqualsFormat() {
|
||||
assertEquals(String.format("%s=%d", "k", 7), "%s=%d".formatted("k", 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void booleanConversionIsTruthyForAnyNonNullNonBoolean() {
|
||||
assertEquals("true", String.format("%b", "false"));
|
||||
assertEquals("true", String.format("%b", 0));
|
||||
assertEquals("false", String.format("%b", (Object) null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void floatRoundingIsHalfUpOnTheDecimalString() {
|
||||
assertEquals("0.3", String.format("%.1f", 0.25));
|
||||
assertEquals("0.4", String.format("%.1f", 0.35)); // C printf gives 0.3 (binary value is below .35)
|
||||
assertEquals("3", String.format("%.0f", 2.5));
|
||||
assertEquals("-3", String.format("%.0f", -2.5));
|
||||
assertEquals("2.68", String.format("%.2f", 2.675));
|
||||
assertEquals("2.68", String.format("%.2f", new BigDecimal("2.675")));
|
||||
assertEquals("2.67", String.format("%.2f", new BigDecimal(2.675))); // exact binary value is 2.67499...
|
||||
assertEquals("2.67", new BigDecimal(2.675).setScale(2, java.math.RoundingMode.HALF_EVEN).toPlainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void javaCannotGroupInLakhsAndCrores() {
|
||||
Locale enIN = Locale.of("en", "IN");
|
||||
assertEquals("123,456,789", String.format(enIN, "%,d", 123456789L));
|
||||
assertEquals("123,456,789", NumberFormat.getInstance(enIN).format(123456789L));
|
||||
assertEquals(3, new DecimalFormat("#,##,##0").getGroupingSize());
|
||||
assertEquals("123,456,789", new DecimalFormat("#,##,##0").format(123456789L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indianFormatGroupsLastThreeThenTwos() {
|
||||
assertEquals("0", IndianNumberFormat.format(0));
|
||||
assertEquals("999", IndianNumberFormat.format(999));
|
||||
assertEquals("1,000", IndianNumberFormat.format(1000));
|
||||
assertEquals("1,23,456", IndianNumberFormat.format(123456));
|
||||
assertEquals("12,34,567", IndianNumberFormat.format(1234567));
|
||||
assertEquals("12,34,56,789", IndianNumberFormat.format(123456789L));
|
||||
assertEquals("-12,34,56,789", IndianNumberFormat.format(-123456789L));
|
||||
assertEquals("92,23,37,20,36,85,47,75,807", IndianNumberFormat.format(Long.MAX_VALUE));
|
||||
assertEquals("12,34,567.89", IndianNumberFormat.format(new BigDecimal("1234567.891"), 2));
|
||||
assertEquals("-1,00,000.00", IndianNumberFormat.format(new BigDecimal("-99999.995"), 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultLocaleChangesNumericOutput() {
|
||||
Locale saved = Locale.getDefault(Locale.Category.FORMAT);
|
||||
try {
|
||||
Locale.setDefault(Locale.Category.FORMAT, Locale.GERMANY);
|
||||
assertEquals("1234,50", String.format("%.2f", 1234.5));
|
||||
assertEquals("1234.50", String.format(Locale.ROOT, "%.2f", 1234.5));
|
||||
} finally {
|
||||
Locale.setDefault(Locale.Category.FORMAT, saved);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorsAreRuntimeExceptions() {
|
||||
assertThrows(IllegalFormatConversionException.class, () -> String.format("%d", "42"));
|
||||
assertThrows(IllegalFormatConversionException.class, () -> String.format("%d", 3.0));
|
||||
assertThrows(MissingFormatArgumentException.class, () -> String.format("%s %s", "one"));
|
||||
assertThrows(UnknownFormatConversionException.class, () -> String.format("%q", 1));
|
||||
assertThrows(IllegalFormatPrecisionException.class, () -> String.format("%.2d", 5));
|
||||
assertThrows(FormatFlagsConversionMismatchException.class, () -> String.format("%,x", 255));
|
||||
assertThrows(MissingFormatWidthException.class, () -> String.format("%-d", 5));
|
||||
var stray = assertThrows(MissingFormatArgumentException.class, () -> String.format("Discount 50% off"));
|
||||
assertEquals("Format specifier '% o'", stray.getMessage()); // "% o" = space flag + octal conversion
|
||||
assertEquals("a", String.format("%s", "a", "b", "c")); // extra arguments are ignored silently
|
||||
}
|
||||
|
||||
@Test
|
||||
void arraysSpreadIntoVarargsUnlessCastToObject() {
|
||||
Object[] objects = {"x", 1};
|
||||
assertEquals("x-1", String.format("%s-%s", objects));
|
||||
String[] strings = {"p", "q"};
|
||||
assertEquals("p-q", String.format("%s-%s", strings));
|
||||
assertTrue(String.format("%s", (Object) strings).startsWith("[Ljava.lang.String;@"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyFormatErrorIsAnIllegalFormatException() {
|
||||
for (Runnable bad : new Runnable[]{
|
||||
() -> String.format("%d", "42"),
|
||||
() -> String.format("%s %s", "one"),
|
||||
() -> String.format("%q", 1),
|
||||
() -> String.format("%.2d", 5),
|
||||
() -> String.format("%,x", 255),
|
||||
() -> String.format("%-d", 5),
|
||||
() -> String.format("%-08d", 5),
|
||||
() -> String.format("%5n")}) {
|
||||
assertThrows(IllegalFormatException.class, bad::run);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void dateTimeSuffixes() {
|
||||
var when = java.time.LocalDateTime.of(2026, 11, 12, 9, 5, 7);
|
||||
assertEquals("2026-11-12 | 09:05:07", String.format(Locale.US, "%tF | %<tT", when));
|
||||
assertThrows(IllegalFormatConversionException.class,
|
||||
() -> String.format("%tH", when.toLocalDate()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user