From 213890119c2b76638f28963adbf502b72b6868de Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 11 Aug 2026 23:00:14 +0200 Subject: [PATCH 01/18] Reincorporate Printf4J as AwkPrintf with AWK printf semantics Replace the external Printf4J dependency (which emulated glibc's printf()) with a new io.jawk.jrt.AwkPrintf class implementing POSIX AWK / gawk printf semantics, verified against gawk 5: - %s converts numbers with AWK's number-to-string rules: integral values print without a fractional part (fixes the "x[1.0]" symptom), and non-integral values honor the script's current CONVFMT, which is now threaded through new AwkSink.printfWithConvFmt() / sprintfWithConvFmt() methods (backward-compatible defaults). - %c prints the character of a numeric code point (including numeric strings and fields) or the first character of a string value. - Dynamic star precision (%.*f), negative star width/precision, gawk positional specifiers (%2$s), and the ' grouping flag are supported. - Out-of-range integers follow gawk: unsigned 64-bit wrapping for %u/%o/%x/%X, full decimal expansion for %d/%i beyond 64 bits, %g fallback for %u/%o/%x/%X beyond 64 bits. - NaN and infinities print as nan/inf/-inf everywhere. - %e/%f/%g round halfway cases to even via BigDecimal, matching the C library used by gawk; %g strips trailing zeros before padding. - printf with too few arguments is a fatal error, like gawk; unknown conversions (including %n and invalid length modifiers) print verbatim without consuming an argument; a single h/l/L modifier is accepted and ignored. Also stop saturating integral values beyond 2^63-1 to Long.MAX_VALUE in constant folding, int(), and number-to-string conversion: print 2^100 now prints the full decimal expansion like gawk. The complete Printf4J unit test suite is incorporated in AwkPrintfTest, including the tests that were disabled or commented out there, with expectations adapted to gawk-verified AWK semantics; PrintfTest adds script-level coverage, and the previously-skipped POSIX star width/precision conformance test is enabled. Fixes #528 Co-Authored-By: Claude Fable 5 --- pom.xml | 6 - src/main/java/io/jawk/backend/AVM.java | 9 +- .../java/io/jawk/intermediate/AwkTuples.java | 48 +- .../java/io/jawk/jrt/AppendableAwkSink.java | 8 + src/main/java/io/jawk/jrt/AwkPrintf.java | 845 ++++++++++++++++++ src/main/java/io/jawk/jrt/AwkSink.java | 89 +- src/main/java/io/jawk/jrt/JRT.java | 50 +- .../java/io/jawk/jrt/OutputStreamAwkSink.java | 5 + src/site/markdown/behavior-changes.md | 36 +- src/site/markdown/compatibility.md.vm | 21 + src/site/markdown/index.md.vm | 2 +- src/site/markdown/java-output.md | 6 + .../java/io/jawk/PosixConformanceTest.java | 1 - src/test/java/io/jawk/PrintfTest.java | 192 ++++ src/test/java/io/jawk/jrt/AwkPrintfTest.java | 778 ++++++++++++++++ 15 files changed, 2004 insertions(+), 92 deletions(-) create mode 100644 src/main/java/io/jawk/jrt/AwkPrintf.java create mode 100644 src/test/java/io/jawk/PrintfTest.java create mode 100644 src/test/java/io/jawk/jrt/AwkPrintfTest.java diff --git a/pom.xml b/pom.xml index 3112bb0d..6d63fab6 100644 --- a/pom.xml +++ b/pom.xml @@ -96,12 +96,6 @@ - - org.metricshub - printf4j - 0.9.08 - - com.github.stefanbirkner system-rules diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index 04665f9a..bb45747e 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -1785,7 +1785,7 @@ private void executeTuples(PositionTracker position) } case INTFUNC: { // stack[0] = arg to int() function - push((long) JRT.toDouble(pop())); + push(JRT.truncateToScalar(JRT.toDouble(pop()))); position.next(); break; } @@ -3003,7 +3003,7 @@ private Object invokeIndirectBuiltin( return jrt.index(jrt.toAwkString(args[0]), jrt.toAwkString(args[1])); case INT: requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber); - return Long.valueOf((long) JRT.toDouble(args[0])); + return JRT.truncateToScalar(JRT.toDouble(args[0])); case LENGTH: requireIndirectArgumentCount(builtin, args, 0, 1, lineNumber); return args.length == 0 ? Integer.valueOf(jrt.jrtGetInputField(0).toString().length()) : lengthOf(args[0]); @@ -3030,7 +3030,8 @@ private Object invokeIndirectBuiltin( requireIndirectArgumentCount(builtin, args, 1, Integer.MAX_VALUE, lineNumber); return jrt .getAwkSink() - .sprintf( + .sprintfWithConvFmt( + jrt.getCONVFMTString(), jrt.toAwkString(args[0]), Arrays.copyOfRange(args, 1, args.length)); case SQRT: @@ -3784,7 +3785,7 @@ private Object[] popArguments(long numArgs) { private String sprintfFunction(long numArgs) { Object[] argArray = popArguments(numArgs - 1); String fmt = jrt.toAwkString(pop()); - return jrt.getAwkSink().sprintf(fmt, argArray); + return jrt.getAwkSink().sprintfWithConvFmt(jrt.getCONVFMTString(), fmt, argArray); } private void setNumOnJRT(long fieldNum, double num) { diff --git a/src/main/java/io/jawk/intermediate/AwkTuples.java b/src/main/java/io/jawk/intermediate/AwkTuples.java index 31090ab2..b98e3c38 100644 --- a/src/main/java/io/jawk/intermediate/AwkTuples.java +++ b/src/main/java/io/jawk/intermediate/AwkTuples.java @@ -2383,55 +2383,37 @@ private Object foldBinary(Object left, Object right, Tuple operation) { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = d1 + d2; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case SUBTRACT: { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = d1 - d2; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case MULTIPLY: { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = d1 * d2; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case DIVIDE: { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = d1 / d2; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case MOD: { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = d1 % d2; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case POW: { double d1 = JRT.toDouble(left); double d2 = JRT.toDouble(right); double ans = Math.pow(d1, d2); - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case CMP_EQ: case CMP_LT: @@ -2462,17 +2444,11 @@ private Object foldUnary(Object literal, Tuple operation) { case NEGATE: { double value = JRT.toDouble(literal); double ans = -value; - if (JRT.isActuallyLong(ans)) { - return Long.valueOf((long) Math.rint(ans)); - } - return Double.valueOf(ans); + return JRT.toScalarNumber(ans); } case UNARY_PLUS: { double value = JRT.toDouble(literal); - if (JRT.isActuallyLong(value)) { - return Long.valueOf((long) Math.rint(value)); - } - return Double.valueOf(value); + return JRT.toScalarNumber(value); } default: return null; @@ -2488,11 +2464,11 @@ private Tuple createLiteralPush(Object value, int lineNumber) { } else if (value instanceof Double) { tuple = new Tuple.PushDoubleTuple(((Double) value).doubleValue()); } else if (value instanceof Number) { - double d = ((Number) value).doubleValue(); - if (JRT.isActuallyLong(d)) { - tuple = new Tuple.PushLongTuple((long) Math.rint(d)); + Object scalar = JRT.toScalarNumber(((Number) value).doubleValue()); + if (scalar instanceof Long) { + tuple = new Tuple.PushLongTuple(((Long) scalar).longValue()); } else { - tuple = new Tuple.PushDoubleTuple(d); + tuple = new Tuple.PushDoubleTuple(((Double) scalar).doubleValue()); } } else if (value instanceof String) { tuple = new Tuple.PushStringTuple((String) value); diff --git a/src/main/java/io/jawk/jrt/AppendableAwkSink.java b/src/main/java/io/jawk/jrt/AppendableAwkSink.java index 82930bef..16d0a0bd 100644 --- a/src/main/java/io/jawk/jrt/AppendableAwkSink.java +++ b/src/main/java/io/jawk/jrt/AppendableAwkSink.java @@ -96,6 +96,14 @@ public void printf(String ofs, String ors, String ofmt, String format, Object... } } + @Override + public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) + throws IOException { + synchronized (lock) { + appendable.append(sprintfWithConvFmt(convfmt, format, values)); + } + } + @Override public void flush() throws IOException { printStream.flush(); diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java new file mode 100644 index 00000000..9f8e265f --- /dev/null +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -0,0 +1,845 @@ +package io.jawk.jrt; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright (C) 2006 - 2026 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; +import java.text.DecimalFormatSymbols; +import java.util.IllegalFormatException; +import java.util.Locale; + +/** + * AWK's {@code printf}/{@code sprintf} formatting engine. + *

+ * This class implements the POSIX AWK formatting semantics (as implemented by + * gawk), which differ from both C's {@code printf()} and + * {@link java.lang.String#format(String, Object...)} in several ways: + *

+ *
    + *
  • {@code %s} converts numeric values to strings with AWK's number-to-string + * rules: integral values are printed without a fractional part, and other + * values are formatted with {@code CONVFMT};
  • + *
  • {@code %c} prints the character for a numeric code point, or the first + * character of a string value;
  • + *
  • {@code %i} is an alias for {@code %d}, and {@code %u} prints the value + * as an unsigned 64-bit integer;
  • + *
  • dynamic field width and precision ({@code *}) consume arguments, and + * gawk-style positional specifiers ({@code %n$}) are honored;
  • + *
  • integer conversions of values that exceed the 64-bit range fall back to + * the full decimal expansion ({@code %d}/{@code %i}) or {@code %g} notation + * ({@code %u}/{@code %o}/{@code %x}/{@code %X}), like gawk;
  • + *
  • NaN and infinities print as {@code nan}, {@code inf}, and {@code -inf};
  • + *
  • {@code %e}, {@code %f}, and {@code %g} round halfway cases to even, like + * the C library used by gawk;
  • + *
  • unknown conversion specifiers are printed verbatim without consuming an + * argument, and a fatal {@link AwkRuntimeException} is raised when there are + * not enough arguments to satisfy the format string.
  • + *
+ *

+ * This formatting logic was originally externalized in the + * Printf4J project, which + * emulated glibc's {@code printf()}. It has been reincorporated into Jawk and + * adapted to AWK's semantics. + *

+ */ +public final class AwkPrintf { + + /** Default AWK number-to-string conversion format ({@code CONVFMT}). */ + public static final String DEFAULT_CONVFMT = "%.6g"; + + /** Conversion characters recognized as AWK format specifiers. */ + private static final String CONVERSION_CHARS = "diouxXeEfFgGaAcs"; + + /** Length modifier characters accepted (and ignored) like gawk. */ + private static final String LENGTH_MODIFIERS = "hlL"; + + /** A one-character string holding the NUL character, printed by {@code %c} for empty values. */ + private static final String NUL_STRING = Character.toString((char) 0); + + /** 2^63 as a double, the first value beyond the signed 64-bit range. */ + private static final double TWO_POW_63 = 9.223372036854775808e18; + + /** 2^64 as a {@link BigInteger}, used for unsigned wrapping checks. */ + private static final BigInteger TWO_POW_64 = BigInteger.ONE.shiftLeft(64); + + private AwkPrintf() { + throw new UnsupportedOperationException(); + } + + /** + * Formats the given arguments with AWK's {@code sprintf()} semantics, using + * {@link Locale#US} and the default {@code CONVFMT} ({@code "%.6g"}). + * + * @param format AWK format string + * @param args arguments supplied after the format string + * @return the formatted text + * @throws AwkRuntimeException when there are not enough arguments to + * satisfy the format string + */ + public static String sprintf(final String format, final Object... args) { + return sprintf(Locale.US, DEFAULT_CONVFMT, format, args); + } + + /** + * Formats the given arguments with AWK's {@code sprintf()} semantics. + * + * @param locale locale used for numeric formatting (decimal separator, + * grouping separator for the {@code '} flag) + * @param convfmt number-to-string conversion format ({@code CONVFMT}) used + * by {@code %s} for non-integral numeric values + * @param format AWK format string + * @param args arguments supplied after the format string + * @return the formatted text + * @throws AwkRuntimeException when there are not enough arguments to + * satisfy the format string + */ + public static String sprintf(final Locale locale, final String convfmt, final String format, final Object... args) { + Locale actualLocale = locale == null ? Locale.US : locale; + String actualConvfmt = convfmt == null || convfmt.isEmpty() ? DEFAULT_CONVFMT : convfmt; + Object[] actualArgs = args == null ? new Object[0] : args; + return new AwkPrintfFormatter(actualLocale, actualConvfmt, format, actualArgs).format(); + } + + /** + * Converts a value to a string using AWK's number-to-string rules. + *

+ * Non-numeric values are converted with {@code toString()}. Numeric values + * holding an integral value are printed without a fractional part (using + * the full decimal expansion when the value exceeds the 64-bit range), NaN + * and infinities print as {@code nan}, {@code inf}, and {@code -inf}, and + * all other numeric values are formatted with the supplied conversion + * format ({@code CONVFMT} or {@code OFMT}). + *

+ * + * @param value value to convert + * @param conversionFormat number-to-string conversion format + * @param locale locale used for numeric formatting + * @return the AWK string value of {@code value} + */ + public static String toAwkString(final Object value, final String conversionFormat, final Locale locale) { + if (value == null) { + return ""; + } + if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { + // Preserve exact 64-bit values that a double round-trip would corrupt. + return Long.toString(((Number) value).longValue()); + } + if (!(value instanceof Number)) { + return value.toString(); + } + return numberToAwkString(((Number) value).doubleValue(), conversionFormat, locale); + } + + private static String numberToAwkString(final double number, final String conversionFormat, final Locale locale) { + if (Double.isNaN(number)) { + return "nan"; + } + if (Double.isInfinite(number)) { + return number > 0 ? "inf" : "-inf"; + } + if (JRT.isActuallyLong(number)) { + double rounded = Math.rint(number); + if (rounded >= -TWO_POW_63 && rounded < TWO_POW_63) { + return Long.toString((long) rounded); + } + return new BigDecimal(rounded).toBigInteger().toString(); // NOPMD - the exact binary value of the double is + // intended + } + String fmt = conversionFormat == null || conversionFormat.isEmpty() ? DEFAULT_CONVFMT : conversionFormat; + return sprintf(locale, DEFAULT_CONVFMT, fmt, Double.valueOf(number)); + } + + /** + * Immutable set of conversion flags parsed from one format specifier. + */ + private static final class Flags { + + private final boolean leftJustify; + private final boolean plusSign; + private final boolean spaceSign; + private final boolean zeroPad; + private final boolean alternate; + private final boolean grouping; + + Flags(boolean leftJustify, boolean plusSign, boolean spaceSign, boolean zeroPad, boolean alternate, + boolean grouping) { + this.leftJustify = leftJustify; + this.plusSign = plusSign; + this.spaceSign = spaceSign; + this.zeroPad = zeroPad; + this.alternate = alternate; + this.grouping = grouping; + } + } + + /** + * Stateful single-pass formatter for one {@code sprintf()} call. + */ + private static final class AwkPrintfFormatter { + + private final Locale locale; + private final String convfmt; + private final String format; + private final Object[] args; + private final StringBuilder out; + + /** Index of the next sequential argument to consume. */ + private int argIndex; + + AwkPrintfFormatter(Locale locale, String convfmt, String format, Object[] args) { + this.locale = locale; + this.convfmt = convfmt; + this.format = format; + this.args = args; + this.out = new StringBuilder(format.length() + 16); + } + + String format() { + int length = format.length(); + int i = 0; + while (i < length) { + char c = format.charAt(i); + if (c != '%') { + out.append(c); + i++; + continue; + } + if (i + 1 >= length) { + // Dangling '%' at the end of the format: print it verbatim. + out.append('%'); + break; + } + if (format.charAt(i + 1) == '%') { + out.append('%'); + i += 2; + continue; + } + i = formatSpecifier(i); + } + return out.toString(); + } + + /** + * Parses and renders one format specifier starting at {@code start} + * (which points at the {@code '%'}), and returns the index of the + * first character after the specifier. + */ + private int formatSpecifier(int start) { + int length = format.length(); + int i = start + 1; + + // gawk-style positional specifier: %n$... + int argPosition = 0; + int digitsEnd = i; + while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) { + digitsEnd++; + } + if (digitsEnd > i && digitsEnd < length && format.charAt(digitsEnd) == '$') { + argPosition = parseInt(format, i, digitsEnd); + i = digitsEnd + 1; + } + + // Flags, in any order and possibly repeated. + boolean leftJustify = false; + boolean plusSign = false; + boolean spaceSign = false; + boolean zeroPad = false; + boolean alternate = false; + boolean grouping = false; + flagLoop: while (i < length) { + switch (format.charAt(i)) { + case '-': + leftJustify = true; + break; + case '+': + plusSign = true; + break; + case ' ': + spaceSign = true; + break; + case '0': + zeroPad = true; + break; + case '#': + alternate = true; + break; + case '\'': + grouping = true; + break; + default: + break flagLoop; + } + i++; + } + + // Field width: digits, or '*' (optionally '*n$'). + int width = -1; + if (i < length && format.charAt(i) == '*') { + i++; + int starArgEnd = starPositionEnd(i); + long dynamicWidth; + if (starArgEnd > i) { + dynamicWidth = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); + i = starArgEnd; + } else { + dynamicWidth = (long) JRT.toDouble(nextArg()); + } + if (dynamicWidth < 0) { + leftJustify = true; + dynamicWidth = -dynamicWidth; + } + width = (int) Math.min(dynamicWidth, Integer.MAX_VALUE); + } else { + int widthEnd = i; + while (widthEnd < length && isAsciiDigit(format.charAt(widthEnd))) { + widthEnd++; + } + if (widthEnd > i) { + width = parseInt(format, i, widthEnd); + i = widthEnd; + } + } + + // Precision: '.' followed by digits (empty means 0), or '.*'. + int precision = -1; + if (i < length && format.charAt(i) == '.') { + i++; + if (i < length && format.charAt(i) == '*') { + i++; + int starArgEnd = starPositionEnd(i); + long dynamicPrecision; + if (starArgEnd > i) { + dynamicPrecision = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); + i = starArgEnd; + } else { + dynamicPrecision = (long) JRT.toDouble(nextArg()); + } + // A negative dynamic precision means "no precision" in C. + if (dynamicPrecision >= 0) { + precision = (int) Math.min(dynamicPrecision, Integer.MAX_VALUE); + } + } else { + int precisionEnd = i; + while (precisionEnd < length && isAsciiDigit(format.charAt(precisionEnd))) { + precisionEnd++; + } + precision = precisionEnd == i ? 0 : parseInt(format, i, precisionEnd); + i = precisionEnd; + } + } + + // A single length modifier (h, l, or L) is accepted and ignored, + // like gawk. Doubled modifiers such as "ll" or "hh" make the + // whole specifier invalid, also like gawk. + if (i < length && LENGTH_MODIFIERS.indexOf(format.charAt(i)) >= 0) { + i++; + } + + if (i >= length || CONVERSION_CHARS.indexOf(format.charAt(i)) < 0) { + // Unknown or unterminated conversion: print the specifier + // verbatim (including the offending character) without + // consuming an argument, like gawk. + int end = i < length ? i + 1 : length; + out.append(format, start, end); + return end; + } + + char conversion = format.charAt(i); + i++; + + Flags flags = new Flags(leftJustify, plusSign, spaceSign, zeroPad, alternate, grouping); + Object arg = argPosition > 0 ? argAt(argPosition) : nextArg(); + render(conversion, flags, width, precision, arg); + return i; + } + + /** + * Returns the index right after a {@code n$} sequence starting at + * {@code i}, or {@code i} when there is no such sequence. + */ + private int starPositionEnd(int i) { + int length = format.length(); + int digitsEnd = i; + while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) { + digitsEnd++; + } + if (digitsEnd > i && digitsEnd < length && format.charAt(digitsEnd) == '$') { + return digitsEnd + 1; + } + return i; + } + + private Object nextArg() { + if (argIndex >= args.length) { + throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); + } + return args[argIndex++]; + } + + private Object argAt(int position) { + if (position <= 0 || position > args.length) { + throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); + } + return args[position - 1]; + } + + private void render(char conversion, Flags flags, int width, int precision, Object arg) { + switch (conversion) { + case 'c': + appendPadded(characterOf(arg), flags.leftJustify, false, width); + break; + case 's': + String s = toAwkString(arg, convfmt, locale); + if (precision >= 0 && s.length() > precision) { + s = s.substring(0, precision); + } + appendPadded(s, flags.leftJustify, false, width); + break; + case 'd': + case 'i': + renderSignedInteger(flags, width, precision, arg); + break; + case 'u': + case 'o': + case 'x': + case 'X': + renderUnsignedInteger(conversion, flags, width, precision, arg); + break; + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': + case 'a': + case 'A': + renderFloat(conversion, flags, width, precision, arg); + break; + default: + // Unreachable: the caller only passes known conversions. + break; + } + } + + /** Renders the {@code %c} character for the given argument. */ + private String characterOf(Object arg) { + if (arg == null) { + return NUL_STRING; + } + boolean numeric = arg instanceof Number || (arg instanceof StrNum && ((StrNum) arg).isNumber()); + if (numeric) { + long code = (long) JRT.toDouble(arg); + StringBuilder sb = new StringBuilder(2); + if (code >= 0 && code <= Character.MAX_CODE_POINT) { + sb.appendCodePoint((int) code); + } else { + sb.append((char) code); + } + return sb.toString(); + } + String s = arg.toString(); + if (s.isEmpty()) { + return NUL_STRING; + } + StringBuilder sb = new StringBuilder(2); + sb.appendCodePoint(s.codePointAt(0)); + return sb.toString(); + } + + private void renderSignedInteger(Flags flags, int width, int precision, Object arg) { + double d = JRT.toDouble(arg); + if (renderNonFinite('d', flags, width, d)) { + return; + } + + boolean negative; + String magnitude; + if (arg instanceof Long || arg instanceof Integer || arg instanceof Short || arg instanceof Byte) { + long v = ((Number) arg).longValue(); + negative = v < 0; + magnitude = negative ? Long.toUnsignedString(-v) : Long.toString(v); + } else if (d >= -TWO_POW_63 && d < TWO_POW_63) { + long v = (long) d; + negative = v < 0; + magnitude = negative ? Long.toUnsignedString(-v) : Long.toString(v); + } else { + // Out of 64-bit range: print the full decimal expansion of the + // (integral) double, like gawk. + BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD - the exact binary value of the double is intended + negative = bi.signum() < 0; + magnitude = bi.abs().toString(); + } + + String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : ""; + appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude)); + } + + private void renderUnsignedInteger(char conversion, Flags flags, int width, int precision, Object arg) { + double d = JRT.toDouble(arg); + if (renderNonFinite(conversion, flags, width, d)) { + return; + } + + int radix = conversion == 'o' ? 8 : conversion == 'u' ? 10 : 16; + String magnitude; + if (arg instanceof Long || arg instanceof Integer || arg instanceof Short || arg instanceof Byte) { + magnitude = Long.toUnsignedString(((Number) arg).longValue(), radix); + } else if (d >= -TWO_POW_63 && d < TWO_POW_63) { + magnitude = Long.toUnsignedString((long) d, radix); + } else { + BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD - the exact binary value of the double is intended + if (bi.signum() >= 0 && bi.compareTo(TWO_POW_64) < 0) { + magnitude = bi.toString(radix); + } else { + // Out of the unsigned 64-bit range: fall back to %g + // notation, like gawk. + appendPadded( + floatBody('g', new Flags(false, false, false, false, false, false), -1, d), + flags.leftJustify, + false, + width); + return; + } + } + if (conversion == 'X') { + magnitude = magnitude.toUpperCase(Locale.ROOT); + } + + boolean zeroMagnitude = isZeroMagnitude(magnitude); + int actualPrecision = precision; + if (flags.alternate && zeroMagnitude && precision == 0 && conversion != 'u') { + // gawk prints "0" for a zero value with '#' and an explicit + // zero precision on %o, %x, and %X. + actualPrecision = 1; + } + String prefix = ""; + if (flags.alternate && !zeroMagnitude) { + if (conversion == 'x') { + prefix = "0x"; + } else if (conversion == 'X') { + prefix = "0X"; + } + } + if (flags.alternate + && conversion == 'o' + && !magnitude.startsWith("0") + && (precision < 0 || precision <= magnitude.length())) { + // '#' with %o forces one leading zero unless the precision + // already provides it. + magnitude = "0" + magnitude; + } + appendInteger("", prefix, magnitude, flags, width, actualPrecision, zeroMagnitude); + } + + /** + * Applies precision, grouping, and width to an integer body and + * appends it to the output. + */ + private void appendInteger( + String sign, + String prefix, + String magnitude, + Flags flags, + int width, + int precision, + boolean zeroMagnitude) { + String digits = magnitude; + if (precision == 0 && zeroMagnitude) { + // C: a zero value with an explicit zero precision prints no + // characters. gawk drops the sign flags as well. + appendPadded("", flags.leftJustify, false, width); + return; + } + if (precision > digits.length()) { + digits = zeros(precision - digits.length()) + digits; + } + if (flags.grouping) { + digits = groupDigits(digits); + } + String body = sign + prefix + digits; + if (width > body.length() && flags.zeroPad && !flags.leftJustify && precision < 0) { + // Zero padding goes between the sign/prefix and the digits. + out.append(sign).append(prefix); + out.append(zeros(width - body.length())); + out.append(digits); + return; + } + appendPadded(body, flags.leftJustify, false, width); + } + + private void renderFloat(char conversion, Flags flags, int width, int precision, Object arg) { + double d = JRT.toDouble(arg); + if (renderNonFinite(conversion, flags, width, d)) { + return; + } + String body = floatBody(conversion, flags, precision, d); + if (body == null) { + return; + } + boolean negative = d < 0 || (d == 0 && Double.doubleToRawLongBits(d) != 0L); + String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : ""; + String magnitude = body.startsWith("-") ? body.substring(1) : body; + String full = sign + magnitude; + if (width > full.length() && flags.zeroPad && !flags.leftJustify) { + out.append(sign); + out.append(zeros(width - full.length())); + out.append(magnitude); + return; + } + appendPadded(full, flags.leftJustify, false, width); + } + + /** + * Renders the digits of a finite double for a floating-point + * conversion, without sign and without width padding. The result + * carries a leading '-' only for {@code %a} (which is delegated to + * Java); all other conversions format the absolute value. + */ + private String floatBody(char conversion, Flags flags, int precision, double d) { + double abs = Math.abs(d); + switch (conversion) { + case 'f': + case 'F': { + int p = precision < 0 ? 6 : precision; + String s = decimalString(new BigDecimal(abs).setScale(p, RoundingMode.HALF_EVEN)); // NOPMD - exact binary value + // intended + if (flags.alternate && p == 0) { + s = s + "."; + } + if (flags.grouping) { + s = groupDigits(s); + } + return s; + } + case 'e': + case 'E': { + int p = precision < 0 ? 6 : precision; + String s = scientific(abs, p); + if (flags.alternate && p == 0) { + s = s.replace("e", ".e"); + } + return conversion == 'E' ? s.toUpperCase(Locale.ROOT) : s; + } + case 'g': + case 'G': { + int p = precision < 0 ? 6 : precision == 0 ? 1 : precision; + String s = generalFloat(abs, p, flags.alternate); + return conversion == 'G' ? s.toUpperCase(Locale.ROOT) : s; + } + case 'a': + case 'A': + default: { + // %a is C-library dependent in gawk; delegate to Java's + // hexadecimal float notation. + StringBuilder spec = new StringBuilder("%"); + if (precision >= 0) { + spec.append('.').append(precision); + } + spec.append(conversion); + try { + String s = String.format(locale, spec.toString(), Double.valueOf(abs)); + return s; + } catch (IllegalFormatException e) { + out.append(spec); + return null; + } + } + } + } + + /** Formats {@code abs >= 0} in C's {@code %e} notation. */ + private String scientific(double abs, int precision) { + BigDecimal mantissa; + int exponent; + if (abs == 0) { + mantissa = BigDecimal.ZERO.setScale(precision); + exponent = 0; + } else { + // The exact binary value of the double is intended: this is what makes + // rounding match the C library used by gawk. + BigDecimal rounded = new BigDecimal(abs) // NOPMD + .round(new MathContext(precision + 1, RoundingMode.HALF_EVEN)); + exponent = rounded.precision() - rounded.scale() - 1; + mantissa = rounded.movePointLeft(exponent).setScale(precision, RoundingMode.UNNECESSARY); + } + return decimalString(mantissa) + "e" + (exponent < 0 ? "-" : "+") + exponentDigits(Math.abs(exponent)); + } + + /** Formats {@code abs >= 0} in C's {@code %g} notation. */ + private String generalFloat(double abs, int precision, boolean alternate) { + if (abs == 0) { + return alternate ? "0." + zeros(precision - 1) : "0"; + } + BigDecimal rounded = new BigDecimal(abs).round(new MathContext(precision, RoundingMode.HALF_EVEN)); // NOPMD - + // exact + // binary + // value + // intended + int exponent = rounded.precision() - rounded.scale() - 1; + if (exponent >= -4 && exponent < precision) { + String s = decimalString(rounded.setScale(precision - 1 - exponent, RoundingMode.UNNECESSARY)); + return alternate ? s : stripTrailingFractionZeros(s); + } + String mantissa = decimalString( + rounded.movePointLeft(exponent).setScale(precision - 1, RoundingMode.UNNECESSARY)); + if (!alternate) { + mantissa = stripTrailingFractionZeros(mantissa); + } + return mantissa + "e" + (exponent < 0 ? "-" : "+") + exponentDigits(Math.abs(exponent)); + } + + /** + * Renders NaN and infinities for any numeric conversion, honoring the + * sign flags and field width, and returns {@code true} when the value + * was such a special value. + */ + private boolean renderNonFinite(char conversion, Flags flags, int width, double d) { + if (!Double.isNaN(d) && !Double.isInfinite(d)) { + return false; + } + String body; + if (Double.isNaN(d)) { + body = flags.plusSign ? "+nan" : flags.spaceSign ? " nan" : "nan"; + } else if (d > 0) { + body = flags.plusSign ? "+inf" : flags.spaceSign ? " inf" : "inf"; + } else { + body = "-inf"; + } + if (isUpperCaseConversion(conversion)) { + body = body.toUpperCase(Locale.ROOT); + } + // The zero flag is ignored for non-finite values, like C. + appendPadded(body, flags.leftJustify, false, width); + return true; + } + + /** Renders a {@link BigDecimal} using the locale's decimal separator. */ + private String decimalString(BigDecimal value) { + String s = value.toPlainString(); + char decimalSeparator = DecimalFormatSymbols.getInstance(locale).getDecimalSeparator(); + return decimalSeparator == '.' ? s : s.replace('.', decimalSeparator); + } + + /** Inserts locale grouping separators into the integer part of {@code s}. */ + private String groupDigits(String s) { + char groupingSeparator = DecimalFormatSymbols.getInstance(locale).getGroupingSeparator(); + char decimalSeparator = DecimalFormatSymbols.getInstance(locale).getDecimalSeparator(); + int end = s.indexOf(decimalSeparator); + if (end < 0) { + end = s.length(); + } + StringBuilder sb = new StringBuilder(s.length() + 8); + for (int i = 0; i < end; i++) { + sb.append(s.charAt(i)); + int remaining = end - 1 - i; + if (remaining > 0 && remaining % 3 == 0 && isAsciiDigit(s.charAt(i))) { + sb.append(groupingSeparator); + } + } + sb.append(s, end, s.length()); + return sb.toString(); + } + + private String stripTrailingFractionZeros(String s) { + char decimalSeparator = DecimalFormatSymbols.getInstance(locale).getDecimalSeparator(); + if (s.indexOf(decimalSeparator) < 0) { + return s; + } + int end = s.length(); + while (end > 0 && s.charAt(end - 1) == '0') { + end--; + } + if (end > 0 && s.charAt(end - 1) == decimalSeparator) { + end--; + } + return s.substring(0, end); + } + + private void appendPadded(String body, boolean leftJustify, boolean zeroPad, int width) { + if (width <= body.length()) { + out.append(body); + return; + } + int padLength = width - body.length(); + if (leftJustify) { + out.append(body); + appendSpaces(padLength); + } else if (zeroPad) { + out.append(zeros(padLength)).append(body); + } else { + appendSpaces(padLength); + out.append(body); + } + } + + private void appendSpaces(int count) { + for (int i = 0; i < count; i++) { + out.append(' '); + } + } + } + + /** Renders an exponent value with at least two digits, like C. */ + private static String exponentDigits(int exponent) { + String digits = Integer.toString(exponent); + return digits.length() < 2 ? "0" + digits : digits; + } + + private static boolean isUpperCaseConversion(char conversion) { + return conversion == 'X' || conversion == 'E' || conversion == 'F' || conversion == 'G' || conversion == 'A'; + } + + private static boolean isZeroMagnitude(String magnitude) { + for (int i = 0; i < magnitude.length(); i++) { + if (magnitude.charAt(i) != '0') { + return false; + } + } + return true; + } + + private static boolean isAsciiDigit(char c) { + return c >= '0' && c <= '9'; + } + + private static int parseInt(String s, int from, int to) { + long value = 0; + for (int i = from; i < to; i++) { + value = value * 10 + s.charAt(i) - '0'; + if (value > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + } + return (int) value; + } + + private static String zeros(int count) { + StringBuilder sb = new StringBuilder(Math.max(count, 0)); + for (int i = 0; i < count; i++) { + sb.append('0'); + } + return sb.toString(); + } +} diff --git a/src/main/java/io/jawk/jrt/AwkSink.java b/src/main/java/io/jawk/jrt/AwkSink.java index 006ca530..bd7d3324 100644 --- a/src/main/java/io/jawk/jrt/AwkSink.java +++ b/src/main/java/io/jawk/jrt/AwkSink.java @@ -27,7 +27,6 @@ import java.io.PrintStream; import java.math.BigDecimal; import java.util.Locale; -import org.metricshub.printf4j.Printf4J; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** @@ -92,6 +91,29 @@ public final Locale getLocale() { public abstract void printf(String ofs, String ors, String ofmt, String format, Object... values) throws IOException; + /** + * Writes one AWK {@code printf} operation, with the current {@code CONVFMT} + * value so that {@code %s} can convert numeric values the way AWK does. + *

+ * The default implementation ignores {@code convfmt} and delegates to + * {@link #printf(String, String, String, String, Object...)}, which keeps + * existing custom sinks working unchanged. The built-in sinks override this + * method so that {@code %s} honors the script's {@code CONVFMT} value. + *

+ * + * @param ofs output field separator + * @param ors output record separator + * @param ofmt numeric output format available to the sink + * @param convfmt number-to-string conversion format ({@code CONVFMT}) + * @param format format string passed to {@code printf} + * @param values arguments supplied after the format string + * @throws IOException if the sink cannot write the output + */ + public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) + throws IOException { + printf(ofs, ors, ofmt, format, values); + } + /** * Flushes any buffered output held by this sink. * @@ -282,13 +304,14 @@ protected final Object normalizePrintArgument(Object value) { } /** - * Formats a string in the same way as AWK's {@code sprintf()} built-in. + * Formats a string in the same way as AWK's {@code sprintf()} built-in, + * using the default {@code CONVFMT} value ({@code "%.6g"}). *

- * Subclasses may override this method to customize formatting. The default - * implementation delegates to {@link Printf4J#sprintf(Locale, String, Object...)}. - * Because {@link #printf(String, String, String, String, Object...)} uses this - * method internally, overriding it ensures that both {@code printf} and - * {@code sprintf} produce consistent output. + * The default implementation delegates to + * {@link #sprintfWithConvFmt(String, String, Object...)}. To customize + * formatting for both {@code printf} and {@code sprintf}, override + * {@link #sprintfWithConvFmt(String, String, Object...)}, which is the + * method the runtime invokes. *

* * @param format format string @@ -296,8 +319,29 @@ protected final Object normalizePrintArgument(Object value) { * @return formatted text */ public String sprintf(String format, Object... values) { + return sprintfWithConvFmt(AwkPrintf.DEFAULT_CONVFMT, format, values); + } + + /** + * Formats a string in the same way as AWK's {@code sprintf()} built-in, + * converting numeric {@code %s} operands with the supplied {@code CONVFMT} + * value. + *

+ * Subclasses may override this method to customize formatting. The default + * implementation delegates to + * {@link AwkPrintf#sprintf(Locale, String, String, Object...)}. Because the + * runtime routes both {@code printf} and {@code sprintf} through this + * method, overriding it ensures that both produce consistent output. + *

+ * + * @param convfmt number-to-string conversion format ({@code CONVFMT}) + * @param format format string + * @param values arguments supplied after the format string + * @return formatted text + */ + public String sprintfWithConvFmt(String convfmt, String format, Object... values) { Object[] safeValues = values == null ? new Object[0] : values; - return Printf4J.sprintf(locale, format, safeValues); + return AwkPrintf.sprintf(locale, convfmt, format, safeValues); } /** @@ -320,33 +364,6 @@ protected final String formatPrintfResult(String format, Object... values) { * @return textual output for {@code value} */ public static String formatOutputValue(Object value, String ofmt, Locale locale) { - if (value == null) { - return ""; - } - if (!(value instanceof Number)) { - return value.toString(); - } - - double number = ((Number) value).doubleValue(); - if (JRT.isActuallyLong(number)) { - return Long.toString((long) Math.rint(number)); - } - - try { - String rendered = String.format(locale, ofmt, number); - if ((rendered.indexOf('.') > -1 || rendered.indexOf(',') > -1) - && rendered.indexOf('e') == -1 - && rendered.indexOf('E') == -1) { - while (rendered.endsWith("0")) { - rendered = rendered.substring(0, rendered.length() - 1); - } - if (rendered.endsWith(".") || rendered.endsWith(",")) { - rendered = rendered.substring(0, rendered.length() - 1); - } - } - return rendered; - } catch (java.util.UnknownFormatConversionException e) { - return ""; - } + return AwkPrintf.toAwkString(value, ofmt, locale); } } diff --git a/src/main/java/io/jawk/jrt/JRT.java b/src/main/java/io/jawk/jrt/JRT.java index 2337f735..f1459f8a 100644 --- a/src/main/java/io/jawk/jrt/JRT.java +++ b/src/main/java/io/jawk/jrt/JRT.java @@ -662,6 +662,45 @@ public static boolean isActuallyLong(double d) { return Math.abs(d - r) < Math.ulp(d); } + /** 2^63 as a double: the first value beyond the signed 64-bit range. */ + private static final double TWO_POW_63 = 9.223372036854775808e18; + + /** + * Converts a computed double to the canonical AWK scalar: a {@link Long} + * when the value is integral and representable as a signed 64-bit integer, + * and the {@link Double} itself otherwise. Values beyond the 64-bit range + * stay doubles so they are not silently saturated to + * {@link Long#MAX_VALUE}. + * + * @param d the computed value + * @return {@code d} as a {@link Long} when exactly representable, or as a + * {@link Double} + */ + public static Object toScalarNumber(double d) { + if (isActuallyLong(d)) { + double rounded = Math.rint(d); + if (rounded >= -TWO_POW_63 && rounded < TWO_POW_63) { + return Long.valueOf((long) rounded); + } + } + return Double.valueOf(d); + } + + /** + * Truncates a double toward zero, as AWK's {@code int()} does, returning a + * {@link Long} when the result is representable and a {@link Double} + * otherwise. + * + * @param d the value to truncate + * @return the truncated value as a canonical AWK scalar + */ + public static Object truncateToScalar(double d) { + if (Double.isNaN(d) || Double.isInfinite(d)) { + return Double.valueOf(d); + } + return toScalarNumber(d < 0 ? Math.ceil(d) : Math.floor(d)); + } + /** * Convert a String, Long, or Double to Long. * @@ -855,10 +894,7 @@ public static Object toJavaScalar(Object value) { return value.toString(); } if (value instanceof Double || value instanceof Float) { - double number = ((Number) value).doubleValue(); - if (isActuallyLong(number)) { - return Long.valueOf((long) Math.rint(number)); - } + return toScalarNumber(((Number) value).doubleValue()); } return value; } @@ -2484,7 +2520,7 @@ public void printToProcess(String cmd, Object[] values) throws IOException { * @throws IOException if the sink cannot be written to */ public void printfDefault(String format, Object[] values) throws IOException { - awkSink.printf(ofs, ors, ofmt, format, values); + awkSink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); } /** @@ -2499,7 +2535,7 @@ public void printfDefault(String format, Object[] values) throws IOException { public void printfToFile(String fileNameParam, boolean append, String format, Object[] values) throws IOException { AwkSink sink = getFileAwkSink(fileNameParam, append); - sink.printf(ofs, ors, ofmt, format, values); + sink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); } /** @@ -2512,7 +2548,7 @@ public void printfToFile(String fileNameParam, boolean append, String format, Ob */ public void printfToProcess(String cmd, String format, Object[] values) throws IOException { AwkSink sink = getPipeAwkSink(cmd); - sink.printf(ofs, ors, ofmt, format, values); + sink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); sink.flush(); } diff --git a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java index 7b35a67e..e89ed0b6 100644 --- a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java +++ b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java @@ -102,6 +102,11 @@ public void printf(String ofs, String ors, String ofmt, String format, Object... printStream.print(formatPrintfResult(format, values)); } + @Override + public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { + printStream.print(sprintfWithConvFmt(convfmt, format, values)); + } + @Override public void flush() { printStream.flush(); diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index 80230edb..cad6d6f8 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -20,7 +20,41 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. ## Unreleased -_No user-visible behavior changes recorded yet._ +- `printf` and `sprintf` are now implemented natively with POSIX AWK / gawk semantics instead of + delegating to the Printf4J library, which emulated glibc's `printf()` + ([#528](https://github.com/jawkio/jawk/issues/528)): + - `%s` converts numeric values with AWK's number-to-string rules: integral values print + without a fractional part (`printf "%s", i` after `i++` now prints `1`, not `1.0`), and + non-integral values honor the script's current `CONVFMT` value. + - `%c` prints the character for a numeric code point (`printf "%c", 65` prints `A`, + previously `A` only for literal numbers, not for numeric strings or fields), or the first + character of a string value. + - Dynamic precision (`%.*f`) is now supported in addition to dynamic width (`%*d`), including + negative values (negative width left-justifies, negative precision means no precision), as + are gawk positional specifiers (`%2$s`) and the `'` grouping flag (`%'d`). + - Out-of-range integer conversions follow gawk: negative values wrap to unsigned 64-bit for + `%u`/`%o`/`%x`/`%X`, values beyond 64 bits print the full decimal expansion for `%d`/`%i` + and fall back to `%g` notation for `%u`/`%o`/`%x`/`%X`. + - NaN and infinities print as `nan`, `inf`, and `-inf` (previously Java's `NaN` / + `Infinity`), in `print`, `printf`, and number-to-string conversions. + - `%e`, `%f`, and `%g` round halfway cases to even like the C library used by gawk + (`printf "%.0f", 2.5` prints `2`, previously `3`), and `%g` strips trailing zeros before + padding (previously only when no padding applied). + - `printf` with too few arguments is now a fatal error, as in gawk (previously the leftover + specifiers were printed verbatim). + - Unknown conversion specifiers (including `%n`, which Printf4J turned into a newline, and + invalid length modifiers such as `ll` or `hh`) are printed verbatim without consuming an + argument, as in gawk; a single `h`, `l`, or `L` length modifier is accepted and ignored. +- Integral values beyond the 64-bit range are no longer saturated to 2^63-1: `print 2^100` now + prints the full decimal expansion `1267650600228229401496703205376` (previously + `9223372036854775807`), and `int()` preserves such values + ([#528](https://github.com/jawkio/jawk/issues/528)). +- For Java embedders: `AwkSink` gains `printfWithConvFmt(...)` and `sprintfWithConvFmt(...)`, + which receive the script's current `CONVFMT` value; the runtime now routes `printf` and + `sprintf` through these methods. Custom sinks that overrode `sprintf(String, Object...)` to + customize formatting should override `sprintfWithConvFmt(String, String, Object...)` instead. + The `org.metricshub:printf4j` dependency has been removed; its formatting logic now lives in + `io.jawk.jrt.AwkPrintf` ([#528](https://github.com/jawkio/jawk/issues/528)). ## [v7.0.01](https://github.com/jawkio/jawk/releases/tag/v7.0.01) (2026-07-31) diff --git a/src/site/markdown/compatibility.md.vm b/src/site/markdown/compatibility.md.vm index 8b1ff339..d8977aaf 100644 --- a/src/site/markdown/compatibility.md.vm +++ b/src/site/markdown/compatibility.md.vm @@ -188,6 +188,27 @@ The date and time functions (`mktime()`, `strftime()`) follow the Java platform' - A positive `mktime()` DST hint applies the zone's current daylight adjustment; zones without daylight saving time ignore the hint. - `strftime()`'s `%Z` prints the zone's current designation (the JDK's time zone data records historical offsets and DST rules, but not historical zone names), and timestamps before the common era use the JDK's year numbering rather than astronomical (negative) years. +${esc.h}${esc.h}${esc.h} printf and sprintf formatting + +`printf` and `sprintf` implement the POSIX AWK conversions with gawk's semantics: `%s` converts +numbers with `CONVFMT` (integral values print without a fractional part), `%c` prints the +character of a numeric code point or the first character of a string, `%i` is an alias for `%d`, +dynamic `*` width and precision consume arguments (a negative width left-justifies, a negative +precision means no precision), gawk positional specifiers (`%2$s`) and the `'` grouping flag are +honored, out-of-range integers wrap or fall back exactly as in gawk, halfway cases round to even +(`printf "%.0f", 2.5` prints `2`), too few arguments is a fatal error, and unknown conversion +specifiers print verbatim without consuming an argument. + +A few edge cases follow Java's platform rules rather than the C library's: + +- `%c` is locale-independent: a numeric argument selects the Unicode code point (so + `printf "%c", 233` always prints `é`), where C-locale gawk emits the raw byte. Values that are + not valid code points are truncated to a UTF-16 char. +- `%a`/`%A` use Java's hexadecimal floating-point notation (`0x1.0p0` where glibc prints + `0x1p+0`); gawk itself documents these conversions as C-library dependent. +- NaN always prints as `nan`: Java does not track the sign of NaN, so gawk's occasional `-nan` + is rendered without a sign. + ${esc.h}${esc.h}${esc.h} Range patterns Range patterns (`begpat, endpat`) evaluate their two conditions lazily, as POSIX requires: the start condition is evaluated only while outside the range, and the end condition only once the range has started — including on the very record that starts it, so a range can begin and end on the same record. Conditions with side effects, such as `a++ == 2, a++ == 5`, therefore behave exactly as in gawk and One True Awk: each condition's side effects run only on the records where that condition is actually tested. diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm index 47dd07de..2e27790b 100644 --- a/src/site/markdown/index.md.vm +++ b/src/site/markdown/index.md.vm @@ -119,7 +119,7 @@ Differences with Traditional AWK Jawk aims to be a practical AWK implementation for JVM environments, but it is not a byte-for-byte clone of every historical AWK behavior. Some differences are deliberate and come from the way Jawk integrates with Java: - Regular expression behavior follows Java's regex engine, which may differ from traditional AWK regexes in edge cases. Notably, alternation picks the first matching branch rather than the POSIX longest one, which can affect `match()`, field splitting with `patsplit()`, and similar content-driven matching: order alternatives longest-first. -- `printf()` and `sprintf()` try to replicate C-style formatting but may have differences due to Java's formatting capabilities and limitations. +- `printf()` and `sprintf()` implement the POSIX AWK conversions with gawk's semantics, including `CONVFMT`-based `%s` conversion, dynamic `*` width and precision, and gawk's out-of-range integer handling; only a few edge cases (`%a` notation, `%c` locale independence, the sign of NaN) follow Java's platform rules. - Some floating-point edge cases may differ due to Java's handling of floating-point arithmetic and representation. - Jawk resolves user-defined function calls during compilation. It does not defer all of that work to runtime. - The date and time functions (`mktime()`, `strftime()`) follow the Java platform's time zone data and calendar rules rather than the C library's, which differs from gawk in a few edge cases. diff --git a/src/site/markdown/java-output.md b/src/site/markdown/java-output.md index 173b8580..d98ed45c 100644 --- a/src/site/markdown/java-output.md +++ b/src/site/markdown/java-output.md @@ -100,8 +100,14 @@ public final class CollectingSink extends AwkSink { > | `ofs` | `OFS` | Output Field Separator, inserted between values | > | `ors` | `ORS` | Output Record Separator, appended after the record | > | `ofmt` | `OFMT` | Default numeric output format | +> | `convfmt` | `CONVFMT` | Number-to-string conversion format used by `%s` (only in `printfWithConvFmt(...)`) | > | `format` | — | The AWK format string | > | `values` | — | The AWK values to be formatted | +> +> The runtime invokes `printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values...)`, whose +> default implementation drops `convfmt` and delegates to `printf(...)`, so existing sinks keep +> working. Override `printfWithConvFmt(...)` when your sink formats output itself and should +> honor the script's `CONVFMT` value. ### getPrintStream diff --git a/src/test/java/io/jawk/PosixConformanceTest.java b/src/test/java/io/jawk/PosixConformanceTest.java index aa8cc54e..3d5004d3 100644 --- a/src/test/java/io/jawk/PosixConformanceTest.java +++ b/src/test/java/io/jawk/PosixConformanceTest.java @@ -685,7 +685,6 @@ public void posix93PrintfPercentCUsesFirstCharacter() throws Exception { @Test public void posix94PrintfStarWidthPrecision() throws Exception { - Assume.assumeTrue("Dynamic width/precision in printf requires printf4j support", false); AwkTestSupport .awkTest("POSIX 9.4 printf star width and precision") .script("BEGIN{ printf \"%*.*f\\n\", 6, 2, 3.14159 }") diff --git a/src/test/java/io/jawk/PrintfTest.java b/src/test/java/io/jawk/PrintfTest.java new file mode 100644 index 00000000..e69d359a --- /dev/null +++ b/src/test/java/io/jawk/PrintfTest.java @@ -0,0 +1,192 @@ +package io.jawk; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright (C) 2006 - 2026 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import io.jawk.jrt.AwkRuntimeException; +import org.junit.Test; + +/** + * Script-level tests for AWK {@code printf}/{@code sprintf} semantics + * (issue #528), matching gawk behavior. + */ +public class PrintfTest { + + @Test + public void testStringConversionOfIntegralDouble() throws Exception { + // The symptom reported in issue #528: i++ produces a double, and %s + // must print its integral value without a fractional part. + AwkTestSupport + .awkTest("printf %s prints integral doubles without fraction") + .script("BEGIN { a[0]=1; a[1]=2; for (i=0; i in a; i++) printf(\"x[%s]\\n\", i) }") + .expectLines("x[0]", "x[1]") + .runAndAssert(); + } + + @Test + public void testStringConversionHonorsConvfmt() throws Exception { + AwkTestSupport + .awkTest("printf %s converts numbers with CONVFMT") + .script("BEGIN { CONVFMT=\"%.2g\"; printf \"%s|\", 3.14159; s = sprintf(\"%s\", 3.14159); print s }") + .expectLines("3.1|3.1") + .runAndAssert(); + } + + @Test + public void testOfmtDoesNotAffectPrintf() throws Exception { + AwkTestSupport + .awkTest("printf %s ignores OFMT") + .script("BEGIN { OFMT=\"%.2f\"; printf \"%s\\n\", 3.14159 }") + .expectLines("3.14159") + .runAndAssert(); + } + + @Test + public void testCharConversionOfNumericValue() throws Exception { + AwkTestSupport + .awkTest("printf %c prints the character of a numeric code") + .script("BEGIN { printf \"%c%c\\n\", 65, 98.7 }") + .expectLines("Ab") + .runAndAssert(); + } + + @Test + public void testCharConversionOfNumericField() throws Exception { + AwkTestSupport + .awkTest("printf %c treats numeric fields as codes") + .script("{ printf \"%c\\n\", $1 }") + .stdin("65\n") + .expectLines("A") + .runAndAssert(); + } + + @Test + public void testDynamicPrecision() throws Exception { + AwkTestSupport + .awkTest("printf dynamic star width and precision") + .script("BEGIN { printf \"%.*s|%*d|%-*d|\\n\", 3, \"foobar\", 5, 42, 5, 42 }") + .expectLines("foo| 42|42 |") + .runAndAssert(); + } + + @Test + public void testIntegerConversions() throws Exception { + AwkTestSupport + .awkTest("printf integer conversions truncate and wrap like gawk") + .script("BEGIN { printf \"%d|%d|%i|%u|%x|%o\\n\", 42.7, -42.7, \"1e3\", -1, -1, 8 }") + .expectLines("42|-42|1000|18446744073709551615|ffffffffffffffff|10") + .runAndAssert(); + } + + @Test + public void testOutOfRangeIntegerConversions() throws Exception { + AwkTestSupport + .awkTest("printf out-of-range integers match gawk") + .script("BEGIN { printf \"%d|%d|%x\\n\", 2^100, 2^63, 2^100 }") + .expectLines("1267650600228229401496703205376|9223372036854775808|1.26765e+30") + .runAndAssert(); + } + + @Test + public void testPrintOfHugeIntegralValues() throws Exception { + AwkTestSupport + .awkTest("print renders huge integral values in full") + .script("BEGIN { print 2^100; print int(2^100); print 2^53 }") + .expectLines("1267650600228229401496703205376", "1267650600228229401496703205376", "9007199254740992") + .runAndAssert(); + } + + @Test + public void testNonFiniteValues() throws Exception { + AwkTestSupport + .awkTest("printf prints nan and inf like gawk") + .script("BEGIN { printf \"%f|%d|%s\\n\", log(-1), log(-1), 2 * 10^308 }") + .expectLines("nan|nan|inf") + .runAndAssert(); + } + + @Test + public void testUnknownSpecifierPrintsVerbatim() throws Exception { + AwkTestSupport + .awkTest("printf unknown conversion prints verbatim without consuming arguments") + .script("BEGIN { printf \"%q%d|%kmarco|a%nb\\n\", 1, 2 }") + .expectLines("%q1|%kmarco|a%nb") + .runAndAssert(); + } + + @Test + public void testNotEnoughArgumentsIsFatal() throws Exception { + AwkTestSupport + .awkTest("printf with too few arguments is a fatal error") + .script("BEGIN { printf \"%s %s\\n\", \"a\" }") + .expectThrow(AwkRuntimeException.class) + .runAndAssert(); + } + + @Test + public void testExtraArgumentsAreIgnored() throws Exception { + AwkTestSupport + .awkTest("printf ignores extra arguments") + .script("BEGIN { printf \"%s %s\\n\", \"a\", \"b\", \"c\" }") + .expectLines("a b") + .runAndAssert(); + } + + @Test + public void testPositionalSpecifiers() throws Exception { + AwkTestSupport + .awkTest("printf gawk positional specifiers") + .script("BEGIN { printf \"%2$s %1$s\\n\", \"world\", \"hello\" }") + .expectLines("hello world") + .runAndAssert(); + } + + @Test + public void testGroupingFlag() throws Exception { + AwkTestSupport + .awkTest("printf apostrophe flag groups thousands") + .script("BEGIN { printf \"%'d\\n\", 1234567 }") + .expectLines("1,234,567") + .runAndAssert(); + } + + @Test + public void testSprintfRoundHalfEven() throws Exception { + AwkTestSupport + .awkTest("printf %f rounds halfway cases to even like gawk") + .script("BEGIN { printf \"%.0f|%.0f|%.0f|%.2f\\n\", 2.5, 3.5, 4.5, 0.125 }") + .expectLines("2|4|4|0.12") + .runAndAssert(); + } + + @Test + public void testPrintfToFileHonorsConvfmt() throws Exception { + AwkTestSupport + .awkTest("printf to a file honors CONVFMT for %s") + .path("out.txt") + .script( + "BEGIN { CONVFMT=\"%.2g\"; f=\"{{out.txt}}\"; printf \"%s\\n\", 3.14159 > f; close(f); " + + "while ((getline x < f) > 0) print x }") + .expectLines("3.1") + .runAndAssert(); + } +} diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java new file mode 100644 index 00000000..488f5895 --- /dev/null +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -0,0 +1,778 @@ +package io.jawk.jrt; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright (C) 2006 - 2026 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static io.jawk.jrt.AwkPrintf.sprintf; + +import java.util.Locale; +import org.junit.Test; + +/** + * Unit tests for {@link AwkPrintf}. + *

+ * This suite incorporates the complete unit test suite of the former + * Printf4J project, + * including the tests that were disabled or commented out there. Where + * Printf4J (which emulated glibc) and AWK semantics differ, the expected + * values below were verified against gawk 5 and are annotated accordingly. + *

+ */ +public class AwkPrintfTest { + + @Test + public void testPlus() { + assertEquals("+42", sprintf("%+d", 42)); + assertEquals("-42", sprintf("%+d", -42)); + assertEquals(" +42", sprintf("%+5d", 42)); + assertEquals(" -42", sprintf("%+5d", -42)); + assertEquals(" +42", sprintf("%+15d", 42)); + assertEquals(" -42", sprintf("%+15d", -42)); + assertEquals("Hello testing", sprintf("%+s", "Hello testing")); + assertEquals("+1024", sprintf("%+d", 1024)); + assertEquals("-1024", sprintf("%+d", -1024)); + assertEquals("+1024", sprintf("%+i", 1024)); + assertEquals("-1024", sprintf("%+i", -1024)); + assertEquals("1024", sprintf("%+u", 1024)); + assertEquals("4294966272", sprintf("%+u", 4294966272L)); + assertEquals("777", sprintf("%+o", 511)); + assertEquals("37777777001", sprintf("%+o", 4294966785L)); + assertEquals("1234abcd", sprintf("%+x", 305441741)); + assertEquals("edcb5433", sprintf("%+x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%+X", 305441741)); + assertEquals("EDCB5433", sprintf("%+X", 3989525555L)); + assertEquals("x", sprintf("%+c", 'x')); + // Was commented out in Printf4J expecting "0": gawk prints nothing for + // a zero value with an explicit zero precision, even with sign flags. + assertEquals("", sprintf("%+.0d", 0)); + } + + @Test + public void testBlank() { + assertEquals(" 42", sprintf("% d", 42)); + assertEquals("-42", sprintf("% d", -42)); + assertEquals(" 42", sprintf("% 5d", 42)); + assertEquals(" -42", sprintf("% 5d", -42)); + assertEquals(" 42", sprintf("% 15d", 42)); + assertEquals(" -42", sprintf("% 15d", -42)); + assertEquals(" -42", sprintf("% 15d", -42)); + assertEquals(" -42.987", sprintf("% 15.3f", -42.987)); + assertEquals(" 42.987", sprintf("% 15.3f", 42.987)); + assertEquals("Hello testing", sprintf("% s", "Hello testing")); + assertEquals(" 1024", sprintf("% d", 1024)); + assertEquals("-1024", sprintf("% d", -1024)); + assertEquals(" 1024", sprintf("% i", 1024)); + assertEquals("-1024", sprintf("% i", -1024)); + assertEquals("1024", sprintf("% u", 1024)); + assertEquals("4294966272", sprintf("% u", 4294966272L)); + assertEquals("777", sprintf("% o", 511)); + assertEquals("37777777001", sprintf("% o", 4294966785L)); + assertEquals("1234abcd", sprintf("% x", 305441741)); + assertEquals("edcb5433", sprintf("% x", 3989525555L)); + assertEquals("1234ABCD", sprintf("% X", 305441741)); + assertEquals("EDCB5433", sprintf("% X", 3989525555L)); + assertEquals("x", sprintf("% c", 'x')); + } + + @Test + public void testZero() { + assertEquals("42", sprintf("%0d", 42)); + assertEquals("42", sprintf("%0ld", 42L)); + assertEquals("-42", sprintf("%0d", -42)); + assertEquals("00042", sprintf("%05d", 42)); + assertEquals("-0042", sprintf("%05d", -42)); + assertEquals("000000000000042", sprintf("%015d", 42)); + assertEquals("-00000000000042", sprintf("%015d", -42)); + assertEquals("000000000042.12", sprintf("%015.2f", 42.1234)); + assertEquals("00000000042.988", sprintf("%015.3f", 42.9876)); + assertEquals("-00000042.98760", sprintf("%015.5f", -42.9876)); + } + + @Test + public void testMinus() { + assertEquals("42", sprintf("%-d", 42)); + assertEquals("-42", sprintf("%-d", -42)); + assertEquals("42 ", sprintf("%-5d", 42)); + assertEquals("-42 ", sprintf("%-5d", -42)); + assertEquals("42 ", sprintf("%-15d", 42)); + assertEquals("-42 ", sprintf("%-15d", -42)); + assertEquals("42", sprintf("%-0d", 42)); + assertEquals("-42", sprintf("%-0d", -42)); + assertEquals("42 ", sprintf("%-05d", 42)); + assertEquals("-42 ", sprintf("%-05d", -42)); + assertEquals("42 ", sprintf("%-015d", 42)); + assertEquals("-42 ", sprintf("%-015d", -42)); + assertEquals("42", sprintf("%0-d", 42)); + assertEquals("-42", sprintf("%0-d", -42)); + assertEquals("42 ", sprintf("%0-5d", 42)); + assertEquals("-42 ", sprintf("%0-5d", -42)); + assertEquals("42 ", sprintf("%0-15d", 42)); + assertEquals("-42 ", sprintf("%0-15d", -42)); + assertEquals("-4.200e+01 ", sprintf("%0-15.3e", -42.)); + // Printf4J expected "-42.0 ": AWK's %g removes trailing + // zeros, so gawk prints "-42 ". + assertEquals("-42 ", sprintf("%0-15.3g", -42.)); + } + + @Test + public void testHash() { + // Printf4J expected "" here, but gawk prints "0" for a zero value + // with '#' and a zero precision on %x. + assertEquals("0", sprintf("%#.0x", 0)); + // Printf4J had this assertion commented out as "the real expected + // behavior, which is wrong IMO" (it returned "0x0" instead): C and + // gawk agree on "0", which is what AwkPrintf now produces. + assertEquals("0", sprintf("%#.1x", 0)); + // "%#.0llx" is invalid in gawk: doubled length modifiers make the + // whole specifier print verbatim, without consuming an argument. + assertEquals("%#.0llx", sprintf("%#.0llx", 0)); + assertEquals("0x0000614e", sprintf("%#.8x", 0x614e)); + // Was commented out in Printf4J ("binary is not supported for now"): + // %b is not an AWK conversion, so gawk prints the specifier verbatim. + assertEquals("%#b", sprintf("%#b", 6)); + } + + @Test + public void testSpecifier() { + assertEquals("Hello testing", sprintf("Hello testing")); + assertEquals("Hello testing", sprintf("%s", "Hello testing")); + assertEquals("1024", sprintf("%d", 1024)); + assertEquals("-1024", sprintf("%d", -1024)); + assertEquals("1024", sprintf("%i", 1024)); + assertEquals("-1024", sprintf("%i", -1024)); + assertEquals("1024", sprintf("%u", 1024)); + assertEquals("4294966272", sprintf("%u", 4294966272L)); + assertEquals("777", sprintf("%o", 511)); + assertEquals("37777777001", sprintf("%o", 4294966785L)); + assertEquals("1234abcd", sprintf("%x", 305441741)); + assertEquals("edcb5433", sprintf("%x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%X", 305441741)); + assertEquals("EDCB5433", sprintf("%X", 3989525555L)); + assertEquals("%", sprintf("%%")); + } + + @Test + public void testWidth() { + assertEquals("Hello testing", sprintf("%1s", "Hello testing")); + assertEquals("1024", sprintf("%1d", 1024)); + assertEquals("-1024", sprintf("%1d", -1024)); + assertEquals("1024", sprintf("%1i", 1024)); + assertEquals("-1024", sprintf("%1i", -1024)); + assertEquals("1024", sprintf("%1u", 1024)); + assertEquals("4294966272", sprintf("%1u", 4294966272L)); + assertEquals("777", sprintf("%1o", 511)); + assertEquals("37777777001", sprintf("%1o", 4294966785L)); + assertEquals("1234abcd", sprintf("%1x", 305441741)); + assertEquals("edcb5433", sprintf("%1x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%1X", 305441741)); + assertEquals("EDCB5433", sprintf("%1X", 3989525555L)); + assertEquals("x", sprintf("%1c", 'x')); + } + + @Test + public void testWidth20() { + assertEquals(" Hello", sprintf("%20s", "Hello")); + assertEquals(" 1024", sprintf("%20d", 1024)); + assertEquals(" -1024", sprintf("%20d", -1024)); + assertEquals(" 1024", sprintf("%20i", 1024)); + assertEquals(" -1024", sprintf("%20i", -1024)); + assertEquals(" 1024", sprintf("%20u", 1024)); + assertEquals(" 4294966272", sprintf("%20u", 4294966272L)); + assertEquals(" 777", sprintf("%20o", 511)); + assertEquals(" 37777777001", sprintf("%20o", 4294966785L)); + assertEquals(" 1234abcd", sprintf("%20x", 305441741)); + assertEquals(" edcb5433", sprintf("%20x", 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%20X", 305441741)); + assertEquals(" EDCB5433", sprintf("%20X", 3989525555L)); + assertEquals(" x", sprintf("%20c", 'x')); + } + + @Test + public void testWidthStar20() { + assertEquals(" Hello", sprintf("%*s", 20, "Hello")); + assertEquals(" 1024", sprintf("%*d", 20, 1024)); + assertEquals(" -1024", sprintf("%*d", 20, -1024)); + assertEquals(" 1024", sprintf("%*i", 20, 1024)); + assertEquals(" -1024", sprintf("%*i", 20, -1024)); + assertEquals(" 1024", sprintf("%*u", 20, 1024)); + assertEquals(" 4294966272", sprintf("%*u", 20, 4294966272L)); + assertEquals(" 777", sprintf("%*o", 20, 511)); + assertEquals(" 37777777001", sprintf("%*o", 20, 4294966785L)); + assertEquals(" 1234abcd", sprintf("%*x", 20, 305441741)); + assertEquals(" edcb5433", sprintf("%*x", 20, 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%*X", 20, 305441741)); + assertEquals(" EDCB5433", sprintf("%*X", 20, 3989525555L)); + assertEquals(" x", sprintf("%*c", 20, 'x')); + } + + @Test + public void testMinus20() { + assertEquals("Hello ", sprintf("%-20s", "Hello")); + assertEquals("1024 ", sprintf("%-20d", 1024)); + assertEquals("-1024 ", sprintf("%-20d", -1024)); + assertEquals("1024 ", sprintf("%-20i", 1024)); + assertEquals("-1024 ", sprintf("%-20i", -1024)); + assertEquals("1024 ", sprintf("%-20u", 1024)); + assertEquals("1024.1234 ", sprintf("%-20.4f", 1024.1234)); + assertEquals("4294966272 ", sprintf("%-20u", 4294966272L)); + assertEquals("777 ", sprintf("%-20o", 511)); + assertEquals("37777777001 ", sprintf("%-20o", 4294966785L)); + assertEquals("1234abcd ", sprintf("%-20x", 305441741)); + assertEquals("edcb5433 ", sprintf("%-20x", 3989525555L)); + assertEquals("1234ABCD ", sprintf("%-20X", 305441741)); + assertEquals("EDCB5433 ", sprintf("%-20X", 3989525555L)); + assertEquals("x ", sprintf("%-20c", 'x')); + assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-2d| |%5d|", 9, 9, 9)); + assertEquals("| 10| |10| | 10|", sprintf("|%5d| |%-2d| |%5d|", 10, 10, 10)); + assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-12d| |%5d|", 9, 9, 9)); + assertEquals("| 10| |10 | | 10|", sprintf("|%5d| |%-12d| |%5d|", 10, 10, 10)); + } + + @Test + public void testZeroMinus20() { + assertEquals("Hello ", sprintf("%0-20s", "Hello")); + assertEquals("1024 ", sprintf("%0-20d", 1024)); + assertEquals("-1024 ", sprintf("%0-20d", -1024)); + assertEquals("1024 ", sprintf("%0-20i", 1024)); + assertEquals("-1024 ", sprintf("%0-20i", -1024)); + assertEquals("1024 ", sprintf("%0-20u", 1024)); + assertEquals("4294966272 ", sprintf("%0-20u", 4294966272L)); + assertEquals("777 ", sprintf("%0-20o", 511)); + assertEquals("37777777001 ", sprintf("%0-20o", 4294966785L)); + assertEquals("1234abcd ", sprintf("%0-20x", 305441741)); + assertEquals("edcb5433 ", sprintf("%0-20x", 3989525555L)); + assertEquals("1234ABCD ", sprintf("%0-20X", 305441741)); + assertEquals("EDCB5433 ", sprintf("%0-20X", 3989525555L)); + assertEquals("x ", sprintf("%0-20c", 'x')); + } + + @Test + public void testPadding20() { + assertEquals("00000000000000001024", sprintf("%020d", 1024)); + assertEquals("-0000000000000001024", sprintf("%020d", -1024)); + assertEquals("00000000000000001024", sprintf("%020i", 1024)); + assertEquals("-0000000000000001024", sprintf("%020i", -1024)); + assertEquals("00000000000000001024", sprintf("%020u", 1024)); + assertEquals("00000000004294966272", sprintf("%020u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%020o", 511)); + assertEquals("00000000037777777001", sprintf("%020o", 4294966785L)); + assertEquals("0000000000001234abcd", sprintf("%020x", 305441741)); + assertEquals("000000000000edcb5433", sprintf("%020x", 3989525555L)); + assertEquals("0000000000001234ABCD", sprintf("%020X", 305441741)); + assertEquals("000000000000EDCB5433", sprintf("%020X", 3989525555L)); + } + + @Test + public void testPaddingPrecision20() { + assertEquals("00000000000000001024", sprintf("%.20d", 1024)); + assertEquals("-00000000000000001024", sprintf("%.20d", -1024)); + assertEquals("00000000000000001024", sprintf("%.20i", 1024)); + assertEquals("-00000000000000001024", sprintf("%.20i", -1024)); + assertEquals("00000000000000001024", sprintf("%.20u", 1024)); + assertEquals("00000000004294966272", sprintf("%.20u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%.20o", 511)); + assertEquals("00000000037777777001", sprintf("%.20o", 4294966785L)); + assertEquals("0000000000001234abcd", sprintf("%.20x", 305441741)); + assertEquals("000000000000edcb5433", sprintf("%.20x", 3989525555L)); + assertEquals("0000000000001234ABCD", sprintf("%.20X", 305441741)); + assertEquals("000000000000EDCB5433", sprintf("%.20X", 3989525555L)); + } + + @Test + public void testPaddingHashZero20() { + assertEquals("00000000000000001024", sprintf("%#020d", 1024)); + assertEquals("-0000000000000001024", sprintf("%#020d", -1024)); + assertEquals("00000000000000001024", sprintf("%#020i", 1024)); + assertEquals("-0000000000000001024", sprintf("%#020i", -1024)); + assertEquals("00000000000000001024", sprintf("%#020u", 1024)); + assertEquals("00000000004294966272", sprintf("%#020u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%#020o", 511)); + assertEquals("00000000037777777001", sprintf("%#020o", 4294966785L)); + assertEquals("0x00000000001234abcd", sprintf("%#020x", 305441741)); + assertEquals("0x0000000000edcb5433", sprintf("%#020x", 3989525555L)); + assertEquals("0X00000000001234ABCD", sprintf("%#020X", 305441741)); + assertEquals("0X0000000000EDCB5433", sprintf("%#020X", 3989525555L)); + } + + @Test + public void testPaddingHash20() { + assertEquals(" 1024", sprintf("%#20d", 1024)); + assertEquals(" -1024", sprintf("%#20d", -1024)); + assertEquals(" 1024", sprintf("%#20i", 1024)); + assertEquals(" -1024", sprintf("%#20i", -1024)); + assertEquals(" 1024", sprintf("%#20u", 1024)); + assertEquals(" 4294966272", sprintf("%#20u", 4294966272L)); + // The following assertions were commented out in Printf4J; they match + // C and gawk, and now pass. + assertEquals(" 0777", sprintf("%#20o", 511)); + assertEquals(" 037777777001", sprintf("%#20o", 4294966785L)); + assertEquals(" 0x1234abcd", sprintf("%#20x", 305441741)); + assertEquals(" 0xedcb5433", sprintf("%#20x", 3989525555L)); + assertEquals(" 0X1234ABCD", sprintf("%#20X", 305441741)); + assertEquals(" 0XEDCB5433", sprintf("%#20X", 3989525555L)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testPadding20Dot5() { + assertEquals(" 01024", sprintf("%20.5d", 1024)); + assertEquals(" -01024", sprintf("%20.5d", -1024)); + assertEquals(" 01024", sprintf("%20.5i", 1024)); + assertEquals(" -01024", sprintf("%20.5i", -1024)); + assertEquals(" 01024", sprintf("%20.5u", 1024)); + assertEquals(" 4294966272", sprintf("%20.5u", 4294966272L)); + assertEquals(" 00777", sprintf("%20.5o", 511)); + assertEquals(" 37777777001", sprintf("%20.5o", 4294966785L)); + assertEquals(" 1234abcd", sprintf("%20.5x", 305441741)); + assertEquals(" 00edcb5433", sprintf("%20.10x", 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%20.5X", 305441741)); + assertEquals(" 00EDCB5433", sprintf("%20.10X", 3989525555L)); + } + + // Was @Disabled in Printf4J; matches C and gawk. + @Test + public void testPaddingNegativeNumbers() { + // space padding + assertEquals("-5", sprintf("% 1d", -5)); + assertEquals("-5", sprintf("% 2d", -5)); + assertEquals(" -5", sprintf("% 3d", -5)); + assertEquals(" -5", sprintf("% 4d", -5)); + // zero padding + assertEquals("-5", sprintf("%01d", -5)); + assertEquals("-5", sprintf("%02d", -5)); + assertEquals("-05", sprintf("%03d", -5)); + assertEquals("-005", sprintf("%04d", -5)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testPaddingNegativeFloat() { + // space padding + assertEquals("-5.0", sprintf("% 3.1f", -5.)); + assertEquals("-5.0", sprintf("% 4.1f", -5.)); + assertEquals(" -5.0", sprintf("% 5.1f", -5.)); + assertEquals(" -5", sprintf("% 6.1g", -5.)); + assertEquals("-5.0e+00", sprintf("% 6.1e", -5.)); + assertEquals(" -5.0e+00", sprintf("% 10.1e", -5.)); + // zero padding + assertEquals("-5.0", sprintf("%03.1f", -5.)); + assertEquals("-5.0", sprintf("%04.1f", -5.)); + assertEquals("-05.0", sprintf("%05.1f", -5.)); + // zero padding no decimal point + assertEquals("-5", sprintf("%01.0f", -5.)); + assertEquals("-5", sprintf("%02.0f", -5.)); + assertEquals("-05", sprintf("%03.0f", -5.)); + assertEquals("-005.0e+00", sprintf("%010.1e", -5.)); + assertEquals("-05E+00", sprintf("%07.0E", -5.)); + assertEquals("-05", sprintf("%03.0g", -5.)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testLength() { + assertEquals("", sprintf("%.0s", "Hello testing")); + assertEquals(" ", sprintf("%20.0s", "Hello testing")); + assertEquals("", sprintf("%.s", "Hello testing")); + assertEquals(" ", sprintf("%20.s", "Hello testing")); + assertEquals(" 1024", sprintf("%20.0d", 1024)); + assertEquals(" -1024", sprintf("%20.0d", -1024)); + assertEquals(" ", sprintf("%20.d", 0)); + assertEquals(" 1024", sprintf("%20.0i", 1024)); + assertEquals(" -1024", sprintf("%20.i", -1024)); + assertEquals(" ", sprintf("%20.i", 0)); + assertEquals(" 1024", sprintf("%20.u", 1024)); + assertEquals(" 4294966272", sprintf("%20.0u", 4294966272L)); + assertEquals(" ", sprintf("%20.u", 0L)); + assertEquals(" 777", sprintf("%20.o", 511)); + assertEquals(" 37777777001", sprintf("%20.0o", 4294966785L)); + assertEquals(" ", sprintf("%20.o", 0L)); + assertEquals(" 1234abcd", sprintf("%20.x", 305441741)); + assertEquals(" 1234abcd", sprintf("%50.x", 305441741)); + assertEquals( + " 1234abcd 12345", + sprintf("%50.x%10.u", 305441741, 12345)); + assertEquals(" edcb5433", sprintf("%20.0x", 3989525555L)); + assertEquals(" ", sprintf("%20.x", 0L)); + assertEquals(" 1234ABCD", sprintf("%20.X", 305441741)); + assertEquals(" EDCB5433", sprintf("%20.0X", 3989525555L)); + assertEquals(" ", sprintf("%20.X", 0L)); + assertEquals(" ", sprintf("%02.0u", 0L)); + assertEquals(" ", sprintf("%02.0d", 0)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testFloat() { + // test special-case floats + assertEquals(" nan", sprintf("%8f", Float.NaN)); + assertEquals(" inf", sprintf("%8f", Float.POSITIVE_INFINITY)); + assertEquals("-inf ", sprintf("%-8f", Float.NEGATIVE_INFINITY)); + assertEquals(" +inf", sprintf("%+8e", Float.POSITIVE_INFINITY)); + assertEquals("3.1415", sprintf("%.4f", 3.1415354)); + assertEquals("30343.142", sprintf("%.3f", 30343.1415354)); + assertEquals("34", sprintf("%.0f", 34.1415354)); + assertEquals("1", sprintf("%.0f", 1.3)); + assertEquals("2", sprintf("%.0f", 1.55)); + assertEquals("1.6", sprintf("%.1f", 1.64)); + assertEquals("42.90", sprintf("%.2f", 42.8952)); + assertEquals("42.895200000", sprintf("%.9f", 42.8952)); + assertEquals("42.8952230000", sprintf("%.10f", 42.895223)); + // Printf4J expected "42.895223123000" and "42.895223877000" here + // because its reference implementation truncated to 9 significant + // fraction digits; gawk prints the correctly rounded values. + assertEquals("42.895223123457", sprintf("%.12f", 42.89522312345678)); + assertEquals("42.895223876543", sprintf("%.12f", 42.89522387654321)); + assertEquals(" 42.90", sprintf("%6.2f", 42.8952)); + assertEquals("+42.90", sprintf("%+6.2f", 42.8952)); + assertEquals("+42.9", sprintf("%+5.1f", 42.9252)); + assertEquals("42.500000", sprintf("%f", 42.5)); + assertEquals("42.5", sprintf("%.1f", 42.5)); + assertEquals("42167.000000", sprintf("%f", 42167.0)); + assertEquals("-12345.987654321", sprintf("%.9f", -12345.987654321)); + assertEquals("4.0", sprintf("%.1f", 3.999)); + assertEquals("4", sprintf("%.0f", 3.5)); + assertEquals("4", sprintf("%.0f", 4.5)); + assertEquals("3", sprintf("%.0f", 3.49)); + assertEquals("3.5", sprintf("%.1f", 3.49)); + assertEquals("a0.5 ", sprintf("a%-5.1f", 0.5)); + assertEquals("a0.5 end", sprintf("a%-5.1fend", 0.5)); + assertEquals("12345.7", sprintf("%G", 12345.678)); + assertEquals("12345.68", sprintf("%.7G", 12345.678)); + assertEquals("1.2346E+08", sprintf("%.5G", 123456789.)); + // Printf4J expected "12345.0": AWK's %G removes trailing zeros. + assertEquals("12345", sprintf("%.6G", 12345.)); + assertEquals(" +1.235e+08", sprintf("%+12.4g", 123456789.)); + assertEquals("0.0012", sprintf("%.2G", 0.001234)); + assertEquals(" +0.001234", sprintf("%+10.4G", 0.001234)); + assertEquals("+001.234e-05", sprintf("%+012.4g", 0.00001234)); + assertEquals("-1.23e-308", sprintf("%.3g", -1.2345e-308)); + assertEquals("+1.230E+308", sprintf("%+.3E", 1.23e+308)); + // Printf4J expected "1.0e+20" (its reference implementation switched + // to exponential notation out of range); gawk prints the full value. + assertEquals("100000000000000000000.0", sprintf("%.1f", 1E20)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5, + // which only accepts a single 'h', 'l', or 'L' length modifier and + // prints any other modifier combination verbatim. + @Test + public void testTypes() { + assertEquals("0", sprintf("%i", 0)); + assertEquals("1234", sprintf("%i", 1234)); + assertEquals("32767", sprintf("%i", 32767)); + assertEquals("-32767", sprintf("%i", -32767)); + assertEquals("30", sprintf("%li", 30L)); + assertEquals("-2147483647", sprintf("%li", -2147483647L)); + assertEquals("2147483647", sprintf("%li", 2147483647L)); + // Doubled modifiers ("ll", "hh") and the "q", "j", "z", and "t" + // modifiers are not valid in gawk: the specifier prints verbatim and + // consumes no argument. + assertEquals("%lli", sprintf("%lli", 30L)); + assertEquals("%lli", sprintf("%lli", -9223372036854775807L)); + assertEquals("%lli", sprintf("%lli", 9223372036854775807L)); + assertEquals("100000", sprintf("%lu", 100000L)); + assertEquals("4294967295", sprintf("%lu", 0xFFFFFFFFL)); + assertEquals("%llu", sprintf("%llu", 281474976710656L)); + assertEquals("%llu", sprintf("%llu", Long.parseUnsignedLong("18446744073709551615"))); + assertEquals("%zu", sprintf("%zu", 2147483647L)); + assertEquals("%zd", sprintf("%zd", 2147483647L)); + assertEquals("%zi", sprintf("%zi", -2147483647L)); + // %b is not an AWK conversion: printed verbatim, like gawk. + assertEquals("%b", sprintf("%b", 60000)); + assertEquals("%lb", sprintf("%lb", 12345678L)); + assertEquals("165140", sprintf("%o", 60000)); + assertEquals("57060516", sprintf("%lo", 12345678L)); + assertEquals("12345678", sprintf("%lx", 0x12345678L)); + assertEquals("%llx", sprintf("%llx", 0x1234567891234567L)); + assertEquals("abcdefab", sprintf("%lx", 0xabcdefabL)); + assertEquals("ABCDEFAB", sprintf("%lX", 0xabcdefabL)); + assertEquals("v", sprintf("%c", 'v')); + assertEquals("wv", sprintf("%cv", 'w')); + assertEquals("A Test", sprintf("%s", "A Test")); + // gawk ignores the single 'h' modifier without truncating the value, + // and prints the invalid "hh" specifiers verbatim. + assertEquals("%hhu", sprintf("%hhu", 0xFFFFL)); + assertEquals("13398", sprintf("%hu", 13398)); + assertEquals("1193046", sprintf("%hu", 0x123456L)); + assertEquals("Test%hhi 10000", sprintf("%s%hhi %hu", "Test", 10000, 0xFFFFFFFFL)); + } + + // Was @Disabled in Printf4J, which expected "kmarco": gawk prints the + // unknown "%k" specifier verbatim. + @Test + public void testUnknown() { + assertEquals("%kmarco", sprintf("%kmarco", 42, 37)); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testStringLength() { + assertEquals("This", sprintf("%.4s", "This is a test")); + assertEquals("test", sprintf("%.4s", "test")); + assertEquals("123", sprintf("%.7s", "123")); + assertEquals("", sprintf("%.7s", "")); + assertEquals("1234ab", sprintf("%.4s%.2s", "123456", "abcdef")); + // Printf4J expected ".2s": gawk prints the whole invalid specifier + // verbatim. + assertEquals("%.4.2s", sprintf("%.4.2s", "123456")); + assertEquals("123", sprintf("%.*s", 3, "123456")); + } + + // Was @Disabled in Printf4J; expected values verified against gawk 5. + @Test + public void testMisc() { + assertEquals("53000atest-20 bit", sprintf("%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit")); + assertEquals("0.33", sprintf("%.*f", 2, 0.33333333)); + assertEquals("1", sprintf("%.*d", -1, 1)); + assertEquals("foo", sprintf("%.3s", "foobar")); + // Printf4J expected " " (glibc behavior): gawk prints nothing at all + // for a zero value with zero precision, even with the space flag. + assertEquals("", sprintf("% .0d", 0)); + assertEquals(" 00004", sprintf("%10.5d", 4)); + assertEquals("hi x", sprintf("%*sx", -3, "hi")); + assertEquals("0.33", sprintf("%.*g", 2, 0.33333333)); + assertEquals("3.33e-01", sprintf("%.*e", 2, 0.33333333)); + } + + @Test + public void testChar() { + assertEquals("A", sprintf("%c", 65)); + assertEquals("A", sprintf("%c", 65L)); + assertEquals("A", sprintf("%c", 65.0)); + assertEquals("A", sprintf("%c", 65.1)); + assertEquals("A", sprintf("%c", Integer.valueOf(65))); + assertEquals("A", sprintf("%c", Long.valueOf(65))); + assertEquals("A", sprintf("%c", Float.valueOf(65))); + assertEquals("A", sprintf("%c", Double.valueOf(65))); + assertEquals("6", sprintf("%c", "65")); + Object nothing = null; + assertEquals("\0", sprintf("%c", nothing)); + } + + // Ported from Printf4J's testToChar; AwkPrintf converts values for %c + // internally, so the equivalent assertions go through sprintf(). + @Test + public void testToChar() { + assertEquals("A", sprintf("%c", 65)); + assertEquals("A", sprintf("%c", 65L)); + assertEquals("A", sprintf("%c", 65.0)); + assertEquals("A", sprintf("%c", 65.1)); + assertEquals("A", sprintf("%c", 65.9)); + assertEquals("A", sprintf("%c", Integer.valueOf(65))); + assertEquals("A", sprintf("%c", Long.valueOf(65))); + assertEquals("A", sprintf("%c", Float.valueOf(65))); + assertEquals("A", sprintf("%c", Double.valueOf(65))); + assertEquals("6", sprintf("%c", "65")); + assertEquals("\0", sprintf("%c", "")); + Object nothing = null; + assertEquals("\0", sprintf("%c", nothing)); + } + + // Ported from Printf4J's testToLong: the same conversion now lives in + // JRT.toLong (they shared the same original implementation). + @Test + public void testToLong() { + assertEquals(65L, JRT.toLong('A')); + assertEquals(65L, JRT.toLong(65)); + assertEquals(65L, JRT.toLong(65L)); + assertEquals(65L, JRT.toLong(65.0)); + assertEquals(65L, JRT.toLong(65.1)); + assertEquals(65L, JRT.toLong(65.9)); + assertEquals(65L, JRT.toLong(Integer.valueOf(65))); + assertEquals(65L, JRT.toLong(Long.valueOf(65))); + assertEquals(65L, JRT.toLong(Float.valueOf(65))); + assertEquals(65L, JRT.toLong(Double.valueOf(65))); + assertEquals(65L, JRT.toLong("65")); + assertEquals(65L, JRT.toLong("65A")); + assertEquals(65L, JRT.toLong("65A6666666666666666666666666600000000033333333333999999999999")); + assertEquals(0L, JRT.toLong("")); + Object nothing = null; + assertEquals(0L, JRT.toLong(nothing)); + } + + // Ported from Printf4J's testToDouble: the same conversion now lives in + // JRT.toDouble (they shared the same original implementation). + @Test + public void testToDouble() { + assertEquals(65.0, JRT.toDouble('A'), 0.0); + assertEquals(65.0, JRT.toDouble(65), 0.0); + assertEquals(65.0, JRT.toDouble(65L), 0.0); + assertEquals(65.0, JRT.toDouble(65.0), 0.0); + assertEquals(65.1, JRT.toDouble(65.1), 0.0); + assertEquals(65.9, JRT.toDouble(65.9), 0.0); + assertEquals(65.0, JRT.toDouble(Integer.valueOf(65)), 0.0); + assertEquals(65.0, JRT.toDouble(Long.valueOf(65)), 0.0); + assertEquals(65.0, JRT.toDouble(Float.valueOf(65)), 0.0); + assertEquals(65.0, JRT.toDouble(Double.valueOf(65)), 0.0); + assertEquals(65.0, JRT.toDouble("65"), 0.0); + assertEquals(65.0, JRT.toDouble("65A"), 0.0); + assertEquals(65.0, JRT.toDouble("65A6666666666666666666666666600000000033333333333999999999999"), 0.0); + assertEquals(65.0, JRT.toDouble("6.5E+1"), 0.0); + assertEquals(0.0, JRT.toDouble(""), 0.0); + Object nothing = null; + assertEquals(0.0, JRT.toDouble(nothing), 0.0); + } + + // ------------------------------------------------------------------ + // AWK-specific semantics beyond the original Printf4J suite. + // ------------------------------------------------------------------ + + @Test + public void testStringConversionUsesAwkNumberToStringRules() { + // The symptom from issue #528: an integral double prints without a + // fractional part. + assertEquals("1", sprintf("%s", 1.0)); + assertEquals("x[1]", sprintf("x[%s]", 1.0)); + // Non-integral values use CONVFMT. + assertEquals("3.14159", sprintf("%s", 3.14159265)); + assertEquals("3.1", sprintf(Locale.US, "%.2g", "%s", 3.14159265)); + // CONVFMT that is not a %g-style format is honored verbatim. + assertEquals("3.14", sprintf(Locale.US, "%.2f", "%s", 3.14159265)); + // Integral values beyond the 64-bit range print in full. + assertEquals("100000000000000000000", sprintf("%s", 1e20)); + // Exact long values are preserved. + assertEquals("9223372036854775807", sprintf("%s", Long.MAX_VALUE)); + } + + @Test + public void testCharConversion() { + // A numeric value selects the corresponding code point. + assertEquals("é", sprintf("%c", 233)); + // A code point beyond the BMP produces the full character. + assertEquals(new String(Character.toChars(0x1F600)), sprintf("%c", 0x1F600)); + // A string value uses its first character. + assertEquals("X", sprintf("%c", "XYZ")); + // Width applies to %c like any other conversion. + assertEquals(" A", sprintf("%5c", 65)); + assertEquals("A ", sprintf("%-5c", 65)); + } + + @Test + public void testDynamicWidthAndPrecision() { + assertEquals(" 3.14", sprintf("%*.*f", 8, 2, 3.14159)); + assertEquals(" 3.14159", sprintf("%9s", 3.14159)); + // A negative dynamic width means left justification. + assertEquals("42 ", sprintf("%*d", -6, 42)); + // Width and precision arguments are converted like AWK numbers. + assertEquals(" 3.14", sprintf("%*.*f", "6", "2", 3.14159)); + } + + @Test + public void testPositionalSpecifiers() { + assertEquals("b a", sprintf("%2$s %1$s", "a", "b")); + assertEquals("a b a", sprintf("%1$s %2$s %1$s", "a", "b")); + } + + @Test + public void testIntegerTruncationAndConversion() { + // %d truncates toward zero. + assertEquals("42", sprintf("%d", 42.7)); + assertEquals("-42", sprintf("%d", -42.7)); + // Strings convert with AWK's number rules (leading/trailing spaces, + // exponent notation, numeric prefixes). + assertEquals("1000", sprintf("%d", "1e3")); + assertEquals("42", sprintf("%d", " 42 ")); + assertEquals("3", sprintf("%d", "+3.9")); + assertEquals("0", sprintf("%d", "abc")); + assertEquals("0", sprintf("%x", "abc")); + } + + @Test + public void testOutOfRangeIntegerConversions() { + // Negative values wrap to unsigned 64-bit for %u, %o, %x. + assertEquals("18446744073709551615", sprintf("%u", -1)); + assertEquals("ffffffffffffffff", sprintf("%x", -1)); + assertEquals("1777777777777777777777", sprintf("%o", -1)); + // 2^63 is out of the signed range but fits unsigned. + assertEquals("9223372036854775808", sprintf("%d", 9.223372036854775808e18)); + // %d beyond 64 bits prints the full decimal expansion, like gawk. + assertEquals("1267650600228229401496703205376", sprintf("%d", Math.pow(2, 100))); + assertEquals("-1267650600228229401496703205376", sprintf("%d", -Math.pow(2, 100))); + // %u, %o, and %x beyond 64 bits fall back to %g notation, like gawk. + assertEquals("1.26765e+30", sprintf("%x", Math.pow(2, 100))); + assertEquals("1.26765e+30", sprintf("%u", Math.pow(2, 100))); + assertEquals("1.26765e+30", sprintf("%o", Math.pow(2, 100))); + } + + @Test + public void testNonFiniteValues() { + assertEquals("nan", sprintf("%d", Double.NaN)); + assertEquals("inf", sprintf("%d", Double.POSITIVE_INFINITY)); + assertEquals("-inf", sprintf("%f", Double.NEGATIVE_INFINITY)); + assertEquals("INF", sprintf("%E", Double.POSITIVE_INFINITY)); + assertEquals("NAN", sprintf("%G", Double.NaN)); + assertEquals("nan", sprintf("%s", Double.NaN)); + assertEquals("inf", sprintf("%s", Double.POSITIVE_INFINITY)); + assertEquals("-inf", sprintf("%s", Double.NEGATIVE_INFINITY)); + } + + @Test + public void testUnknownSpecifiersDoNotConsumeArguments() { + // The unknown %q prints verbatim and its argument feeds %d instead. + assertEquals("%q1", sprintf("%q%d", 1, 2)); + // %n is not an AWK conversion (Printf4J used to print a newline). + assertEquals("a%nb", sprintf("a%nb")); + // A dangling % prints verbatim. + assertEquals("abc%", sprintf("abc%")); + } + + @Test + public void testNotEnoughArgumentsIsFatal() { + assertThrows(AwkRuntimeException.class, () -> sprintf("%d %s", 1)); + assertThrows(AwkRuntimeException.class, () -> sprintf("%5s")); + assertThrows(AwkRuntimeException.class, () -> sprintf("%*d", 5)); + } + + @Test + public void testExtraArgumentsAreIgnored() { + assertEquals("a b", sprintf("%s %s", "a", "b", "c")); + } + + @Test + public void testGroupingFlag() { + assertEquals("1,234,567", sprintf("%'d", 1234567)); + assertEquals("1,234,567.89", sprintf("%'.2f", 1234567.891)); + assertEquals("1.234.567", sprintf(Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567)); + } + + @Test + public void testLocaleDecimalSeparator() { + assertEquals("3,14", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159)); + assertEquals("3,14159", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159)); + } + + @Test + public void testToAwkString() { + assertEquals("", AwkPrintf.toAwkString(null, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("text", AwkPrintf.toAwkString("text", AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("1", AwkPrintf.toAwkString(1.0, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("0.1", AwkPrintf.toAwkString(0.1, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("3.14159", AwkPrintf.toAwkString(3.14159265, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("100000000000000000000", AwkPrintf.toAwkString(1e20, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("9223372036854775807", AwkPrintf.toAwkString(Long.MAX_VALUE, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("nan", AwkPrintf.toAwkString(Double.NaN, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("inf", AwkPrintf.toAwkString(Double.POSITIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("-inf", AwkPrintf.toAwkString(Double.NEGATIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + } +} From 9a0004adb730f4f2a093e4f675810b178c3c69d1 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 11 Aug 2026 23:20:27 +0200 Subject: [PATCH 02/18] Address Codex review comments on AwkPrintf - Route the AwkPrintfTest suite through new AwkTestSupport helper methods (assertSprintf, assertSprintfThrows, assertToAwkString), per the repository testing guidelines. - Out-of-range %u/%o/%x/%X values now fall back to %g with the original sign, flags, precision, and width, like gawk: printf "%u", -(2^100) prints -1.26765e+30, and %.10x / %#.3x / %020u honor their precision and flags (gawk-verified). - The '#' flag keeps the decimal point on %g/%e results without fractional digits: %#.1g of 1 prints "1." and %#.1g of 12345 prints "1.e+04", like gawk. - Unsigned conversions with an explicit zero precision print "0" when a nonzero value truncates to zero (%.0x of 0.1) or when '#' is given (%#.0u of 0), while an exact zero still prints nothing, matching gawk; %d keeps printing nothing in both cases. - Mixing positional (n$) and sequential specifiers in one format string is now a fatal error, like gawk ("must use `count$' on all formats or none"). All new expectations verified against gawk 5.0/5.1. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 74 +- src/site/markdown/behavior-changes.md | 3 +- src/test/java/io/jawk/AwkTestSupport.java | 69 ++ src/test/java/io/jawk/PrintfTest.java | 9 + src/test/java/io/jawk/jrt/AwkPrintfTest.java | 955 ++++++++++--------- 5 files changed, 627 insertions(+), 483 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 9f8e265f..7a1fec9c 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -164,8 +164,8 @@ private static String numberToAwkString(final double number, final String conver if (rounded >= -TWO_POW_63 && rounded < TWO_POW_63) { return Long.toString((long) rounded); } - return new BigDecimal(rounded).toBigInteger().toString(); // NOPMD - the exact binary value of the double is - // intended + // The exact binary value of the double is intended: it makes rounding match gawk's C library. + return new BigDecimal(rounded).toBigInteger().toString(); // NOPMD } String fmt = conversionFormat == null || conversionFormat.isEmpty() ? DEFAULT_CONVFMT : conversionFormat; return sprintf(locale, DEFAULT_CONVFMT, fmt, Double.valueOf(number)); @@ -208,6 +208,12 @@ private static final class AwkPrintfFormatter { /** Index of the next sequential argument to consume. */ private int argIndex; + /** Whether a positional ({@code n$}) argument reference was seen. */ + private boolean sawPositional; + + /** Whether a sequential argument reference was seen. */ + private boolean sawSequential; + AwkPrintfFormatter(Locale locale, String convfmt, String format, Object[] args) { this.locale = locale; this.convfmt = convfmt; @@ -392,6 +398,8 @@ private int starPositionEnd(int i) { } private Object nextArg() { + sawSequential = true; + rejectMixedArgumentModes(); if (argIndex >= args.length) { throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); } @@ -399,12 +407,24 @@ private Object nextArg() { } private Object argAt(int position) { + sawPositional = true; + rejectMixedArgumentModes(); if (position <= 0 || position > args.length) { throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); } return args[position - 1]; } + /** + * Rejects format strings that mix positional ({@code n$}) and + * sequential argument references, like gawk. + */ + private void rejectMixedArgumentModes() { + if (sawPositional && sawSequential) { + throw new AwkRuntimeException("must use `count$' on all formats or none in `" + format + "'"); + } + } + private void render(char conversion, Flags flags, int width, int precision, Object arg) { switch (conversion) { case 'c': @@ -487,7 +507,8 @@ private void renderSignedInteger(Flags flags, int width, int precision, Object a } else { // Out of 64-bit range: print the full decimal expansion of the // (integral) double, like gawk. - BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD - the exact binary value of the double is intended + // The exact binary value of the double is intended: it makes rounding match gawk's C library. + BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD negative = bi.signum() < 0; magnitude = bi.abs().toString(); } @@ -509,17 +530,15 @@ private void renderUnsignedInteger(char conversion, Flags flags, int width, int } else if (d >= -TWO_POW_63 && d < TWO_POW_63) { magnitude = Long.toUnsignedString((long) d, radix); } else { - BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD - the exact binary value of the double is intended + // The exact binary value of the double is intended: it makes rounding match gawk's C library. + BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD if (bi.signum() >= 0 && bi.compareTo(TWO_POW_64) < 0) { magnitude = bi.toString(radix); } else { // Out of the unsigned 64-bit range: fall back to %g - // notation, like gawk. - appendPadded( - floatBody('g', new Flags(false, false, false, false, false, false), -1, d), - flags.leftJustify, - false, - width); + // notation with the original sign, flags, precision, and + // width, like gawk. + renderFloat('g', flags, width, precision, Double.valueOf(d)); return; } } @@ -529,9 +548,11 @@ private void renderUnsignedInteger(char conversion, Flags flags, int width, int boolean zeroMagnitude = isZeroMagnitude(magnitude); int actualPrecision = precision; - if (flags.alternate && zeroMagnitude && precision == 0 && conversion != 'u') { - // gawk prints "0" for a zero value with '#' and an explicit - // zero precision on %o, %x, and %X. + if (zeroMagnitude && precision == 0 && (flags.alternate || d != 0)) { + // gawk prints "0" rather than nothing for a zero magnitude + // with an explicit zero precision when the '#' flag is given, + // or when the original value is nonzero and merely truncates + // to zero. actualPrecision = 1; } String prefix = ""; @@ -623,8 +644,8 @@ private String floatBody(char conversion, Flags flags, int precision, double d) case 'f': case 'F': { int p = precision < 0 ? 6 : precision; - String s = decimalString(new BigDecimal(abs).setScale(p, RoundingMode.HALF_EVEN)); // NOPMD - exact binary value - // intended + // The exact binary value of the double is intended: it makes rounding match gawk's C library. + String s = decimalString(new BigDecimal(abs).setScale(p, RoundingMode.HALF_EVEN)); // NOPMD if (flags.alternate && p == 0) { s = s + "."; } @@ -692,21 +713,16 @@ private String generalFloat(double abs, int precision, boolean alternate) { if (abs == 0) { return alternate ? "0." + zeros(precision - 1) : "0"; } - BigDecimal rounded = new BigDecimal(abs).round(new MathContext(precision, RoundingMode.HALF_EVEN)); // NOPMD - - // exact - // binary - // value - // intended + // The exact binary value of the double is intended: it makes rounding match gawk's C library. + BigDecimal rounded = new BigDecimal(abs).round(new MathContext(precision, RoundingMode.HALF_EVEN)); // NOPMD int exponent = rounded.precision() - rounded.scale() - 1; if (exponent >= -4 && exponent < precision) { String s = decimalString(rounded.setScale(precision - 1 - exponent, RoundingMode.UNNECESSARY)); - return alternate ? s : stripTrailingFractionZeros(s); + return alternate ? forceDecimalSeparator(s) : stripTrailingFractionZeros(s); } String mantissa = decimalString( rounded.movePointLeft(exponent).setScale(precision - 1, RoundingMode.UNNECESSARY)); - if (!alternate) { - mantissa = stripTrailingFractionZeros(mantissa); - } + mantissa = alternate ? forceDecimalSeparator(mantissa) : stripTrailingFractionZeros(mantissa); return mantissa + "e" + (exponent < 0 ? "-" : "+") + exponentDigits(Math.abs(exponent)); } @@ -762,6 +778,16 @@ private String groupDigits(String s) { return sb.toString(); } + /** + * Appends the locale decimal separator when {@code s} has none, as + * the '#' flag requires for {@code %g} results without fractional + * digits. + */ + private String forceDecimalSeparator(String s) { + char decimalSeparator = DecimalFormatSymbols.getInstance(locale).getDecimalSeparator(); + return s.indexOf(decimalSeparator) < 0 ? s + decimalSeparator : s; + } + private String stripTrailingFractionZeros(String s) { char decimalSeparator = DecimalFormatSymbols.getInstance(locale).getDecimalSeparator(); if (s.indexOf(decimalSeparator) < 0) { diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index cad6d6f8..ddd69590 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -31,7 +31,8 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. character of a string value. - Dynamic precision (`%.*f`) is now supported in addition to dynamic width (`%*d`), including negative values (negative width left-justifies, negative precision means no precision), as - are gawk positional specifiers (`%2$s`) and the `'` grouping flag (`%'d`). + are gawk positional specifiers (`%2$s`) and the `'` grouping flag (`%'d`); mixing + positional and sequential specifiers in one format string is a fatal error, as in gawk. - Out-of-range integer conversions follow gawk: negative values wrap to unsigned 64-bit for `%u`/`%o`/`%x`/`%X`, values beyond 64 bits print the full decimal expansion for `%d`/`%i` and fall back to `%g` notation for `%u`/`%o`/`%x`/`%X`. diff --git a/src/test/java/io/jawk/AwkTestSupport.java b/src/test/java/io/jawk/AwkTestSupport.java index 1b257179..8af1e7cb 100644 --- a/src/test/java/io/jawk/AwkTestSupport.java +++ b/src/test/java/io/jawk/AwkTestSupport.java @@ -120,6 +120,75 @@ public static Path sharedTempDirectory() { return SHARED_TEMP_DIR; } + /** + * Asserts that AWK's {@code sprintf()} formatting engine + * ({@link io.jawk.jrt.AwkPrintf}) produces the expected text with the + * default {@link Locale#US} locale and default {@code CONVFMT}. This is the + * standard helper for formatter-level unit tests, mirroring what a script + * calling {@code sprintf(format, args...)} would produce. + * + * @param expected the expected formatted text + * @param format AWK format string + * @param args arguments supplied after the format string + */ + public static void assertSprintf(String expected, String format, Object... args) { + org.junit.Assert + .assertEquals( + "sprintf(\"" + format + "\")", + expected, + io.jawk.jrt.AwkPrintf.sprintf(format, args)); + } + + /** + * Asserts that AWK's {@code sprintf()} formatting engine + * ({@link io.jawk.jrt.AwkPrintf}) produces the expected text with an + * explicit locale and {@code CONVFMT} value. + * + * @param expected the expected formatted text + * @param locale locale used for numeric formatting + * @param convfmt number-to-string conversion format ({@code CONVFMT}) + * @param format AWK format string + * @param args arguments supplied after the format string + */ + public static void assertSprintf(String expected, Locale locale, String convfmt, String format, Object... args) { + org.junit.Assert + .assertEquals( + "sprintf(\"" + format + "\") with locale " + locale + " and CONVFMT \"" + convfmt + "\"", + expected, + io.jawk.jrt.AwkPrintf.sprintf(locale, convfmt, format, args)); + } + + /** + * Asserts that AWK's {@code sprintf()} formatting engine raises the given + * exception, as it does for a format string with too few arguments. + * + * @param expectedThrowable the exception type expected from the call + * @param format AWK format string + * @param args arguments supplied after the format string + */ + public static void assertSprintfThrows( + Class expectedThrowable, + String format, + Object... args) { + org.junit.Assert.assertThrows(expectedThrowable, () -> io.jawk.jrt.AwkPrintf.sprintf(format, args)); + } + + /** + * Asserts AWK's number-to-string conversion + * ({@link io.jawk.jrt.AwkPrintf#toAwkString(Object, String, Locale)}) with + * the default {@link Locale#US} locale and default {@code CONVFMT}. + * + * @param expected the expected AWK string value + * @param value the value to convert + */ + public static void assertToAwkString(String expected, Object value) { + org.junit.Assert + .assertEquals( + "toAwkString(" + value + ")", + expected, + io.jawk.jrt.AwkPrintf.toAwkString(value, io.jawk.jrt.AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + } + /** * Represents a fully configured test case produced by one of the builders. * Implementations know how to prepare the execution environment, run the diff --git a/src/test/java/io/jawk/PrintfTest.java b/src/test/java/io/jawk/PrintfTest.java index e69d359a..5f2a2f0a 100644 --- a/src/test/java/io/jawk/PrintfTest.java +++ b/src/test/java/io/jawk/PrintfTest.java @@ -160,6 +160,15 @@ public void testPositionalSpecifiers() throws Exception { .runAndAssert(); } + @Test + public void testMixedPositionalSpecifiersAreFatal() throws Exception { + AwkTestSupport + .awkTest("printf mixing positional and sequential specifiers is fatal") + .script("BEGIN { printf \"%2$s %s\\n\", \"a\", \"b\" }") + .expectThrow(AwkRuntimeException.class) + .runAndAssert(); + } + @Test public void testGroupingFlag() throws Exception { AwkTestSupport diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index 488f5895..6b3ed295 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -23,14 +23,18 @@ */ import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static io.jawk.jrt.AwkPrintf.sprintf; +import static io.jawk.AwkTestSupport.assertSprintf; +import static io.jawk.AwkTestSupport.assertSprintfThrows; +import static io.jawk.AwkTestSupport.assertToAwkString; import java.util.Locale; import org.junit.Test; /** - * Unit tests for {@link AwkPrintf}. + * Unit tests for {@link AwkPrintf}, written with the + * {@code io.jawk.AwkTestSupport} formatter assertion helpers + * ({@code assertSprintf}, {@code assertSprintfThrows}, + * {@code assertToAwkString}). *

* This suite incorporates the complete unit test suite of the former * Printf4J project, @@ -43,434 +47,432 @@ public class AwkPrintfTest { @Test public void testPlus() { - assertEquals("+42", sprintf("%+d", 42)); - assertEquals("-42", sprintf("%+d", -42)); - assertEquals(" +42", sprintf("%+5d", 42)); - assertEquals(" -42", sprintf("%+5d", -42)); - assertEquals(" +42", sprintf("%+15d", 42)); - assertEquals(" -42", sprintf("%+15d", -42)); - assertEquals("Hello testing", sprintf("%+s", "Hello testing")); - assertEquals("+1024", sprintf("%+d", 1024)); - assertEquals("-1024", sprintf("%+d", -1024)); - assertEquals("+1024", sprintf("%+i", 1024)); - assertEquals("-1024", sprintf("%+i", -1024)); - assertEquals("1024", sprintf("%+u", 1024)); - assertEquals("4294966272", sprintf("%+u", 4294966272L)); - assertEquals("777", sprintf("%+o", 511)); - assertEquals("37777777001", sprintf("%+o", 4294966785L)); - assertEquals("1234abcd", sprintf("%+x", 305441741)); - assertEquals("edcb5433", sprintf("%+x", 3989525555L)); - assertEquals("1234ABCD", sprintf("%+X", 305441741)); - assertEquals("EDCB5433", sprintf("%+X", 3989525555L)); - assertEquals("x", sprintf("%+c", 'x')); + assertSprintf("+42", "%+d", 42); + assertSprintf("-42", "%+d", -42); + assertSprintf(" +42", "%+5d", 42); + assertSprintf(" -42", "%+5d", -42); + assertSprintf(" +42", "%+15d", 42); + assertSprintf(" -42", "%+15d", -42); + assertSprintf("Hello testing", "%+s", "Hello testing"); + assertSprintf("+1024", "%+d", 1024); + assertSprintf("-1024", "%+d", -1024); + assertSprintf("+1024", "%+i", 1024); + assertSprintf("-1024", "%+i", -1024); + assertSprintf("1024", "%+u", 1024); + assertSprintf("4294966272", "%+u", 4294966272L); + assertSprintf("777", "%+o", 511); + assertSprintf("37777777001", "%+o", 4294966785L); + assertSprintf("1234abcd", "%+x", 305441741); + assertSprintf("edcb5433", "%+x", 3989525555L); + assertSprintf("1234ABCD", "%+X", 305441741); + assertSprintf("EDCB5433", "%+X", 3989525555L); + assertSprintf("x", "%+c", 'x'); // Was commented out in Printf4J expecting "0": gawk prints nothing for // a zero value with an explicit zero precision, even with sign flags. - assertEquals("", sprintf("%+.0d", 0)); + assertSprintf("", "%+.0d", 0); } @Test public void testBlank() { - assertEquals(" 42", sprintf("% d", 42)); - assertEquals("-42", sprintf("% d", -42)); - assertEquals(" 42", sprintf("% 5d", 42)); - assertEquals(" -42", sprintf("% 5d", -42)); - assertEquals(" 42", sprintf("% 15d", 42)); - assertEquals(" -42", sprintf("% 15d", -42)); - assertEquals(" -42", sprintf("% 15d", -42)); - assertEquals(" -42.987", sprintf("% 15.3f", -42.987)); - assertEquals(" 42.987", sprintf("% 15.3f", 42.987)); - assertEquals("Hello testing", sprintf("% s", "Hello testing")); - assertEquals(" 1024", sprintf("% d", 1024)); - assertEquals("-1024", sprintf("% d", -1024)); - assertEquals(" 1024", sprintf("% i", 1024)); - assertEquals("-1024", sprintf("% i", -1024)); - assertEquals("1024", sprintf("% u", 1024)); - assertEquals("4294966272", sprintf("% u", 4294966272L)); - assertEquals("777", sprintf("% o", 511)); - assertEquals("37777777001", sprintf("% o", 4294966785L)); - assertEquals("1234abcd", sprintf("% x", 305441741)); - assertEquals("edcb5433", sprintf("% x", 3989525555L)); - assertEquals("1234ABCD", sprintf("% X", 305441741)); - assertEquals("EDCB5433", sprintf("% X", 3989525555L)); - assertEquals("x", sprintf("% c", 'x')); + assertSprintf(" 42", "% d", 42); + assertSprintf("-42", "% d", -42); + assertSprintf(" 42", "% 5d", 42); + assertSprintf(" -42", "% 5d", -42); + assertSprintf(" 42", "% 15d", 42); + assertSprintf(" -42", "% 15d", -42); + assertSprintf(" -42", "% 15d", -42); + assertSprintf(" -42.987", "% 15.3f", -42.987); + assertSprintf(" 42.987", "% 15.3f", 42.987); + assertSprintf("Hello testing", "% s", "Hello testing"); + assertSprintf(" 1024", "% d", 1024); + assertSprintf("-1024", "% d", -1024); + assertSprintf(" 1024", "% i", 1024); + assertSprintf("-1024", "% i", -1024); + assertSprintf("1024", "% u", 1024); + assertSprintf("4294966272", "% u", 4294966272L); + assertSprintf("777", "% o", 511); + assertSprintf("37777777001", "% o", 4294966785L); + assertSprintf("1234abcd", "% x", 305441741); + assertSprintf("edcb5433", "% x", 3989525555L); + assertSprintf("1234ABCD", "% X", 305441741); + assertSprintf("EDCB5433", "% X", 3989525555L); + assertSprintf("x", "% c", 'x'); } @Test public void testZero() { - assertEquals("42", sprintf("%0d", 42)); - assertEquals("42", sprintf("%0ld", 42L)); - assertEquals("-42", sprintf("%0d", -42)); - assertEquals("00042", sprintf("%05d", 42)); - assertEquals("-0042", sprintf("%05d", -42)); - assertEquals("000000000000042", sprintf("%015d", 42)); - assertEquals("-00000000000042", sprintf("%015d", -42)); - assertEquals("000000000042.12", sprintf("%015.2f", 42.1234)); - assertEquals("00000000042.988", sprintf("%015.3f", 42.9876)); - assertEquals("-00000042.98760", sprintf("%015.5f", -42.9876)); + assertSprintf("42", "%0d", 42); + assertSprintf("42", "%0ld", 42L); + assertSprintf("-42", "%0d", -42); + assertSprintf("00042", "%05d", 42); + assertSprintf("-0042", "%05d", -42); + assertSprintf("000000000000042", "%015d", 42); + assertSprintf("-00000000000042", "%015d", -42); + assertSprintf("000000000042.12", "%015.2f", 42.1234); + assertSprintf("00000000042.988", "%015.3f", 42.9876); + assertSprintf("-00000042.98760", "%015.5f", -42.9876); } @Test public void testMinus() { - assertEquals("42", sprintf("%-d", 42)); - assertEquals("-42", sprintf("%-d", -42)); - assertEquals("42 ", sprintf("%-5d", 42)); - assertEquals("-42 ", sprintf("%-5d", -42)); - assertEquals("42 ", sprintf("%-15d", 42)); - assertEquals("-42 ", sprintf("%-15d", -42)); - assertEquals("42", sprintf("%-0d", 42)); - assertEquals("-42", sprintf("%-0d", -42)); - assertEquals("42 ", sprintf("%-05d", 42)); - assertEquals("-42 ", sprintf("%-05d", -42)); - assertEquals("42 ", sprintf("%-015d", 42)); - assertEquals("-42 ", sprintf("%-015d", -42)); - assertEquals("42", sprintf("%0-d", 42)); - assertEquals("-42", sprintf("%0-d", -42)); - assertEquals("42 ", sprintf("%0-5d", 42)); - assertEquals("-42 ", sprintf("%0-5d", -42)); - assertEquals("42 ", sprintf("%0-15d", 42)); - assertEquals("-42 ", sprintf("%0-15d", -42)); - assertEquals("-4.200e+01 ", sprintf("%0-15.3e", -42.)); + assertSprintf("42", "%-d", 42); + assertSprintf("-42", "%-d", -42); + assertSprintf("42 ", "%-5d", 42); + assertSprintf("-42 ", "%-5d", -42); + assertSprintf("42 ", "%-15d", 42); + assertSprintf("-42 ", "%-15d", -42); + assertSprintf("42", "%-0d", 42); + assertSprintf("-42", "%-0d", -42); + assertSprintf("42 ", "%-05d", 42); + assertSprintf("-42 ", "%-05d", -42); + assertSprintf("42 ", "%-015d", 42); + assertSprintf("-42 ", "%-015d", -42); + assertSprintf("42", "%0-d", 42); + assertSprintf("-42", "%0-d", -42); + assertSprintf("42 ", "%0-5d", 42); + assertSprintf("-42 ", "%0-5d", -42); + assertSprintf("42 ", "%0-15d", 42); + assertSprintf("-42 ", "%0-15d", -42); + assertSprintf("-4.200e+01 ", "%0-15.3e", -42.); // Printf4J expected "-42.0 ": AWK's %g removes trailing // zeros, so gawk prints "-42 ". - assertEquals("-42 ", sprintf("%0-15.3g", -42.)); + assertSprintf("-42 ", "%0-15.3g", -42.); } @Test public void testHash() { // Printf4J expected "" here, but gawk prints "0" for a zero value // with '#' and a zero precision on %x. - assertEquals("0", sprintf("%#.0x", 0)); + assertSprintf("0", "%#.0x", 0); // Printf4J had this assertion commented out as "the real expected // behavior, which is wrong IMO" (it returned "0x0" instead): C and // gawk agree on "0", which is what AwkPrintf now produces. - assertEquals("0", sprintf("%#.1x", 0)); + assertSprintf("0", "%#.1x", 0); // "%#.0llx" is invalid in gawk: doubled length modifiers make the // whole specifier print verbatim, without consuming an argument. - assertEquals("%#.0llx", sprintf("%#.0llx", 0)); - assertEquals("0x0000614e", sprintf("%#.8x", 0x614e)); + assertSprintf("%#.0llx", "%#.0llx", 0); + assertSprintf("0x0000614e", "%#.8x", 0x614e); // Was commented out in Printf4J ("binary is not supported for now"): // %b is not an AWK conversion, so gawk prints the specifier verbatim. - assertEquals("%#b", sprintf("%#b", 6)); + assertSprintf("%#b", "%#b", 6); } @Test public void testSpecifier() { - assertEquals("Hello testing", sprintf("Hello testing")); - assertEquals("Hello testing", sprintf("%s", "Hello testing")); - assertEquals("1024", sprintf("%d", 1024)); - assertEquals("-1024", sprintf("%d", -1024)); - assertEquals("1024", sprintf("%i", 1024)); - assertEquals("-1024", sprintf("%i", -1024)); - assertEquals("1024", sprintf("%u", 1024)); - assertEquals("4294966272", sprintf("%u", 4294966272L)); - assertEquals("777", sprintf("%o", 511)); - assertEquals("37777777001", sprintf("%o", 4294966785L)); - assertEquals("1234abcd", sprintf("%x", 305441741)); - assertEquals("edcb5433", sprintf("%x", 3989525555L)); - assertEquals("1234ABCD", sprintf("%X", 305441741)); - assertEquals("EDCB5433", sprintf("%X", 3989525555L)); - assertEquals("%", sprintf("%%")); + assertSprintf("Hello testing", "Hello testing"); + assertSprintf("Hello testing", "%s", "Hello testing"); + assertSprintf("1024", "%d", 1024); + assertSprintf("-1024", "%d", -1024); + assertSprintf("1024", "%i", 1024); + assertSprintf("-1024", "%i", -1024); + assertSprintf("1024", "%u", 1024); + assertSprintf("4294966272", "%u", 4294966272L); + assertSprintf("777", "%o", 511); + assertSprintf("37777777001", "%o", 4294966785L); + assertSprintf("1234abcd", "%x", 305441741); + assertSprintf("edcb5433", "%x", 3989525555L); + assertSprintf("1234ABCD", "%X", 305441741); + assertSprintf("EDCB5433", "%X", 3989525555L); + assertSprintf("%", "%%"); } @Test public void testWidth() { - assertEquals("Hello testing", sprintf("%1s", "Hello testing")); - assertEquals("1024", sprintf("%1d", 1024)); - assertEquals("-1024", sprintf("%1d", -1024)); - assertEquals("1024", sprintf("%1i", 1024)); - assertEquals("-1024", sprintf("%1i", -1024)); - assertEquals("1024", sprintf("%1u", 1024)); - assertEquals("4294966272", sprintf("%1u", 4294966272L)); - assertEquals("777", sprintf("%1o", 511)); - assertEquals("37777777001", sprintf("%1o", 4294966785L)); - assertEquals("1234abcd", sprintf("%1x", 305441741)); - assertEquals("edcb5433", sprintf("%1x", 3989525555L)); - assertEquals("1234ABCD", sprintf("%1X", 305441741)); - assertEquals("EDCB5433", sprintf("%1X", 3989525555L)); - assertEquals("x", sprintf("%1c", 'x')); + assertSprintf("Hello testing", "%1s", "Hello testing"); + assertSprintf("1024", "%1d", 1024); + assertSprintf("-1024", "%1d", -1024); + assertSprintf("1024", "%1i", 1024); + assertSprintf("-1024", "%1i", -1024); + assertSprintf("1024", "%1u", 1024); + assertSprintf("4294966272", "%1u", 4294966272L); + assertSprintf("777", "%1o", 511); + assertSprintf("37777777001", "%1o", 4294966785L); + assertSprintf("1234abcd", "%1x", 305441741); + assertSprintf("edcb5433", "%1x", 3989525555L); + assertSprintf("1234ABCD", "%1X", 305441741); + assertSprintf("EDCB5433", "%1X", 3989525555L); + assertSprintf("x", "%1c", 'x'); } @Test public void testWidth20() { - assertEquals(" Hello", sprintf("%20s", "Hello")); - assertEquals(" 1024", sprintf("%20d", 1024)); - assertEquals(" -1024", sprintf("%20d", -1024)); - assertEquals(" 1024", sprintf("%20i", 1024)); - assertEquals(" -1024", sprintf("%20i", -1024)); - assertEquals(" 1024", sprintf("%20u", 1024)); - assertEquals(" 4294966272", sprintf("%20u", 4294966272L)); - assertEquals(" 777", sprintf("%20o", 511)); - assertEquals(" 37777777001", sprintf("%20o", 4294966785L)); - assertEquals(" 1234abcd", sprintf("%20x", 305441741)); - assertEquals(" edcb5433", sprintf("%20x", 3989525555L)); - assertEquals(" 1234ABCD", sprintf("%20X", 305441741)); - assertEquals(" EDCB5433", sprintf("%20X", 3989525555L)); - assertEquals(" x", sprintf("%20c", 'x')); + assertSprintf(" Hello", "%20s", "Hello"); + assertSprintf(" 1024", "%20d", 1024); + assertSprintf(" -1024", "%20d", -1024); + assertSprintf(" 1024", "%20i", 1024); + assertSprintf(" -1024", "%20i", -1024); + assertSprintf(" 1024", "%20u", 1024); + assertSprintf(" 4294966272", "%20u", 4294966272L); + assertSprintf(" 777", "%20o", 511); + assertSprintf(" 37777777001", "%20o", 4294966785L); + assertSprintf(" 1234abcd", "%20x", 305441741); + assertSprintf(" edcb5433", "%20x", 3989525555L); + assertSprintf(" 1234ABCD", "%20X", 305441741); + assertSprintf(" EDCB5433", "%20X", 3989525555L); + assertSprintf(" x", "%20c", 'x'); } @Test public void testWidthStar20() { - assertEquals(" Hello", sprintf("%*s", 20, "Hello")); - assertEquals(" 1024", sprintf("%*d", 20, 1024)); - assertEquals(" -1024", sprintf("%*d", 20, -1024)); - assertEquals(" 1024", sprintf("%*i", 20, 1024)); - assertEquals(" -1024", sprintf("%*i", 20, -1024)); - assertEquals(" 1024", sprintf("%*u", 20, 1024)); - assertEquals(" 4294966272", sprintf("%*u", 20, 4294966272L)); - assertEquals(" 777", sprintf("%*o", 20, 511)); - assertEquals(" 37777777001", sprintf("%*o", 20, 4294966785L)); - assertEquals(" 1234abcd", sprintf("%*x", 20, 305441741)); - assertEquals(" edcb5433", sprintf("%*x", 20, 3989525555L)); - assertEquals(" 1234ABCD", sprintf("%*X", 20, 305441741)); - assertEquals(" EDCB5433", sprintf("%*X", 20, 3989525555L)); - assertEquals(" x", sprintf("%*c", 20, 'x')); + assertSprintf(" Hello", "%*s", 20, "Hello"); + assertSprintf(" 1024", "%*d", 20, 1024); + assertSprintf(" -1024", "%*d", 20, -1024); + assertSprintf(" 1024", "%*i", 20, 1024); + assertSprintf(" -1024", "%*i", 20, -1024); + assertSprintf(" 1024", "%*u", 20, 1024); + assertSprintf(" 4294966272", "%*u", 20, 4294966272L); + assertSprintf(" 777", "%*o", 20, 511); + assertSprintf(" 37777777001", "%*o", 20, 4294966785L); + assertSprintf(" 1234abcd", "%*x", 20, 305441741); + assertSprintf(" edcb5433", "%*x", 20, 3989525555L); + assertSprintf(" 1234ABCD", "%*X", 20, 305441741); + assertSprintf(" EDCB5433", "%*X", 20, 3989525555L); + assertSprintf(" x", "%*c", 20, 'x'); } @Test public void testMinus20() { - assertEquals("Hello ", sprintf("%-20s", "Hello")); - assertEquals("1024 ", sprintf("%-20d", 1024)); - assertEquals("-1024 ", sprintf("%-20d", -1024)); - assertEquals("1024 ", sprintf("%-20i", 1024)); - assertEquals("-1024 ", sprintf("%-20i", -1024)); - assertEquals("1024 ", sprintf("%-20u", 1024)); - assertEquals("1024.1234 ", sprintf("%-20.4f", 1024.1234)); - assertEquals("4294966272 ", sprintf("%-20u", 4294966272L)); - assertEquals("777 ", sprintf("%-20o", 511)); - assertEquals("37777777001 ", sprintf("%-20o", 4294966785L)); - assertEquals("1234abcd ", sprintf("%-20x", 305441741)); - assertEquals("edcb5433 ", sprintf("%-20x", 3989525555L)); - assertEquals("1234ABCD ", sprintf("%-20X", 305441741)); - assertEquals("EDCB5433 ", sprintf("%-20X", 3989525555L)); - assertEquals("x ", sprintf("%-20c", 'x')); - assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-2d| |%5d|", 9, 9, 9)); - assertEquals("| 10| |10| | 10|", sprintf("|%5d| |%-2d| |%5d|", 10, 10, 10)); - assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-12d| |%5d|", 9, 9, 9)); - assertEquals("| 10| |10 | | 10|", sprintf("|%5d| |%-12d| |%5d|", 10, 10, 10)); + assertSprintf("Hello ", "%-20s", "Hello"); + assertSprintf("1024 ", "%-20d", 1024); + assertSprintf("-1024 ", "%-20d", -1024); + assertSprintf("1024 ", "%-20i", 1024); + assertSprintf("-1024 ", "%-20i", -1024); + assertSprintf("1024 ", "%-20u", 1024); + assertSprintf("1024.1234 ", "%-20.4f", 1024.1234); + assertSprintf("4294966272 ", "%-20u", 4294966272L); + assertSprintf("777 ", "%-20o", 511); + assertSprintf("37777777001 ", "%-20o", 4294966785L); + assertSprintf("1234abcd ", "%-20x", 305441741); + assertSprintf("edcb5433 ", "%-20x", 3989525555L); + assertSprintf("1234ABCD ", "%-20X", 305441741); + assertSprintf("EDCB5433 ", "%-20X", 3989525555L); + assertSprintf("x ", "%-20c", 'x'); + assertSprintf("| 9| |9 | | 9|", "|%5d| |%-2d| |%5d|", 9, 9, 9); + assertSprintf("| 10| |10| | 10|", "|%5d| |%-2d| |%5d|", 10, 10, 10); + assertSprintf("| 9| |9 | | 9|", "|%5d| |%-12d| |%5d|", 9, 9, 9); + assertSprintf("| 10| |10 | | 10|", "|%5d| |%-12d| |%5d|", 10, 10, 10); } @Test public void testZeroMinus20() { - assertEquals("Hello ", sprintf("%0-20s", "Hello")); - assertEquals("1024 ", sprintf("%0-20d", 1024)); - assertEquals("-1024 ", sprintf("%0-20d", -1024)); - assertEquals("1024 ", sprintf("%0-20i", 1024)); - assertEquals("-1024 ", sprintf("%0-20i", -1024)); - assertEquals("1024 ", sprintf("%0-20u", 1024)); - assertEquals("4294966272 ", sprintf("%0-20u", 4294966272L)); - assertEquals("777 ", sprintf("%0-20o", 511)); - assertEquals("37777777001 ", sprintf("%0-20o", 4294966785L)); - assertEquals("1234abcd ", sprintf("%0-20x", 305441741)); - assertEquals("edcb5433 ", sprintf("%0-20x", 3989525555L)); - assertEquals("1234ABCD ", sprintf("%0-20X", 305441741)); - assertEquals("EDCB5433 ", sprintf("%0-20X", 3989525555L)); - assertEquals("x ", sprintf("%0-20c", 'x')); + assertSprintf("Hello ", "%0-20s", "Hello"); + assertSprintf("1024 ", "%0-20d", 1024); + assertSprintf("-1024 ", "%0-20d", -1024); + assertSprintf("1024 ", "%0-20i", 1024); + assertSprintf("-1024 ", "%0-20i", -1024); + assertSprintf("1024 ", "%0-20u", 1024); + assertSprintf("4294966272 ", "%0-20u", 4294966272L); + assertSprintf("777 ", "%0-20o", 511); + assertSprintf("37777777001 ", "%0-20o", 4294966785L); + assertSprintf("1234abcd ", "%0-20x", 305441741); + assertSprintf("edcb5433 ", "%0-20x", 3989525555L); + assertSprintf("1234ABCD ", "%0-20X", 305441741); + assertSprintf("EDCB5433 ", "%0-20X", 3989525555L); + assertSprintf("x ", "%0-20c", 'x'); } @Test public void testPadding20() { - assertEquals("00000000000000001024", sprintf("%020d", 1024)); - assertEquals("-0000000000000001024", sprintf("%020d", -1024)); - assertEquals("00000000000000001024", sprintf("%020i", 1024)); - assertEquals("-0000000000000001024", sprintf("%020i", -1024)); - assertEquals("00000000000000001024", sprintf("%020u", 1024)); - assertEquals("00000000004294966272", sprintf("%020u", 4294966272L)); - assertEquals("00000000000000000777", sprintf("%020o", 511)); - assertEquals("00000000037777777001", sprintf("%020o", 4294966785L)); - assertEquals("0000000000001234abcd", sprintf("%020x", 305441741)); - assertEquals("000000000000edcb5433", sprintf("%020x", 3989525555L)); - assertEquals("0000000000001234ABCD", sprintf("%020X", 305441741)); - assertEquals("000000000000EDCB5433", sprintf("%020X", 3989525555L)); + assertSprintf("00000000000000001024", "%020d", 1024); + assertSprintf("-0000000000000001024", "%020d", -1024); + assertSprintf("00000000000000001024", "%020i", 1024); + assertSprintf("-0000000000000001024", "%020i", -1024); + assertSprintf("00000000000000001024", "%020u", 1024); + assertSprintf("00000000004294966272", "%020u", 4294966272L); + assertSprintf("00000000000000000777", "%020o", 511); + assertSprintf("00000000037777777001", "%020o", 4294966785L); + assertSprintf("0000000000001234abcd", "%020x", 305441741); + assertSprintf("000000000000edcb5433", "%020x", 3989525555L); + assertSprintf("0000000000001234ABCD", "%020X", 305441741); + assertSprintf("000000000000EDCB5433", "%020X", 3989525555L); } @Test public void testPaddingPrecision20() { - assertEquals("00000000000000001024", sprintf("%.20d", 1024)); - assertEquals("-00000000000000001024", sprintf("%.20d", -1024)); - assertEquals("00000000000000001024", sprintf("%.20i", 1024)); - assertEquals("-00000000000000001024", sprintf("%.20i", -1024)); - assertEquals("00000000000000001024", sprintf("%.20u", 1024)); - assertEquals("00000000004294966272", sprintf("%.20u", 4294966272L)); - assertEquals("00000000000000000777", sprintf("%.20o", 511)); - assertEquals("00000000037777777001", sprintf("%.20o", 4294966785L)); - assertEquals("0000000000001234abcd", sprintf("%.20x", 305441741)); - assertEquals("000000000000edcb5433", sprintf("%.20x", 3989525555L)); - assertEquals("0000000000001234ABCD", sprintf("%.20X", 305441741)); - assertEquals("000000000000EDCB5433", sprintf("%.20X", 3989525555L)); + assertSprintf("00000000000000001024", "%.20d", 1024); + assertSprintf("-00000000000000001024", "%.20d", -1024); + assertSprintf("00000000000000001024", "%.20i", 1024); + assertSprintf("-00000000000000001024", "%.20i", -1024); + assertSprintf("00000000000000001024", "%.20u", 1024); + assertSprintf("00000000004294966272", "%.20u", 4294966272L); + assertSprintf("00000000000000000777", "%.20o", 511); + assertSprintf("00000000037777777001", "%.20o", 4294966785L); + assertSprintf("0000000000001234abcd", "%.20x", 305441741); + assertSprintf("000000000000edcb5433", "%.20x", 3989525555L); + assertSprintf("0000000000001234ABCD", "%.20X", 305441741); + assertSprintf("000000000000EDCB5433", "%.20X", 3989525555L); } @Test public void testPaddingHashZero20() { - assertEquals("00000000000000001024", sprintf("%#020d", 1024)); - assertEquals("-0000000000000001024", sprintf("%#020d", -1024)); - assertEquals("00000000000000001024", sprintf("%#020i", 1024)); - assertEquals("-0000000000000001024", sprintf("%#020i", -1024)); - assertEquals("00000000000000001024", sprintf("%#020u", 1024)); - assertEquals("00000000004294966272", sprintf("%#020u", 4294966272L)); - assertEquals("00000000000000000777", sprintf("%#020o", 511)); - assertEquals("00000000037777777001", sprintf("%#020o", 4294966785L)); - assertEquals("0x00000000001234abcd", sprintf("%#020x", 305441741)); - assertEquals("0x0000000000edcb5433", sprintf("%#020x", 3989525555L)); - assertEquals("0X00000000001234ABCD", sprintf("%#020X", 305441741)); - assertEquals("0X0000000000EDCB5433", sprintf("%#020X", 3989525555L)); + assertSprintf("00000000000000001024", "%#020d", 1024); + assertSprintf("-0000000000000001024", "%#020d", -1024); + assertSprintf("00000000000000001024", "%#020i", 1024); + assertSprintf("-0000000000000001024", "%#020i", -1024); + assertSprintf("00000000000000001024", "%#020u", 1024); + assertSprintf("00000000004294966272", "%#020u", 4294966272L); + assertSprintf("00000000000000000777", "%#020o", 511); + assertSprintf("00000000037777777001", "%#020o", 4294966785L); + assertSprintf("0x00000000001234abcd", "%#020x", 305441741); + assertSprintf("0x0000000000edcb5433", "%#020x", 3989525555L); + assertSprintf("0X00000000001234ABCD", "%#020X", 305441741); + assertSprintf("0X0000000000EDCB5433", "%#020X", 3989525555L); } @Test public void testPaddingHash20() { - assertEquals(" 1024", sprintf("%#20d", 1024)); - assertEquals(" -1024", sprintf("%#20d", -1024)); - assertEquals(" 1024", sprintf("%#20i", 1024)); - assertEquals(" -1024", sprintf("%#20i", -1024)); - assertEquals(" 1024", sprintf("%#20u", 1024)); - assertEquals(" 4294966272", sprintf("%#20u", 4294966272L)); + assertSprintf(" 1024", "%#20d", 1024); + assertSprintf(" -1024", "%#20d", -1024); + assertSprintf(" 1024", "%#20i", 1024); + assertSprintf(" -1024", "%#20i", -1024); + assertSprintf(" 1024", "%#20u", 1024); + assertSprintf(" 4294966272", "%#20u", 4294966272L); // The following assertions were commented out in Printf4J; they match // C and gawk, and now pass. - assertEquals(" 0777", sprintf("%#20o", 511)); - assertEquals(" 037777777001", sprintf("%#20o", 4294966785L)); - assertEquals(" 0x1234abcd", sprintf("%#20x", 305441741)); - assertEquals(" 0xedcb5433", sprintf("%#20x", 3989525555L)); - assertEquals(" 0X1234ABCD", sprintf("%#20X", 305441741)); - assertEquals(" 0XEDCB5433", sprintf("%#20X", 3989525555L)); + assertSprintf(" 0777", "%#20o", 511); + assertSprintf(" 037777777001", "%#20o", 4294966785L); + assertSprintf(" 0x1234abcd", "%#20x", 305441741); + assertSprintf(" 0xedcb5433", "%#20x", 3989525555L); + assertSprintf(" 0X1234ABCD", "%#20X", 305441741); + assertSprintf(" 0XEDCB5433", "%#20X", 3989525555L); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testPadding20Dot5() { - assertEquals(" 01024", sprintf("%20.5d", 1024)); - assertEquals(" -01024", sprintf("%20.5d", -1024)); - assertEquals(" 01024", sprintf("%20.5i", 1024)); - assertEquals(" -01024", sprintf("%20.5i", -1024)); - assertEquals(" 01024", sprintf("%20.5u", 1024)); - assertEquals(" 4294966272", sprintf("%20.5u", 4294966272L)); - assertEquals(" 00777", sprintf("%20.5o", 511)); - assertEquals(" 37777777001", sprintf("%20.5o", 4294966785L)); - assertEquals(" 1234abcd", sprintf("%20.5x", 305441741)); - assertEquals(" 00edcb5433", sprintf("%20.10x", 3989525555L)); - assertEquals(" 1234ABCD", sprintf("%20.5X", 305441741)); - assertEquals(" 00EDCB5433", sprintf("%20.10X", 3989525555L)); + assertSprintf(" 01024", "%20.5d", 1024); + assertSprintf(" -01024", "%20.5d", -1024); + assertSprintf(" 01024", "%20.5i", 1024); + assertSprintf(" -01024", "%20.5i", -1024); + assertSprintf(" 01024", "%20.5u", 1024); + assertSprintf(" 4294966272", "%20.5u", 4294966272L); + assertSprintf(" 00777", "%20.5o", 511); + assertSprintf(" 37777777001", "%20.5o", 4294966785L); + assertSprintf(" 1234abcd", "%20.5x", 305441741); + assertSprintf(" 00edcb5433", "%20.10x", 3989525555L); + assertSprintf(" 1234ABCD", "%20.5X", 305441741); + assertSprintf(" 00EDCB5433", "%20.10X", 3989525555L); } // Was @Disabled in Printf4J; matches C and gawk. @Test public void testPaddingNegativeNumbers() { // space padding - assertEquals("-5", sprintf("% 1d", -5)); - assertEquals("-5", sprintf("% 2d", -5)); - assertEquals(" -5", sprintf("% 3d", -5)); - assertEquals(" -5", sprintf("% 4d", -5)); + assertSprintf("-5", "% 1d", -5); + assertSprintf("-5", "% 2d", -5); + assertSprintf(" -5", "% 3d", -5); + assertSprintf(" -5", "% 4d", -5); // zero padding - assertEquals("-5", sprintf("%01d", -5)); - assertEquals("-5", sprintf("%02d", -5)); - assertEquals("-05", sprintf("%03d", -5)); - assertEquals("-005", sprintf("%04d", -5)); + assertSprintf("-5", "%01d", -5); + assertSprintf("-5", "%02d", -5); + assertSprintf("-05", "%03d", -5); + assertSprintf("-005", "%04d", -5); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testPaddingNegativeFloat() { // space padding - assertEquals("-5.0", sprintf("% 3.1f", -5.)); - assertEquals("-5.0", sprintf("% 4.1f", -5.)); - assertEquals(" -5.0", sprintf("% 5.1f", -5.)); - assertEquals(" -5", sprintf("% 6.1g", -5.)); - assertEquals("-5.0e+00", sprintf("% 6.1e", -5.)); - assertEquals(" -5.0e+00", sprintf("% 10.1e", -5.)); + assertSprintf("-5.0", "% 3.1f", -5.); + assertSprintf("-5.0", "% 4.1f", -5.); + assertSprintf(" -5.0", "% 5.1f", -5.); + assertSprintf(" -5", "% 6.1g", -5.); + assertSprintf("-5.0e+00", "% 6.1e", -5.); + assertSprintf(" -5.0e+00", "% 10.1e", -5.); // zero padding - assertEquals("-5.0", sprintf("%03.1f", -5.)); - assertEquals("-5.0", sprintf("%04.1f", -5.)); - assertEquals("-05.0", sprintf("%05.1f", -5.)); + assertSprintf("-5.0", "%03.1f", -5.); + assertSprintf("-5.0", "%04.1f", -5.); + assertSprintf("-05.0", "%05.1f", -5.); // zero padding no decimal point - assertEquals("-5", sprintf("%01.0f", -5.)); - assertEquals("-5", sprintf("%02.0f", -5.)); - assertEquals("-05", sprintf("%03.0f", -5.)); - assertEquals("-005.0e+00", sprintf("%010.1e", -5.)); - assertEquals("-05E+00", sprintf("%07.0E", -5.)); - assertEquals("-05", sprintf("%03.0g", -5.)); + assertSprintf("-5", "%01.0f", -5.); + assertSprintf("-5", "%02.0f", -5.); + assertSprintf("-05", "%03.0f", -5.); + assertSprintf("-005.0e+00", "%010.1e", -5.); + assertSprintf("-05E+00", "%07.0E", -5.); + assertSprintf("-05", "%03.0g", -5.); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testLength() { - assertEquals("", sprintf("%.0s", "Hello testing")); - assertEquals(" ", sprintf("%20.0s", "Hello testing")); - assertEquals("", sprintf("%.s", "Hello testing")); - assertEquals(" ", sprintf("%20.s", "Hello testing")); - assertEquals(" 1024", sprintf("%20.0d", 1024)); - assertEquals(" -1024", sprintf("%20.0d", -1024)); - assertEquals(" ", sprintf("%20.d", 0)); - assertEquals(" 1024", sprintf("%20.0i", 1024)); - assertEquals(" -1024", sprintf("%20.i", -1024)); - assertEquals(" ", sprintf("%20.i", 0)); - assertEquals(" 1024", sprintf("%20.u", 1024)); - assertEquals(" 4294966272", sprintf("%20.0u", 4294966272L)); - assertEquals(" ", sprintf("%20.u", 0L)); - assertEquals(" 777", sprintf("%20.o", 511)); - assertEquals(" 37777777001", sprintf("%20.0o", 4294966785L)); - assertEquals(" ", sprintf("%20.o", 0L)); - assertEquals(" 1234abcd", sprintf("%20.x", 305441741)); - assertEquals(" 1234abcd", sprintf("%50.x", 305441741)); - assertEquals( - " 1234abcd 12345", - sprintf("%50.x%10.u", 305441741, 12345)); - assertEquals(" edcb5433", sprintf("%20.0x", 3989525555L)); - assertEquals(" ", sprintf("%20.x", 0L)); - assertEquals(" 1234ABCD", sprintf("%20.X", 305441741)); - assertEquals(" EDCB5433", sprintf("%20.0X", 3989525555L)); - assertEquals(" ", sprintf("%20.X", 0L)); - assertEquals(" ", sprintf("%02.0u", 0L)); - assertEquals(" ", sprintf("%02.0d", 0)); + assertSprintf("", "%.0s", "Hello testing"); + assertSprintf(" ", "%20.0s", "Hello testing"); + assertSprintf("", "%.s", "Hello testing"); + assertSprintf(" ", "%20.s", "Hello testing"); + assertSprintf(" 1024", "%20.0d", 1024); + assertSprintf(" -1024", "%20.0d", -1024); + assertSprintf(" ", "%20.d", 0); + assertSprintf(" 1024", "%20.0i", 1024); + assertSprintf(" -1024", "%20.i", -1024); + assertSprintf(" ", "%20.i", 0); + assertSprintf(" 1024", "%20.u", 1024); + assertSprintf(" 4294966272", "%20.0u", 4294966272L); + assertSprintf(" ", "%20.u", 0L); + assertSprintf(" 777", "%20.o", 511); + assertSprintf(" 37777777001", "%20.0o", 4294966785L); + assertSprintf(" ", "%20.o", 0L); + assertSprintf(" 1234abcd", "%20.x", 305441741); + assertSprintf(" 1234abcd", "%50.x", 305441741); + assertSprintf(" 1234abcd 12345", "%50.x%10.u", 305441741, 12345); + assertSprintf(" edcb5433", "%20.0x", 3989525555L); + assertSprintf(" ", "%20.x", 0L); + assertSprintf(" 1234ABCD", "%20.X", 305441741); + assertSprintf(" EDCB5433", "%20.0X", 3989525555L); + assertSprintf(" ", "%20.X", 0L); + assertSprintf(" ", "%02.0u", 0L); + assertSprintf(" ", "%02.0d", 0); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testFloat() { // test special-case floats - assertEquals(" nan", sprintf("%8f", Float.NaN)); - assertEquals(" inf", sprintf("%8f", Float.POSITIVE_INFINITY)); - assertEquals("-inf ", sprintf("%-8f", Float.NEGATIVE_INFINITY)); - assertEquals(" +inf", sprintf("%+8e", Float.POSITIVE_INFINITY)); - assertEquals("3.1415", sprintf("%.4f", 3.1415354)); - assertEquals("30343.142", sprintf("%.3f", 30343.1415354)); - assertEquals("34", sprintf("%.0f", 34.1415354)); - assertEquals("1", sprintf("%.0f", 1.3)); - assertEquals("2", sprintf("%.0f", 1.55)); - assertEquals("1.6", sprintf("%.1f", 1.64)); - assertEquals("42.90", sprintf("%.2f", 42.8952)); - assertEquals("42.895200000", sprintf("%.9f", 42.8952)); - assertEquals("42.8952230000", sprintf("%.10f", 42.895223)); + assertSprintf(" nan", "%8f", Float.NaN); + assertSprintf(" inf", "%8f", Float.POSITIVE_INFINITY); + assertSprintf("-inf ", "%-8f", Float.NEGATIVE_INFINITY); + assertSprintf(" +inf", "%+8e", Float.POSITIVE_INFINITY); + assertSprintf("3.1415", "%.4f", 3.1415354); + assertSprintf("30343.142", "%.3f", 30343.1415354); + assertSprintf("34", "%.0f", 34.1415354); + assertSprintf("1", "%.0f", 1.3); + assertSprintf("2", "%.0f", 1.55); + assertSprintf("1.6", "%.1f", 1.64); + assertSprintf("42.90", "%.2f", 42.8952); + assertSprintf("42.895200000", "%.9f", 42.8952); + assertSprintf("42.8952230000", "%.10f", 42.895223); // Printf4J expected "42.895223123000" and "42.895223877000" here // because its reference implementation truncated to 9 significant // fraction digits; gawk prints the correctly rounded values. - assertEquals("42.895223123457", sprintf("%.12f", 42.89522312345678)); - assertEquals("42.895223876543", sprintf("%.12f", 42.89522387654321)); - assertEquals(" 42.90", sprintf("%6.2f", 42.8952)); - assertEquals("+42.90", sprintf("%+6.2f", 42.8952)); - assertEquals("+42.9", sprintf("%+5.1f", 42.9252)); - assertEquals("42.500000", sprintf("%f", 42.5)); - assertEquals("42.5", sprintf("%.1f", 42.5)); - assertEquals("42167.000000", sprintf("%f", 42167.0)); - assertEquals("-12345.987654321", sprintf("%.9f", -12345.987654321)); - assertEquals("4.0", sprintf("%.1f", 3.999)); - assertEquals("4", sprintf("%.0f", 3.5)); - assertEquals("4", sprintf("%.0f", 4.5)); - assertEquals("3", sprintf("%.0f", 3.49)); - assertEquals("3.5", sprintf("%.1f", 3.49)); - assertEquals("a0.5 ", sprintf("a%-5.1f", 0.5)); - assertEquals("a0.5 end", sprintf("a%-5.1fend", 0.5)); - assertEquals("12345.7", sprintf("%G", 12345.678)); - assertEquals("12345.68", sprintf("%.7G", 12345.678)); - assertEquals("1.2346E+08", sprintf("%.5G", 123456789.)); + assertSprintf("42.895223123457", "%.12f", 42.89522312345678); + assertSprintf("42.895223876543", "%.12f", 42.89522387654321); + assertSprintf(" 42.90", "%6.2f", 42.8952); + assertSprintf("+42.90", "%+6.2f", 42.8952); + assertSprintf("+42.9", "%+5.1f", 42.9252); + assertSprintf("42.500000", "%f", 42.5); + assertSprintf("42.5", "%.1f", 42.5); + assertSprintf("42167.000000", "%f", 42167.0); + assertSprintf("-12345.987654321", "%.9f", -12345.987654321); + assertSprintf("4.0", "%.1f", 3.999); + assertSprintf("4", "%.0f", 3.5); + assertSprintf("4", "%.0f", 4.5); + assertSprintf("3", "%.0f", 3.49); + assertSprintf("3.5", "%.1f", 3.49); + assertSprintf("a0.5 ", "a%-5.1f", 0.5); + assertSprintf("a0.5 end", "a%-5.1fend", 0.5); + assertSprintf("12345.7", "%G", 12345.678); + assertSprintf("12345.68", "%.7G", 12345.678); + assertSprintf("1.2346E+08", "%.5G", 123456789.); // Printf4J expected "12345.0": AWK's %G removes trailing zeros. - assertEquals("12345", sprintf("%.6G", 12345.)); - assertEquals(" +1.235e+08", sprintf("%+12.4g", 123456789.)); - assertEquals("0.0012", sprintf("%.2G", 0.001234)); - assertEquals(" +0.001234", sprintf("%+10.4G", 0.001234)); - assertEquals("+001.234e-05", sprintf("%+012.4g", 0.00001234)); - assertEquals("-1.23e-308", sprintf("%.3g", -1.2345e-308)); - assertEquals("+1.230E+308", sprintf("%+.3E", 1.23e+308)); + assertSprintf("12345", "%.6G", 12345.); + assertSprintf(" +1.235e+08", "%+12.4g", 123456789.); + assertSprintf("0.0012", "%.2G", 0.001234); + assertSprintf(" +0.001234", "%+10.4G", 0.001234); + assertSprintf("+001.234e-05", "%+012.4g", 0.00001234); + assertSprintf("-1.23e-308", "%.3g", -1.2345e-308); + assertSprintf("+1.230E+308", "%+.3E", 1.23e+308); // Printf4J expected "1.0e+20" (its reference implementation switched // to exponential notation out of range); gawk prints the full value. - assertEquals("100000000000000000000.0", sprintf("%.1f", 1E20)); + assertSprintf("100000000000000000000.0", "%.1f", 1E20); } // Was @Disabled in Printf4J; expected values verified against gawk 5, @@ -478,115 +480,115 @@ public void testFloat() { // prints any other modifier combination verbatim. @Test public void testTypes() { - assertEquals("0", sprintf("%i", 0)); - assertEquals("1234", sprintf("%i", 1234)); - assertEquals("32767", sprintf("%i", 32767)); - assertEquals("-32767", sprintf("%i", -32767)); - assertEquals("30", sprintf("%li", 30L)); - assertEquals("-2147483647", sprintf("%li", -2147483647L)); - assertEquals("2147483647", sprintf("%li", 2147483647L)); + assertSprintf("0", "%i", 0); + assertSprintf("1234", "%i", 1234); + assertSprintf("32767", "%i", 32767); + assertSprintf("-32767", "%i", -32767); + assertSprintf("30", "%li", 30L); + assertSprintf("-2147483647", "%li", -2147483647L); + assertSprintf("2147483647", "%li", 2147483647L); // Doubled modifiers ("ll", "hh") and the "q", "j", "z", and "t" // modifiers are not valid in gawk: the specifier prints verbatim and // consumes no argument. - assertEquals("%lli", sprintf("%lli", 30L)); - assertEquals("%lli", sprintf("%lli", -9223372036854775807L)); - assertEquals("%lli", sprintf("%lli", 9223372036854775807L)); - assertEquals("100000", sprintf("%lu", 100000L)); - assertEquals("4294967295", sprintf("%lu", 0xFFFFFFFFL)); - assertEquals("%llu", sprintf("%llu", 281474976710656L)); - assertEquals("%llu", sprintf("%llu", Long.parseUnsignedLong("18446744073709551615"))); - assertEquals("%zu", sprintf("%zu", 2147483647L)); - assertEquals("%zd", sprintf("%zd", 2147483647L)); - assertEquals("%zi", sprintf("%zi", -2147483647L)); + assertSprintf("%lli", "%lli", 30L); + assertSprintf("%lli", "%lli", -9223372036854775807L); + assertSprintf("%lli", "%lli", 9223372036854775807L); + assertSprintf("100000", "%lu", 100000L); + assertSprintf("4294967295", "%lu", 0xFFFFFFFFL); + assertSprintf("%llu", "%llu", 281474976710656L); + assertSprintf("%llu", "%llu", Long.parseUnsignedLong("18446744073709551615")); + assertSprintf("%zu", "%zu", 2147483647L); + assertSprintf("%zd", "%zd", 2147483647L); + assertSprintf("%zi", "%zi", -2147483647L); // %b is not an AWK conversion: printed verbatim, like gawk. - assertEquals("%b", sprintf("%b", 60000)); - assertEquals("%lb", sprintf("%lb", 12345678L)); - assertEquals("165140", sprintf("%o", 60000)); - assertEquals("57060516", sprintf("%lo", 12345678L)); - assertEquals("12345678", sprintf("%lx", 0x12345678L)); - assertEquals("%llx", sprintf("%llx", 0x1234567891234567L)); - assertEquals("abcdefab", sprintf("%lx", 0xabcdefabL)); - assertEquals("ABCDEFAB", sprintf("%lX", 0xabcdefabL)); - assertEquals("v", sprintf("%c", 'v')); - assertEquals("wv", sprintf("%cv", 'w')); - assertEquals("A Test", sprintf("%s", "A Test")); + assertSprintf("%b", "%b", 60000); + assertSprintf("%lb", "%lb", 12345678L); + assertSprintf("165140", "%o", 60000); + assertSprintf("57060516", "%lo", 12345678L); + assertSprintf("12345678", "%lx", 0x12345678L); + assertSprintf("%llx", "%llx", 0x1234567891234567L); + assertSprintf("abcdefab", "%lx", 0xabcdefabL); + assertSprintf("ABCDEFAB", "%lX", 0xabcdefabL); + assertSprintf("v", "%c", 'v'); + assertSprintf("wv", "%cv", 'w'); + assertSprintf("A Test", "%s", "A Test"); // gawk ignores the single 'h' modifier without truncating the value, // and prints the invalid "hh" specifiers verbatim. - assertEquals("%hhu", sprintf("%hhu", 0xFFFFL)); - assertEquals("13398", sprintf("%hu", 13398)); - assertEquals("1193046", sprintf("%hu", 0x123456L)); - assertEquals("Test%hhi 10000", sprintf("%s%hhi %hu", "Test", 10000, 0xFFFFFFFFL)); + assertSprintf("%hhu", "%hhu", 0xFFFFL); + assertSprintf("13398", "%hu", 13398); + assertSprintf("1193046", "%hu", 0x123456L); + assertSprintf("Test%hhi 10000", "%s%hhi %hu", "Test", 10000, 0xFFFFFFFFL); } // Was @Disabled in Printf4J, which expected "kmarco": gawk prints the // unknown "%k" specifier verbatim. @Test public void testUnknown() { - assertEquals("%kmarco", sprintf("%kmarco", 42, 37)); + assertSprintf("%kmarco", "%kmarco", 42, 37); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testStringLength() { - assertEquals("This", sprintf("%.4s", "This is a test")); - assertEquals("test", sprintf("%.4s", "test")); - assertEquals("123", sprintf("%.7s", "123")); - assertEquals("", sprintf("%.7s", "")); - assertEquals("1234ab", sprintf("%.4s%.2s", "123456", "abcdef")); + assertSprintf("This", "%.4s", "This is a test"); + assertSprintf("test", "%.4s", "test"); + assertSprintf("123", "%.7s", "123"); + assertSprintf("", "%.7s", ""); + assertSprintf("1234ab", "%.4s%.2s", "123456", "abcdef"); // Printf4J expected ".2s": gawk prints the whole invalid specifier // verbatim. - assertEquals("%.4.2s", sprintf("%.4.2s", "123456")); - assertEquals("123", sprintf("%.*s", 3, "123456")); + assertSprintf("%.4.2s", "%.4.2s", "123456"); + assertSprintf("123", "%.*s", 3, "123456"); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testMisc() { - assertEquals("53000atest-20 bit", sprintf("%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit")); - assertEquals("0.33", sprintf("%.*f", 2, 0.33333333)); - assertEquals("1", sprintf("%.*d", -1, 1)); - assertEquals("foo", sprintf("%.3s", "foobar")); + assertSprintf("53000atest-20 bit", "%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit"); + assertSprintf("0.33", "%.*f", 2, 0.33333333); + assertSprintf("1", "%.*d", -1, 1); + assertSprintf("foo", "%.3s", "foobar"); // Printf4J expected " " (glibc behavior): gawk prints nothing at all // for a zero value with zero precision, even with the space flag. - assertEquals("", sprintf("% .0d", 0)); - assertEquals(" 00004", sprintf("%10.5d", 4)); - assertEquals("hi x", sprintf("%*sx", -3, "hi")); - assertEquals("0.33", sprintf("%.*g", 2, 0.33333333)); - assertEquals("3.33e-01", sprintf("%.*e", 2, 0.33333333)); + assertSprintf("", "% .0d", 0); + assertSprintf(" 00004", "%10.5d", 4); + assertSprintf("hi x", "%*sx", -3, "hi"); + assertSprintf("0.33", "%.*g", 2, 0.33333333); + assertSprintf("3.33e-01", "%.*e", 2, 0.33333333); } @Test public void testChar() { - assertEquals("A", sprintf("%c", 65)); - assertEquals("A", sprintf("%c", 65L)); - assertEquals("A", sprintf("%c", 65.0)); - assertEquals("A", sprintf("%c", 65.1)); - assertEquals("A", sprintf("%c", Integer.valueOf(65))); - assertEquals("A", sprintf("%c", Long.valueOf(65))); - assertEquals("A", sprintf("%c", Float.valueOf(65))); - assertEquals("A", sprintf("%c", Double.valueOf(65))); - assertEquals("6", sprintf("%c", "65")); + assertSprintf("A", "%c", 65); + assertSprintf("A", "%c", 65L); + assertSprintf("A", "%c", 65.0); + assertSprintf("A", "%c", 65.1); + assertSprintf("A", "%c", Integer.valueOf(65)); + assertSprintf("A", "%c", Long.valueOf(65)); + assertSprintf("A", "%c", Float.valueOf(65)); + assertSprintf("A", "%c", Double.valueOf(65)); + assertSprintf("6", "%c", "65"); Object nothing = null; - assertEquals("\0", sprintf("%c", nothing)); + assertSprintf("\0", "%c", nothing); } // Ported from Printf4J's testToChar; AwkPrintf converts values for %c // internally, so the equivalent assertions go through sprintf(). @Test public void testToChar() { - assertEquals("A", sprintf("%c", 65)); - assertEquals("A", sprintf("%c", 65L)); - assertEquals("A", sprintf("%c", 65.0)); - assertEquals("A", sprintf("%c", 65.1)); - assertEquals("A", sprintf("%c", 65.9)); - assertEquals("A", sprintf("%c", Integer.valueOf(65))); - assertEquals("A", sprintf("%c", Long.valueOf(65))); - assertEquals("A", sprintf("%c", Float.valueOf(65))); - assertEquals("A", sprintf("%c", Double.valueOf(65))); - assertEquals("6", sprintf("%c", "65")); - assertEquals("\0", sprintf("%c", "")); + assertSprintf("A", "%c", 65); + assertSprintf("A", "%c", 65L); + assertSprintf("A", "%c", 65.0); + assertSprintf("A", "%c", 65.1); + assertSprintf("A", "%c", 65.9); + assertSprintf("A", "%c", Integer.valueOf(65)); + assertSprintf("A", "%c", Long.valueOf(65)); + assertSprintf("A", "%c", Float.valueOf(65)); + assertSprintf("A", "%c", Double.valueOf(65)); + assertSprintf("6", "%c", "65"); + assertSprintf("\0", "%c", ""); Object nothing = null; - assertEquals("\0", sprintf("%c", nothing)); + assertSprintf("\0", "%c", nothing); } // Ported from Printf4J's testToLong: the same conversion now lives in @@ -642,137 +644,174 @@ public void testToDouble() { public void testStringConversionUsesAwkNumberToStringRules() { // The symptom from issue #528: an integral double prints without a // fractional part. - assertEquals("1", sprintf("%s", 1.0)); - assertEquals("x[1]", sprintf("x[%s]", 1.0)); + assertSprintf("1", "%s", 1.0); + assertSprintf("x[1]", "x[%s]", 1.0); // Non-integral values use CONVFMT. - assertEquals("3.14159", sprintf("%s", 3.14159265)); - assertEquals("3.1", sprintf(Locale.US, "%.2g", "%s", 3.14159265)); + assertSprintf("3.14159", "%s", 3.14159265); + assertSprintf("3.1", Locale.US, "%.2g", "%s", 3.14159265); // CONVFMT that is not a %g-style format is honored verbatim. - assertEquals("3.14", sprintf(Locale.US, "%.2f", "%s", 3.14159265)); + assertSprintf("3.14", Locale.US, "%.2f", "%s", 3.14159265); // Integral values beyond the 64-bit range print in full. - assertEquals("100000000000000000000", sprintf("%s", 1e20)); + assertSprintf("100000000000000000000", "%s", 1e20); // Exact long values are preserved. - assertEquals("9223372036854775807", sprintf("%s", Long.MAX_VALUE)); + assertSprintf("9223372036854775807", "%s", Long.MAX_VALUE); } @Test public void testCharConversion() { // A numeric value selects the corresponding code point. - assertEquals("é", sprintf("%c", 233)); + assertSprintf("é", "%c", 233); // A code point beyond the BMP produces the full character. - assertEquals(new String(Character.toChars(0x1F600)), sprintf("%c", 0x1F600)); + assertSprintf(new String(Character.toChars(0x1F600)), "%c", 0x1F600); // A string value uses its first character. - assertEquals("X", sprintf("%c", "XYZ")); + assertSprintf("X", "%c", "XYZ"); // Width applies to %c like any other conversion. - assertEquals(" A", sprintf("%5c", 65)); - assertEquals("A ", sprintf("%-5c", 65)); + assertSprintf(" A", "%5c", 65); + assertSprintf("A ", "%-5c", 65); } @Test public void testDynamicWidthAndPrecision() { - assertEquals(" 3.14", sprintf("%*.*f", 8, 2, 3.14159)); - assertEquals(" 3.14159", sprintf("%9s", 3.14159)); + assertSprintf(" 3.14", "%*.*f", 8, 2, 3.14159); + assertSprintf(" 3.14159", "%9s", 3.14159); // A negative dynamic width means left justification. - assertEquals("42 ", sprintf("%*d", -6, 42)); + assertSprintf("42 ", "%*d", -6, 42); // Width and precision arguments are converted like AWK numbers. - assertEquals(" 3.14", sprintf("%*.*f", "6", "2", 3.14159)); + assertSprintf(" 3.14", "%*.*f", "6", "2", 3.14159); } @Test public void testPositionalSpecifiers() { - assertEquals("b a", sprintf("%2$s %1$s", "a", "b")); - assertEquals("a b a", sprintf("%1$s %2$s %1$s", "a", "b")); + assertSprintf("b a", "%2$s %1$s", "a", "b"); + assertSprintf("a b a", "%1$s %2$s %1$s", "a", "b"); + // Mixing positional and sequential specifiers is fatal, like gawk. + assertSprintfThrows(AwkRuntimeException.class, "%2$s %s", "a", "b"); + } + + @Test + public void testOutOfRangeFallbackKeepsSignFlagsAndPrecision() { + // gawk-verified: the %g fallback for out-of-range %u/%o/%x/%X keeps + // the sign, the precision, and the zero and '#' flags. + assertSprintf("-1.26765e+30", "%u", -Math.pow(2, 100)); + assertSprintf("1.2676506e+30", "%.10x", Math.pow(2, 100)); + assertSprintf("1.27e+30", "%#.3x", Math.pow(2, 100)); + assertSprintf("0000000001.26765e+30", "%020u", Math.pow(2, 100)); + } + + @Test + public void testAlternateFormKeepsDecimalPoint() { + // gawk-verified: '#' forces a decimal point even when no fractional + // digits remain. + assertSprintf("1.", "%#.1g", 1); + assertSprintf("1.e+04", "%#.1g", 12345); + assertSprintf("1.2e+04", "%#.2g", 12345); + assertSprintf("1.e+04", "%#.0e", 12345); + assertSprintf("1.00000", "%#g", 1); + } + + @Test + public void testZeroPrecisionZeroValue() { + // gawk-verified: unsigned conversions print "0" when a nonzero value + // truncates to zero, or when the '#' flag is given; signed %d prints + // nothing in both zero cases. + assertSprintf("0", "%.0x", 0.1); + assertSprintf("0", "%.0u", 0.1); + assertSprintf("0", "%.0o", 0.1); + assertSprintf("", "%.0d", 0.1); + assertSprintf("0", "%#.0u", 0); + assertSprintf("", "%.0u", 0); + assertSprintf("", "%.0x", 0); } @Test public void testIntegerTruncationAndConversion() { // %d truncates toward zero. - assertEquals("42", sprintf("%d", 42.7)); - assertEquals("-42", sprintf("%d", -42.7)); + assertSprintf("42", "%d", 42.7); + assertSprintf("-42", "%d", -42.7); // Strings convert with AWK's number rules (leading/trailing spaces, // exponent notation, numeric prefixes). - assertEquals("1000", sprintf("%d", "1e3")); - assertEquals("42", sprintf("%d", " 42 ")); - assertEquals("3", sprintf("%d", "+3.9")); - assertEquals("0", sprintf("%d", "abc")); - assertEquals("0", sprintf("%x", "abc")); + assertSprintf("1000", "%d", "1e3"); + assertSprintf("42", "%d", " 42 "); + assertSprintf("3", "%d", "+3.9"); + assertSprintf("0", "%d", "abc"); + assertSprintf("0", "%x", "abc"); } @Test public void testOutOfRangeIntegerConversions() { // Negative values wrap to unsigned 64-bit for %u, %o, %x. - assertEquals("18446744073709551615", sprintf("%u", -1)); - assertEquals("ffffffffffffffff", sprintf("%x", -1)); - assertEquals("1777777777777777777777", sprintf("%o", -1)); + assertSprintf("18446744073709551615", "%u", -1); + assertSprintf("ffffffffffffffff", "%x", -1); + assertSprintf("1777777777777777777777", "%o", -1); // 2^63 is out of the signed range but fits unsigned. - assertEquals("9223372036854775808", sprintf("%d", 9.223372036854775808e18)); + assertSprintf("9223372036854775808", "%d", 9.223372036854775808e18); // %d beyond 64 bits prints the full decimal expansion, like gawk. - assertEquals("1267650600228229401496703205376", sprintf("%d", Math.pow(2, 100))); - assertEquals("-1267650600228229401496703205376", sprintf("%d", -Math.pow(2, 100))); + assertSprintf("1267650600228229401496703205376", "%d", Math.pow(2, 100)); + assertSprintf("-1267650600228229401496703205376", "%d", -Math.pow(2, 100)); // %u, %o, and %x beyond 64 bits fall back to %g notation, like gawk. - assertEquals("1.26765e+30", sprintf("%x", Math.pow(2, 100))); - assertEquals("1.26765e+30", sprintf("%u", Math.pow(2, 100))); - assertEquals("1.26765e+30", sprintf("%o", Math.pow(2, 100))); + assertSprintf("1.26765e+30", "%x", Math.pow(2, 100)); + assertSprintf("1.26765e+30", "%u", Math.pow(2, 100)); + assertSprintf("1.26765e+30", "%o", Math.pow(2, 100)); } @Test public void testNonFiniteValues() { - assertEquals("nan", sprintf("%d", Double.NaN)); - assertEquals("inf", sprintf("%d", Double.POSITIVE_INFINITY)); - assertEquals("-inf", sprintf("%f", Double.NEGATIVE_INFINITY)); - assertEquals("INF", sprintf("%E", Double.POSITIVE_INFINITY)); - assertEquals("NAN", sprintf("%G", Double.NaN)); - assertEquals("nan", sprintf("%s", Double.NaN)); - assertEquals("inf", sprintf("%s", Double.POSITIVE_INFINITY)); - assertEquals("-inf", sprintf("%s", Double.NEGATIVE_INFINITY)); + assertSprintf("nan", "%d", Double.NaN); + assertSprintf("inf", "%d", Double.POSITIVE_INFINITY); + assertSprintf("-inf", "%f", Double.NEGATIVE_INFINITY); + assertSprintf("INF", "%E", Double.POSITIVE_INFINITY); + assertSprintf("NAN", "%G", Double.NaN); + assertSprintf("nan", "%s", Double.NaN); + assertSprintf("inf", "%s", Double.POSITIVE_INFINITY); + assertSprintf("-inf", "%s", Double.NEGATIVE_INFINITY); } @Test public void testUnknownSpecifiersDoNotConsumeArguments() { // The unknown %q prints verbatim and its argument feeds %d instead. - assertEquals("%q1", sprintf("%q%d", 1, 2)); + assertSprintf("%q1", "%q%d", 1, 2); // %n is not an AWK conversion (Printf4J used to print a newline). - assertEquals("a%nb", sprintf("a%nb")); + assertSprintf("a%nb", "a%nb"); // A dangling % prints verbatim. - assertEquals("abc%", sprintf("abc%")); + assertSprintf("abc%", "abc%"); } @Test public void testNotEnoughArgumentsIsFatal() { - assertThrows(AwkRuntimeException.class, () -> sprintf("%d %s", 1)); - assertThrows(AwkRuntimeException.class, () -> sprintf("%5s")); - assertThrows(AwkRuntimeException.class, () -> sprintf("%*d", 5)); + assertSprintfThrows(AwkRuntimeException.class, "%d %s", 1); + assertSprintfThrows(AwkRuntimeException.class, "%5s"); + assertSprintfThrows(AwkRuntimeException.class, "%*d", 5); } @Test public void testExtraArgumentsAreIgnored() { - assertEquals("a b", sprintf("%s %s", "a", "b", "c")); + assertSprintf("a b", "%s %s", "a", "b", "c"); } @Test public void testGroupingFlag() { - assertEquals("1,234,567", sprintf("%'d", 1234567)); - assertEquals("1,234,567.89", sprintf("%'.2f", 1234567.891)); - assertEquals("1.234.567", sprintf(Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567)); + assertSprintf("1,234,567", "%'d", 1234567); + assertSprintf("1,234,567.89", "%'.2f", 1234567.891); + assertSprintf("1.234.567", Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567); } @Test public void testLocaleDecimalSeparator() { - assertEquals("3,14", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159)); - assertEquals("3,14159", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159)); + assertSprintf("3,14", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159); + assertSprintf("3,14159", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159); } @Test public void testToAwkString() { - assertEquals("", AwkPrintf.toAwkString(null, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("text", AwkPrintf.toAwkString("text", AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("1", AwkPrintf.toAwkString(1.0, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("0.1", AwkPrintf.toAwkString(0.1, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("3.14159", AwkPrintf.toAwkString(3.14159265, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("100000000000000000000", AwkPrintf.toAwkString(1e20, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("9223372036854775807", AwkPrintf.toAwkString(Long.MAX_VALUE, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("nan", AwkPrintf.toAwkString(Double.NaN, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("inf", AwkPrintf.toAwkString(Double.POSITIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - assertEquals("-inf", AwkPrintf.toAwkString(Double.NEGATIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertToAwkString("", null); + assertToAwkString("text", "text"); + assertToAwkString("1", 1.0); + assertToAwkString("0.1", 0.1); + assertToAwkString("3.14159", 3.14159265); + assertToAwkString("100000000000000000000", 1e20); + assertToAwkString("9223372036854775807", Long.MAX_VALUE); + assertToAwkString("nan", Double.NaN); + assertToAwkString("inf", Double.POSITIVE_INFINITY); + assertToAwkString("-inf", Double.NEGATIVE_INFINITY); } } From d75fde6cd3a1c395598931c76adedf70a8447e9f Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 11 Aug 2026 23:31:55 +0200 Subject: [PATCH 03/18] Address second round of Codex review comments on AwkPrintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - %s precision counts characters (code points), so it can never split a surrogate pair: sprintf("%.1s", "😀x") returns the emoji, like gawk in a multibyte locale. - The '#' prefix for %x/%X/%o depends on the original value rather than the truncated magnitude: %#.0x of 0.1 prints "0x0" and %#x of 0.5 prints "0x0", like gawk. - gawk's alternate octal form always adds its leading zero on nonzero values, in addition to any precision padding: %#.5o of 1 prints "000001" and %#.3o of 8 prints "0010" (gawk-verified; this also covers %#.0o of 0.2 printing "00"). - A zero positional argument index (%0$s) is now fatal, like gawk ("argument index with `$' must be > 0"), both for conversions and for dynamic *n$ width/precision references. All expectations verified against gawk 5.0/5.1. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 33 ++++++++++++-------- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 17 ++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 7a1fec9c..a05cec56 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -264,6 +264,9 @@ private int formatSpecifier(int start) { } if (digitsEnd > i && digitsEnd < length && format.charAt(digitsEnd) == '$') { argPosition = parseInt(format, i, digitsEnd); + if (argPosition <= 0) { + throw new AwkRuntimeException("argument index with `$' must be > 0 in `" + format + "'"); + } i = digitsEnd + 1; } @@ -409,7 +412,10 @@ private Object nextArg() { private Object argAt(int position) { sawPositional = true; rejectMixedArgumentModes(); - if (position <= 0 || position > args.length) { + if (position <= 0) { + throw new AwkRuntimeException("argument index with `$' must be > 0 in `" + format + "'"); + } + if (position > args.length) { throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); } return args[position - 1]; @@ -432,8 +438,10 @@ private void render(char conversion, Flags flags, int width, int precision, Obje break; case 's': String s = toAwkString(arg, convfmt, locale); - if (precision >= 0 && s.length() > precision) { - s = s.substring(0, precision); + if (precision >= 0 && s.codePointCount(0, s.length()) > precision) { + // The precision counts characters (code points), so it + // can never split a surrogate pair. + s = s.substring(0, s.offsetByCodePoints(0, precision)); } appendPadded(s, flags.leftJustify, false, width); break; @@ -546,31 +554,30 @@ private void renderUnsignedInteger(char conversion, Flags flags, int width, int magnitude = magnitude.toUpperCase(Locale.ROOT); } + boolean zeroValue = d == 0; boolean zeroMagnitude = isZeroMagnitude(magnitude); int actualPrecision = precision; - if (zeroMagnitude && precision == 0 && (flags.alternate || d != 0)) { + if (zeroMagnitude && precision == 0 && (flags.alternate || !zeroValue)) { // gawk prints "0" rather than nothing for a zero magnitude // with an explicit zero precision when the '#' flag is given, // or when the original value is nonzero and merely truncates // to zero. actualPrecision = 1; } + // The '#' prefix depends on the original value, not the truncated + // magnitude: gawk prints "0x0" for %#.0x with 0.1. For %o, gawk + // always adds the alternate leading zero in addition to any + // precision padding: %#.5o of 1 prints "000001". String prefix = ""; - if (flags.alternate && !zeroMagnitude) { + if (flags.alternate && !zeroValue) { if (conversion == 'x') { prefix = "0x"; } else if (conversion == 'X') { prefix = "0X"; + } else if (conversion == 'o') { + prefix = "0"; } } - if (flags.alternate - && conversion == 'o' - && !magnitude.startsWith("0") - && (precision < 0 || precision <= magnitude.length())) { - // '#' with %o forces one leading zero unless the precision - // already provides it. - magnitude = "0" + magnitude; - } appendInteger("", prefix, magnitude, flags, width, actualPrecision, zeroMagnitude); } diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index 6b3ed295..55bf2ee7 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -155,6 +155,17 @@ public void testHash() { // Was commented out in Printf4J ("binary is not supported for now"): // %b is not an AWK conversion, so gawk prints the specifier verbatim. assertSprintf("%#b", "%#b", 6); + // gawk-verified: the '#' prefix depends on the original value, so a + // nonzero fraction that truncates to zero keeps the prefix. + assertSprintf("0x0", "%#.0x", 0.1); + assertSprintf("0x0", "%#x", 0.5); + // gawk-verified: '#' with %o always adds its leading zero on nonzero + // values, in addition to any precision padding. + assertSprintf("00", "%#o", 0.5); + assertSprintf("00", "%#.0o", 0.2); + assertSprintf("000001", "%#.5o", 1); + assertSprintf("0010", "%#.3o", 8); + assertSprintf("010", "%#o", 8); } @Test @@ -539,6 +550,10 @@ public void testStringLength() { // verbatim. assertSprintf("%.4.2s", "%.4.2s", "123456"); assertSprintf("123", "%.*s", 3, "123456"); + // The precision counts characters, so it never splits a surrogate + // pair, like gawk in a multibyte locale. + assertSprintf("😀", "%.1s", "😀x"); + assertSprintf("😀x", "%.2s", "😀x"); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @@ -686,6 +701,8 @@ public void testPositionalSpecifiers() { assertSprintf("a b a", "%1$s %2$s %1$s", "a", "b"); // Mixing positional and sequential specifiers is fatal, like gawk. assertSprintfThrows(AwkRuntimeException.class, "%2$s %s", "a", "b"); + // A zero positional index is fatal, like gawk. + assertSprintfThrows(AwkRuntimeException.class, "%0$s", "a"); } @Test From 30df61f06a3f4df991c2a2d02a50c81480a9950d Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 11 Aug 2026 23:42:41 +0200 Subject: [PATCH 04/18] Address third round of Codex review comments on AwkPrintf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The positional/sequential mixing check now applies only to how conversions select their value argument: gawk allows an explicitly positioned star operand alongside sequential conversions, so sprintf("%*2$s|%s", "a", 5) now produces " a|5" (gawk-verified). - Field widths count characters (code points), so a supplementary character fills one column: sprintf("%3s", "😀") pads with two spaces, like gawk's %s in a multibyte locale. gawk's %c padding counts bytes (a C-locale artifact); Jawk pads %c by characters too, documented in compatibility.md.vm. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 28 +++++++++++++------- src/site/markdown/compatibility.md.vm | 4 ++- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 10 +++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index a05cec56..cf6fa2d3 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -379,6 +379,7 @@ private int formatSpecifier(int start) { i++; Flags flags = new Flags(leftJustify, plusSign, spaceSign, zeroPad, alternate, grouping); + recordArgumentMode(argPosition > 0); Object arg = argPosition > 0 ? argAt(argPosition) : nextArg(); render(conversion, flags, width, precision, arg); return i; @@ -401,8 +402,6 @@ private int starPositionEnd(int i) { } private Object nextArg() { - sawSequential = true; - rejectMixedArgumentModes(); if (argIndex >= args.length) { throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); } @@ -410,8 +409,6 @@ private Object nextArg() { } private Object argAt(int position) { - sawPositional = true; - rejectMixedArgumentModes(); if (position <= 0) { throw new AwkRuntimeException("argument index with `$' must be > 0 in `" + format + "'"); } @@ -422,10 +419,20 @@ private Object argAt(int position) { } /** - * Rejects format strings that mix positional ({@code n$}) and - * sequential argument references, like gawk. + * Records how one conversion selects its value argument and rejects + * format strings that mix positional ({@code n$}) and sequential + * conversions, like gawk. Star width and precision operands are not + * tracked: gawk allows an explicitly positioned star operand + * ({@code %*2$s}) alongside sequential conversions. + * + * @param positional whether the conversion used an {@code n$} index */ - private void rejectMixedArgumentModes() { + private void recordArgumentMode(boolean positional) { + if (positional) { + sawPositional = true; + } else { + sawSequential = true; + } if (sawPositional && sawSequential) { throw new AwkRuntimeException("must use `count$' on all formats or none in `" + format + "'"); } @@ -811,11 +818,14 @@ private String stripTrailingFractionZeros(String s) { } private void appendPadded(String body, boolean leftJustify, boolean zeroPad, int width) { - if (width <= body.length()) { + // The field width counts characters (code points), so that a + // supplementary character fills one column, not two. + int bodyLength = body.codePointCount(0, body.length()); + if (width <= bodyLength) { out.append(body); return; } - int padLength = width - body.length(); + int padLength = width - bodyLength; if (leftJustify) { out.append(body); appendSpaces(padLength); diff --git a/src/site/markdown/compatibility.md.vm b/src/site/markdown/compatibility.md.vm index d8977aaf..63d5ab84 100644 --- a/src/site/markdown/compatibility.md.vm +++ b/src/site/markdown/compatibility.md.vm @@ -203,7 +203,9 @@ A few edge cases follow Java's platform rules rather than the C library's: - `%c` is locale-independent: a numeric argument selects the Unicode code point (so `printf "%c", 233` always prints `é`), where C-locale gawk emits the raw byte. Values that are - not valid code points are truncated to a UTF-16 char. + not valid code points are truncated to a UTF-16 char. Field widths and `%s` precision count + characters (code points) regardless of locale, like gawk in a multibyte locale — except that + gawk pads `%c` by bytes, which Jawk does not reproduce. - `%a`/`%A` use Java's hexadecimal floating-point notation (`0x1.0p0` where glibc prints `0x1p+0`); gawk itself documents these conversions as C-library dependent. - NaN always prints as `nan`: Java does not track the sign of NaN, so gawk's occasional `-nan` diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index 55bf2ee7..bc662038 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -554,6 +554,12 @@ public void testStringLength() { // pair, like gawk in a multibyte locale. assertSprintf("😀", "%.1s", "😀x"); assertSprintf("😀x", "%.2s", "😀x"); + // The field width also counts characters: a supplementary character + // fills one column (gawk pads %s the same way; its %c padding counts + // bytes, a C-locale artifact that Jawk does not reproduce). + assertSprintf(" 😀", "%3s", "😀"); + assertSprintf("😀 ", "%-3s", "😀"); + assertSprintf(" 😀", "%3c", 0x1F600); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @@ -703,6 +709,10 @@ public void testPositionalSpecifiers() { assertSprintfThrows(AwkRuntimeException.class, "%2$s %s", "a", "b"); // A zero positional index is fatal, like gawk. assertSprintfThrows(AwkRuntimeException.class, "%0$s", "a"); + // gawk-verified: an explicitly positioned star operand may accompany + // sequential conversions. + assertSprintf(" a|5", "%*2$s|%s", "a", 5); + assertSprintf("a 5", "%1$s %2$*3$d", "a", 5, 6); } @Test From bfb0cdd2e53d9cc2b09baedc43d8d5d2fb14ecf3 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 11 Aug 2026 23:56:10 +0200 Subject: [PATCH 05/18] Address fourth round of Codex review comments on AwkPrintf - Sequential star operands now pin the format to sequential mode, so a positional conversion with an unpositioned star width or precision (%2$*d) is a mixed-mode fatal error, like gawk. - An explicitly positioned unknown specifier (%2$q) pins the format to positional mode even though it prints verbatim, so following it with a sequential conversion is fatal, like gawk. - The hexadecimal prefix of %a/%A stays ahead of zero padding: %020a of 1234.5 prints 0x00000000001.34ap10 instead of inserting zeros before the 0x prefix. All gawk-verified. The %#.2g rollover comment is answered on the PR instead: glibc's "1.e+02" for %#.2g of 99.99 is inconsistent with its own "1.0e+04" for %#.2g of 9999, so Jawk keeps the C-standard result (which mingw-gawk also produces), per the project's compatibility policy of not reproducing C-library accidents. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 27 +++++++++++++++----- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 15 +++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index cf6fa2d3..d9bb8a7e 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -313,6 +313,9 @@ private int formatSpecifier(int start) { dynamicWidth = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); i = starArgEnd; } else { + // A sequential star operand pins the format to sequential + // mode; an explicitly positioned one is neutral. + recordArgumentMode(false); dynamicWidth = (long) JRT.toDouble(nextArg()); } if (dynamicWidth < 0) { @@ -343,6 +346,8 @@ private int formatSpecifier(int start) { dynamicPrecision = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); i = starArgEnd; } else { + // Same sequential-mode tracking as the width operand. + recordArgumentMode(false); dynamicPrecision = (long) JRT.toDouble(nextArg()); } // A negative dynamic precision means "no precision" in C. @@ -369,7 +374,11 @@ private int formatSpecifier(int start) { if (i >= length || CONVERSION_CHARS.indexOf(format.charAt(i)) < 0) { // Unknown or unterminated conversion: print the specifier // verbatim (including the offending character) without - // consuming an argument, like gawk. + // consuming an argument, like gawk. An explicit position + // still pins the format to positional mode, also like gawk. + if (argPosition > 0) { + recordArgumentMode(true); + } int end = i < length ? i + 1 : length; out.append(format, start, end); return end; @@ -629,16 +638,22 @@ private void renderFloat(char conversion, Flags flags, int width, int precision, if (renderNonFinite(conversion, flags, width, d)) { return; } - String body = floatBody(conversion, flags, precision, d); - if (body == null) { + String magnitude = floatBody(conversion, flags, precision, d); + if (magnitude == null) { return; } boolean negative = d < 0 || (d == 0 && Double.doubleToRawLongBits(d) != 0L); String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : ""; - String magnitude = body.startsWith("-") ? body.substring(1) : body; - String full = sign + magnitude; + // The hexadecimal prefix of %a/%A stays ahead of any zero padding, + // like an integer prefix. + String prefix = ""; + if (magnitude.startsWith("0x") || magnitude.startsWith("0X")) { + prefix = magnitude.substring(0, 2); + magnitude = magnitude.substring(2); + } + String full = sign + prefix + magnitude; if (width > full.length() && flags.zeroPad && !flags.leftJustify) { - out.append(sign); + out.append(sign).append(prefix); out.append(zeros(width - full.length())); out.append(magnitude); return; diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index bc662038..fe9cfd11 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -713,6 +713,12 @@ public void testPositionalSpecifiers() { // sequential conversions. assertSprintf(" a|5", "%*2$s|%s", "a", 5); assertSprintf("a 5", "%1$s %2$*3$d", "a", 5, 6); + // gawk-verified: a sequential star operand with a positional + // conversion is a mixed-mode fatal error... + assertSprintfThrows(AwkRuntimeException.class, "%2$*d", 5, 12); + // ...and an explicitly positioned unknown specifier pins the format + // to positional mode even though it prints verbatim. + assertSprintfThrows(AwkRuntimeException.class, "%2$q|%d", 5, 12); } @Test @@ -781,6 +787,15 @@ public void testOutOfRangeIntegerConversions() { assertSprintf("1.26765e+30", "%o", Math.pow(2, 100)); } + @Test + public void testHexFloat() { + // %a uses Java's hexadecimal float notation (gawk documents %a as + // C-library dependent); the 0x prefix stays ahead of zero padding. + assertSprintf("0x1.34ap10", "%a", 1234.5); + assertSprintf("0x00000000001.34ap10", "%020a", 1234.5); + assertSprintf("-0x1.34ap10", "%a", -1234.5); + } + @Test public void testNonFiniteValues() { assertSprintf("nan", "%d", Double.NaN); From 75294faa52c2d60a628a6ce6bd1caba1ce06fade Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:05:10 +0200 Subject: [PATCH 06/18] Use the locale decimal separator in alternate %f/%e forms The '#' flag with zero precision appended a hard-coded '.' for %f and %e; it now inserts the locale's decimal separator, consistent with the other floating-point paths: %#.0f of 1 with Locale.FRANCE prints "1," and %#.0e of 12345 prints "1,e+04". Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 10 +++++----- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index d9bb8a7e..94a1117c 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -663,9 +663,8 @@ private void renderFloat(char conversion, Flags flags, int width, int precision, /** * Renders the digits of a finite double for a floating-point - * conversion, without sign and without width padding. The result - * carries a leading '-' only for {@code %a} (which is delegated to - * Java); all other conversions format the absolute value. + * conversion, without sign and without width padding: the absolute + * value is formatted and the caller applies the sign. */ private String floatBody(char conversion, Flags flags, int precision, double d) { double abs = Math.abs(d); @@ -676,7 +675,7 @@ private String floatBody(char conversion, Flags flags, int precision, double d) // The exact binary value of the double is intended: it makes rounding match gawk's C library. String s = decimalString(new BigDecimal(abs).setScale(p, RoundingMode.HALF_EVEN)); // NOPMD if (flags.alternate && p == 0) { - s = s + "."; + s = forceDecimalSeparator(s); } if (flags.grouping) { s = groupDigits(s); @@ -688,7 +687,8 @@ private String floatBody(char conversion, Flags flags, int precision, double d) int p = precision < 0 ? 6 : precision; String s = scientific(abs, p); if (flags.alternate && p == 0) { - s = s.replace("e", ".e"); + int exponentStart = s.indexOf('e'); + s = forceDecimalSeparator(s.substring(0, exponentStart)) + s.substring(exponentStart); } return conversion == 'E' ? s.toUpperCase(Locale.ROOT) : s; } diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index fe9cfd11..c4cf72ec 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -841,6 +841,10 @@ public void testGroupingFlag() { public void testLocaleDecimalSeparator() { assertSprintf("3,14", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159); assertSprintf("3,14159", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159); + // The '#' decimal point follows the locale as well. + assertSprintf("1,", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0f", 1); + assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0e", 12345); + assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.1g", 12345); } @Test From 4cef883ee3f83253fbc9488c65909f13aa3959a2 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:15:23 +0200 Subject: [PATCH 07/18] Address sixth round of Codex review comments on AwkPrintf - The ' grouping flag applies to decimal conversions only: %'x and %'o no longer insert grouping separators, like gawk. - %g in fixed notation now honors the grouping flag (%'g of 12345 prints "12,345"), while exponential %g and %e stay ungrouped, like gawk. - A percent conversion reached through flags, width, or precision prints a plain '%' with the modifiers ignored: %5% prints "%", like gawk, instead of being treated as an unknown specifier. All gawk-verified (en_US locale for the grouping cases). Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 30 +++++++++++++++----- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 13 +++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 94a1117c..d38107f4 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -371,6 +371,14 @@ private int formatSpecifier(int start) { i++; } + if (i < length && format.charAt(i) == '%') { + // A percent conversion reached through flags, width, or + // precision prints a plain '%' and ignores them all, like + // gawk ("%5%" prints "%"). + out.append('%'); + return i + 1; + } + if (i >= length || CONVERSION_CHARS.indexOf(format.charAt(i)) < 0) { // Unknown or unterminated conversion: print the specifier // verbatim (including the offending character) without @@ -538,7 +546,7 @@ private void renderSignedInteger(Flags flags, int width, int precision, Object a } String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : ""; - appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude)); + appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude), true); } private void renderUnsignedInteger(char conversion, Flags flags, int width, int precision, Object arg) { @@ -594,12 +602,16 @@ private void renderUnsignedInteger(char conversion, Flags flags, int width, int prefix = "0"; } } - appendInteger("", prefix, magnitude, flags, width, actualPrecision, zeroMagnitude); + appendInteger("", prefix, magnitude, flags, width, actualPrecision, zeroMagnitude, conversion == 'u'); } /** * Applies precision, grouping, and width to an integer body and * appends it to the output. + * + * @param groupable whether the {@code '} flag may group this + * conversion: gawk groups decimal output only, never octal or + * hexadecimal */ private void appendInteger( String sign, @@ -608,7 +620,8 @@ private void appendInteger( Flags flags, int width, int precision, - boolean zeroMagnitude) { + boolean zeroMagnitude, + boolean groupable) { String digits = magnitude; if (precision == 0 && zeroMagnitude) { // C: a zero value with an explicit zero precision prints no @@ -619,7 +632,7 @@ private void appendInteger( if (precision > digits.length()) { digits = zeros(precision - digits.length()) + digits; } - if (flags.grouping) { + if (flags.grouping && groupable) { digits = groupDigits(digits); } String body = sign + prefix + digits; @@ -695,7 +708,7 @@ private String floatBody(char conversion, Flags flags, int precision, double d) case 'g': case 'G': { int p = precision < 0 ? 6 : precision == 0 ? 1 : precision; - String s = generalFloat(abs, p, flags.alternate); + String s = generalFloat(abs, p, flags.alternate, flags.grouping); return conversion == 'G' ? s.toUpperCase(Locale.ROOT) : s; } case 'a': @@ -738,7 +751,7 @@ private String scientific(double abs, int precision) { } /** Formats {@code abs >= 0} in C's {@code %g} notation. */ - private String generalFloat(double abs, int precision, boolean alternate) { + private String generalFloat(double abs, int precision, boolean alternate, boolean grouping) { if (abs == 0) { return alternate ? "0." + zeros(precision - 1) : "0"; } @@ -747,7 +760,10 @@ private String generalFloat(double abs, int precision, boolean alternate) { int exponent = rounded.precision() - rounded.scale() - 1; if (exponent >= -4 && exponent < precision) { String s = decimalString(rounded.setScale(precision - 1 - exponent, RoundingMode.UNNECESSARY)); - return alternate ? forceDecimalSeparator(s) : stripTrailingFractionZeros(s); + s = alternate ? forceDecimalSeparator(s) : stripTrailingFractionZeros(s); + // gawk groups %g in fixed notation, like %f, but never in + // exponential notation. + return grouping ? groupDigits(s) : s; } String mantissa = decimalString( rounded.movePointLeft(exponent).setScale(precision - 1, RoundingMode.UNNECESSARY)); diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index c4cf72ec..b97d4c35 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -185,6 +185,11 @@ public void testSpecifier() { assertSprintf("1234ABCD", "%X", 305441741); assertSprintf("EDCB5433", "%X", 3989525555L); assertSprintf("%", "%%"); + // gawk-verified: a percent conversion ignores flags, width, and + // precision. + assertSprintf("%", "%5%"); + assertSprintf("%", "%-3%"); + assertSprintf("%", "%0.2%"); } @Test @@ -833,8 +838,16 @@ public void testExtraArgumentsAreIgnored() { @Test public void testGroupingFlag() { assertSprintf("1,234,567", "%'d", 1234567); + assertSprintf("1,234,567", "%'u", 1234567); assertSprintf("1,234,567.89", "%'.2f", 1234567.891); assertSprintf("1.234.567", Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567); + // gawk-verified: %g groups in fixed notation only, and octal and + // hexadecimal output is never grouped. + assertSprintf("12,345", "%'g", 12345); + assertSprintf("1,234,567.25", "%'.10g", 1234567.25); + assertSprintf("1.234567e+06", "%'e", 1234567); + assertSprintf("2540be400", "%'x", 10000000000L); + assertSprintf("1747", "%'o", 999); } @Test From 0dca6de370c435b9db5605c64ada6c5da2259489 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:18:08 +0200 Subject: [PATCH 08/18] Fix checkstyle violation: fold grouping eligibility into Flags The extra appendInteger parameter exceeded the 7-parameter checkstyle limit; octal and hexadecimal conversions now clear the grouping flag via Flags.withoutGrouping() instead. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index d38107f4..e543dc10 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -192,6 +192,16 @@ private static final class Flags { this.alternate = alternate; this.grouping = grouping; } + + /** + * Returns these flags with the {@code '} grouping flag cleared, for + * conversions that gawk never groups (octal and hexadecimal). + * + * @return an equivalent flag set without grouping + */ + Flags withoutGrouping() { + return grouping ? new Flags(leftJustify, plusSign, spaceSign, zeroPad, alternate, false) : this; + } } /** @@ -546,7 +556,7 @@ private void renderSignedInteger(Flags flags, int width, int precision, Object a } String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : ""; - appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude), true); + appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude)); } private void renderUnsignedInteger(char conversion, Flags flags, int width, int precision, Object arg) { @@ -602,16 +612,15 @@ private void renderUnsignedInteger(char conversion, Flags flags, int width, int prefix = "0"; } } - appendInteger("", prefix, magnitude, flags, width, actualPrecision, zeroMagnitude, conversion == 'u'); + // gawk's ' flag groups decimal output only, never octal or + // hexadecimal. + Flags integerFlags = conversion == 'u' ? flags : flags.withoutGrouping(); + appendInteger("", prefix, magnitude, integerFlags, width, actualPrecision, zeroMagnitude); } /** * Applies precision, grouping, and width to an integer body and * appends it to the output. - * - * @param groupable whether the {@code '} flag may group this - * conversion: gawk groups decimal output only, never octal or - * hexadecimal */ private void appendInteger( String sign, @@ -620,8 +629,7 @@ private void appendInteger( Flags flags, int width, int precision, - boolean zeroMagnitude, - boolean groupable) { + boolean zeroMagnitude) { String digits = magnitude; if (precision == 0 && zeroMagnitude) { // C: a zero value with an explicit zero precision prints no @@ -632,7 +640,7 @@ private void appendInteger( if (precision > digits.length()) { digits = zeros(precision - digits.length()) + digits; } - if (flags.grouping && groupable) { + if (flags.grouping) { digits = groupDigits(digits); } String body = sign + prefix + digits; From 922d2de58722a857a7e8174fbfe5738dce70da68 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:28:58 +0200 Subject: [PATCH 09/18] Address seventh round of Codex review comments on AwkPrintf - A positional percent conversion (%1$%) pins the format to positional mode before printing its '%', so following it with a sequential conversion is fatal, like gawk. - %#g of zero uses the locale decimal separator ("0,00000" with Locale.FRANCE) instead of a hard-coded period. - Length modifiers now match current gawk (verified against gawk master's printf.c): h, j, l, L, t, and z are each accepted at most once and ignored; distinct modifiers may stack (%lhd), while a repeated modifier (%lld, %hhd) still invalidates the specifier. This restores the original Printf4J expectations for %zu/%zd/%zi. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 24 ++++++++++++++------ src/site/markdown/behavior-changes.md | 3 ++- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 21 ++++++++++++----- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index e543dc10..7a46afe6 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -73,7 +73,7 @@ public final class AwkPrintf { private static final String CONVERSION_CHARS = "diouxXeEfFgGaAcs"; /** Length modifier characters accepted (and ignored) like gawk. */ - private static final String LENGTH_MODIFIERS = "hlL"; + private static final String LENGTH_MODIFIERS = "hjlLtz"; /** A one-character string holding the NUL character, printed by {@code %c} for empty values. */ private static final String NUL_STRING = Character.toString((char) 0); @@ -374,17 +374,27 @@ private int formatSpecifier(int start) { } } - // A single length modifier (h, l, or L) is accepted and ignored, - // like gawk. Doubled modifiers such as "ll" or "hh" make the - // whole specifier invalid, also like gawk. - if (i < length && LENGTH_MODIFIERS.indexOf(format.charAt(i)) >= 0) { + // Length modifiers (h, j, l, L, t, z) are each accepted at most + // once and ignored, like gawk. A repeated modifier such as "ll" + // or "hh" makes the whole specifier invalid, also like gawk. + int modifierMask = 0; + while (i < length) { + int modifierIndex = LENGTH_MODIFIERS.indexOf(format.charAt(i)); + if (modifierIndex < 0 || (modifierMask & 1 << modifierIndex) != 0) { + break; + } + modifierMask |= 1 << modifierIndex; i++; } if (i < length && format.charAt(i) == '%') { // A percent conversion reached through flags, width, or // precision prints a plain '%' and ignores them all, like - // gawk ("%5%" prints "%"). + // gawk ("%5%" prints "%"). An explicit position still pins + // the format to positional mode, also like gawk. + if (argPosition > 0) { + recordArgumentMode(true); + } out.append('%'); return i + 1; } @@ -761,7 +771,7 @@ private String scientific(double abs, int precision) { /** Formats {@code abs >= 0} in C's {@code %g} notation. */ private String generalFloat(double abs, int precision, boolean alternate, boolean grouping) { if (abs == 0) { - return alternate ? "0." + zeros(precision - 1) : "0"; + return alternate ? forceDecimalSeparator("0") + zeros(precision - 1) : "0"; } // The exact binary value of the double is intended: it makes rounding match gawk's C library. BigDecimal rounded = new BigDecimal(abs).round(new MathContext(precision, RoundingMode.HALF_EVEN)); // NOPMD diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index ddd69590..1ab7f3bd 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -45,7 +45,8 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. specifiers were printed verbatim). - Unknown conversion specifiers (including `%n`, which Printf4J turned into a newline, and invalid length modifiers such as `ll` or `hh`) are printed verbatim without consuming an - argument, as in gawk; a single `h`, `l`, or `L` length modifier is accepted and ignored. + argument, as in gawk; the `h`, `j`, `l`, `L`, `t`, and `z` length modifiers are each + accepted at most once and ignored. - Integral values beyond the 64-bit range are no longer saturated to 2^63-1: `print 2^100` now prints the full decimal expansion `1267650600228229401496703205376` (previously `9223372036854775807`), and `int()` preserves such values diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index b97d4c35..c39dd70b 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -190,6 +190,9 @@ public void testSpecifier() { assertSprintf("%", "%5%"); assertSprintf("%", "%-3%"); assertSprintf("%", "%0.2%"); + // ...but an explicit position still pins the format to positional + // mode, so mixing with a sequential conversion is fatal, like gawk. + assertSprintfThrows(AwkRuntimeException.class, "%1$%|%s", "a"); } @Test @@ -503,9 +506,8 @@ public void testTypes() { assertSprintf("30", "%li", 30L); assertSprintf("-2147483647", "%li", -2147483647L); assertSprintf("2147483647", "%li", 2147483647L); - // Doubled modifiers ("ll", "hh") and the "q", "j", "z", and "t" - // modifiers are not valid in gawk: the specifier prints verbatim and - // consumes no argument. + // Doubled modifiers ("ll", "hh") and the "q" modifier are not valid + // in gawk: the specifier prints verbatim and consumes no argument. assertSprintf("%lli", "%lli", 30L); assertSprintf("%lli", "%lli", -9223372036854775807L); assertSprintf("%lli", "%lli", 9223372036854775807L); @@ -513,9 +515,15 @@ public void testTypes() { assertSprintf("4294967295", "%lu", 0xFFFFFFFFL); assertSprintf("%llu", "%llu", 281474976710656L); assertSprintf("%llu", "%llu", Long.parseUnsignedLong("18446744073709551615")); - assertSprintf("%zu", "%zu", 2147483647L); - assertSprintf("%zd", "%zd", 2147483647L); - assertSprintf("%zi", "%zi", -2147483647L); + // Single j, z, and t modifiers are accepted and ignored, like h, l, + // and L (gawk 5.2+). + assertSprintf("2147483647", "%zu", 2147483647L); + assertSprintf("2147483647", "%zd", 2147483647L); + assertSprintf("-2147483647", "%zi", -2147483647L); + assertSprintf("5", "%jd", 5); + assertSprintf("6", "%td", 6); + // Distinct modifiers may stack; only repeats are invalid. + assertSprintf("42", "%lhd", 42); // %b is not an AWK conversion: printed verbatim, like gawk. assertSprintf("%b", "%b", 60000); assertSprintf("%lb", "%lb", 12345678L); @@ -858,6 +866,7 @@ public void testLocaleDecimalSeparator() { assertSprintf("1,", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0f", 1); assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0e", 12345); assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.1g", 12345); + assertSprintf("0,00000", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#g", 0); } @Test From e7e2a7afee96dc0e0397072c2a9900f6069ead9d Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:39:20 +0200 Subject: [PATCH 10/18] Address eighth round of Codex review comments on AwkPrintf - An explicitly empty CONVFMT or OFMT stays empty, like gawk: only a null (absent) format selects the %.6g default, so CONVFMT="" makes %s convert non-integral numbers to the empty string. - Positional argument indexes are validated even for conversions that consume no argument: %2$% and %2$q with one argument are fatal, with gawk's message (argument index N greater than total number of supplied arguments). Both gawk-verified. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 25 ++++++++++++++++---- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 9 +++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 7a46afe6..1df558cc 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -117,7 +117,9 @@ public static String sprintf(final String format, final Object... args) { */ public static String sprintf(final Locale locale, final String convfmt, final String format, final Object... args) { Locale actualLocale = locale == null ? Locale.US : locale; - String actualConvfmt = convfmt == null || convfmt.isEmpty() ? DEFAULT_CONVFMT : convfmt; + // An explicitly empty CONVFMT stays empty, like gawk; only a null + // (absent) format selects the default. + String actualConvfmt = convfmt == null ? DEFAULT_CONVFMT : convfmt; Object[] actualArgs = args == null ? new Object[0] : args; return new AwkPrintfFormatter(actualLocale, actualConvfmt, format, actualArgs).format(); } @@ -167,7 +169,9 @@ private static String numberToAwkString(final double number, final String conver // The exact binary value of the double is intended: it makes rounding match gawk's C library. return new BigDecimal(rounded).toBigInteger().toString(); // NOPMD } - String fmt = conversionFormat == null || conversionFormat.isEmpty() ? DEFAULT_CONVFMT : conversionFormat; + // An explicitly empty CONVFMT/OFMT stays empty, like gawk; only a + // null (absent) format selects the default. + String fmt = conversionFormat == null ? DEFAULT_CONVFMT : conversionFormat; return sprintf(locale, DEFAULT_CONVFMT, fmt, Double.valueOf(number)); } @@ -394,6 +398,7 @@ private int formatSpecifier(int start) { // the format to positional mode, also like gawk. if (argPosition > 0) { recordArgumentMode(true); + requireArgumentIndex(argPosition); } out.append('%'); return i + 1; @@ -406,6 +411,7 @@ private int formatSpecifier(int start) { // still pins the format to positional mode, also like gawk. if (argPosition > 0) { recordArgumentMode(true); + requireArgumentIndex(argPosition); } int end = i < length ? i + 1 : length; out.append(format, start, end); @@ -446,13 +452,24 @@ private Object nextArg() { } private Object argAt(int position) { + requireArgumentIndex(position); + return args[position - 1]; + } + + /** + * Validates a positional ({@code n$}) argument index against the + * supplied arguments, like gawk, which checks the index even for + * conversions that do not consume the referenced value. + */ + private void requireArgumentIndex(int position) { if (position <= 0) { throw new AwkRuntimeException("argument index with `$' must be > 0 in `" + format + "'"); } if (position > args.length) { - throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'"); + throw new AwkRuntimeException( + "argument index " + position + " greater than total number of supplied arguments in `" + + format + "'"); } - return args[position - 1]; } /** diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index c39dd70b..07bec3ba 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -193,6 +193,11 @@ public void testSpecifier() { // ...but an explicit position still pins the format to positional // mode, so mixing with a sequential conversion is fatal, like gawk. assertSprintfThrows(AwkRuntimeException.class, "%1$%|%s", "a"); + // gawk validates the index of a positioned conversion even when it + // consumes no argument. + assertSprintfThrows(AwkRuntimeException.class, "%2$%", 1); + assertSprintfThrows(AwkRuntimeException.class, "%2$q", 1); + assertSprintfThrows(AwkRuntimeException.class, "%1$%"); } @Test @@ -685,6 +690,10 @@ public void testStringConversionUsesAwkNumberToStringRules() { assertSprintf("3.1", Locale.US, "%.2g", "%s", 3.14159265); // CONVFMT that is not a %g-style format is honored verbatim. assertSprintf("3.14", Locale.US, "%.2f", "%s", 3.14159265); + // An explicitly empty CONVFMT converts non-integral numbers to the + // empty string, like gawk; integral values still print as integers. + assertSprintf("", Locale.US, "", "%s", 1.5); + assertSprintf("1", Locale.US, "", "%s", 1.0); // Integral values beyond the 64-bit range print in full. assertSprintf("100000000000000000000", "%s", 1e20); // Exact long values are preserved. From b21491a0ee37f73130e9ecbc9aea40364badc39a Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 00:49:54 +0200 Subject: [PATCH 11/18] Treat zero-indexed star operands as zero, like gawk A zero positional index on a star operand (%*0$d, %.*0$f) means the value zero without consuming an argument, while a conversion with a zero index (%0$d) remains fatal. gawk-verified. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkPrintf.java | 9 +++++++-- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 1df558cc..659b6c8a 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -324,7 +324,10 @@ private int formatSpecifier(int start) { int starArgEnd = starPositionEnd(i); long dynamicWidth; if (starArgEnd > i) { - dynamicWidth = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); + int starPosition = parseInt(format, i, starArgEnd - 1); + // gawk treats a zero-indexed star operand ("%*0$d") as + // the value zero, without consuming an argument. + dynamicWidth = starPosition == 0 ? 0 : (long) JRT.toDouble(argAt(starPosition)); i = starArgEnd; } else { // A sequential star operand pins the format to sequential @@ -357,7 +360,9 @@ private int formatSpecifier(int start) { int starArgEnd = starPositionEnd(i); long dynamicPrecision; if (starArgEnd > i) { - dynamicPrecision = (long) JRT.toDouble(argAt(parseInt(format, i, starArgEnd - 1))); + int starPosition = parseInt(format, i, starArgEnd - 1); + // Same zero-index rule as the width operand. + dynamicPrecision = starPosition == 0 ? 0 : (long) JRT.toDouble(argAt(starPosition)); i = starArgEnd; } else { // Same sequential-mode tracking as the width operand. diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index 07bec3ba..7cf4c8c8 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -198,6 +198,11 @@ public void testSpecifier() { assertSprintfThrows(AwkRuntimeException.class, "%2$%", 1); assertSprintfThrows(AwkRuntimeException.class, "%2$q", 1); assertSprintfThrows(AwkRuntimeException.class, "%1$%"); + // gawk-verified: a zero-indexed star operand means the value zero + // without consuming an argument, while an out-of-range one is fatal. + assertSprintf("7|42", "%*0$d|%d", 7, 42); + assertSprintf("3|42", "%.*0$f|%d", 3.14159, 42); + assertSprintfThrows(AwkRuntimeException.class, "%*5$d|%d", 7, 42); } @Test From ab35dd3f7fd0904464130c11f770c0101d3a2741 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 01:02:16 +0200 Subject: [PATCH 12/18] Preserve legacy sprintf overrides in the CONVFMT bridge The default sprintfWithConvFmt now detects (once per sink class, via reflection) whether the historical sprintf(String, Object...) customization point is overridden and routes through it, so pre-7.1 custom sinks keep their formatting behavior for both printf and sprintf. CONVFMT cannot reach the legacy signature, so such sinks convert %s operands with the default CONVFMT; overriding sprintfWithConvFmt receives the live value. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkSink.java | 34 +++++++++++++++++ src/site/markdown/behavior-changes.md | 6 ++- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 40 ++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkSink.java b/src/main/java/io/jawk/jrt/AwkSink.java index bd7d3324..8d82f838 100644 --- a/src/main/java/io/jawk/jrt/AwkSink.java +++ b/src/main/java/io/jawk/jrt/AwkSink.java @@ -333,6 +333,14 @@ public String sprintf(String format, Object... values) { * runtime routes both {@code printf} and {@code sprintf} through this * method, overriding it ensures that both produce consistent output. *

+ *

+ * For compatibility, a sink class that overrides the historical + * {@link #sprintf(String, Object...)} method but not this one keeps its + * customization: the default implementation detects such overrides and + * routes through them. {@code CONVFMT} cannot reach that legacy signature, + * so those sinks convert {@code %s} operands with the default + * {@code CONVFMT}. + *

* * @param convfmt number-to-string conversion format ({@code CONVFMT}) * @param format format string @@ -341,9 +349,35 @@ public String sprintf(String format, Object... values) { */ public String sprintfWithConvFmt(String convfmt, String format, Object... values) { Object[] safeValues = values == null ? new Object[0] : values; + if (overridesLegacySprintf()) { + return sprintf(format, safeValues); + } return AwkPrintf.sprintf(locale, convfmt, format, safeValues); } + /** + * Lazily computed flag: whether this sink's class overrides the historical + * {@link #sprintf(String, Object...)} customization point. + */ + private volatile Boolean legacySprintfOverride; + + private boolean overridesLegacySprintf() { + Boolean overridden = legacySprintfOverride; + if (overridden == null) { + try { + overridden = Boolean + .valueOf( + getClass() + .getMethod("sprintf", String.class, Object[].class) + .getDeclaringClass() != AwkSink.class); + } catch (NoSuchMethodException e) { + overridden = Boolean.FALSE; + } + legacySprintfOverride = overridden; + } + return overridden.booleanValue(); + } + /** * Formats one {@code printf} result string using this sink's locale. * diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index 1ab7f3bd..db92d508 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -53,8 +53,10 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. ([#528](https://github.com/jawkio/jawk/issues/528)). - For Java embedders: `AwkSink` gains `printfWithConvFmt(...)` and `sprintfWithConvFmt(...)`, which receive the script's current `CONVFMT` value; the runtime now routes `printf` and - `sprintf` through these methods. Custom sinks that overrode `sprintf(String, Object...)` to - customize formatting should override `sprintfWithConvFmt(String, String, Object...)` instead. + `sprintf` through these methods. Custom sinks that override the historical + `sprintf(String, Object...)` keep their customization (the default `sprintfWithConvFmt` + detects and routes through such overrides); override + `sprintfWithConvFmt(String, String, Object...)` to also receive the script's `CONVFMT`. The `org.metricshub:printf4j` dependency has been removed; its formatting logic now lives in `io.jawk.jrt.AwkPrintf` ([#528](https://github.com/jawkio/jawk/issues/528)). diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index 7cf4c8c8..aedaf731 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -883,6 +883,46 @@ public void testLocaleDecimalSeparator() { assertSprintf("0,00000", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#g", 0); } + @Test + public void testLegacySprintfOverrideIsPreserved() { + // A sink that overrides the historical sprintf(String, Object...) + // customization point keeps working when the runtime routes through + // sprintfWithConvFmt. + AwkSink legacySink = new AwkSink() { + + @Override + public void print(String ofs, String ors, String ofmt, Object... values) { + // not needed for this test + } + + @Override + public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + // not needed for this test + } + + @Override + public String sprintf(String format, Object... values) { + return "custom:" + format; + } + }; + assertEquals("custom:%s", legacySink.sprintfWithConvFmt("%.2g", "%s", 1.5)); + + // A sink without the legacy override uses the CONVFMT-aware engine. + AwkSink plainSink = new AwkSink() { + + @Override + public void print(String ofs, String ors, String ofmt, Object... values) { + // not needed for this test + } + + @Override + public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + // not needed for this test + } + }; + assertEquals("3.1", plainSink.sprintfWithConvFmt("%.2g", "%s", 3.14159265)); + } + @Test public void testToAwkString() { assertToAwkString("", null); From a50dc43416adc9ef67cf98235c0e541591f0e78c Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 01:12:58 +0200 Subject: [PATCH 13/18] Avoid recursion through legacy sprintf overrides The base sprintf(String, Object...) now invokes the formatting engine directly instead of delegating to sprintfWithConvFmt, so a legacy override that decorates super.sprintf(...) reaches the base formatter without being redispatched into itself (previously a StackOverflowError). Covered by a decorating-sink unit test. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/jrt/AwkSink.java | 13 ++++++------ src/test/java/io/jawk/jrt/AwkPrintfTest.java | 21 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jawk/jrt/AwkSink.java b/src/main/java/io/jawk/jrt/AwkSink.java index 8d82f838..40e4d865 100644 --- a/src/main/java/io/jawk/jrt/AwkSink.java +++ b/src/main/java/io/jawk/jrt/AwkSink.java @@ -307,11 +307,11 @@ protected final Object normalizePrintArgument(Object value) { * Formats a string in the same way as AWK's {@code sprintf()} built-in, * using the default {@code CONVFMT} value ({@code "%.6g"}). *

- * The default implementation delegates to - * {@link #sprintfWithConvFmt(String, String, Object...)}. To customize - * formatting for both {@code printf} and {@code sprintf}, override - * {@link #sprintfWithConvFmt(String, String, Object...)}, which is the - * method the runtime invokes. + * The default implementation invokes the built-in formatting engine + * directly, so an override may safely decorate {@code super.sprintf(...)} + * results. The runtime routes script {@code printf}/{@code sprintf} calls + * through {@link #sprintfWithConvFmt(String, String, Object...)}, which + * honors overrides of either method. *

* * @param format format string @@ -319,7 +319,8 @@ protected final Object normalizePrintArgument(Object value) { * @return formatted text */ public String sprintf(String format, Object... values) { - return sprintfWithConvFmt(AwkPrintf.DEFAULT_CONVFMT, format, values); + Object[] safeValues = values == null ? new Object[0] : values; + return AwkPrintf.sprintf(locale, AwkPrintf.DEFAULT_CONVFMT, format, safeValues); } /** diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index aedaf731..af7c7fbc 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -907,6 +907,27 @@ public String sprintf(String format, Object... values) { }; assertEquals("custom:%s", legacySink.sprintfWithConvFmt("%.2g", "%s", 1.5)); + // A legacy override that decorates super.sprintf(...) must reach the + // base formatter without being redispatched into itself. + AwkSink decoratingSink = new AwkSink() { + + @Override + public void print(String ofs, String ors, String ofmt, Object... values) { + // not needed for this test + } + + @Override + public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + // not needed for this test + } + + @Override + public String sprintf(String format, Object... values) { + return "[" + super.sprintf(format, values) + "]"; + } + }; + assertEquals("[1.5]", decoratingSink.sprintfWithConvFmt("%.2g", "%s", 1.5)); + // A sink without the legacy override uses the CONVFMT-aware engine. AwkSink plainSink = new AwkSink() { From 08b073155ee881dd51fd28598c7c899298c55a64 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 01:29:10 +0200 Subject: [PATCH 14/18] Reject positional formats in POSIX mode and unterminated star digits - Strict --posix mode now rejects gawk positional argument references in printf/sprintf formats with gawk's message (`$' is not permitted in awk formats): AVM validates formats via the new AwkPrintf.usesPositionalArguments() before formatting. - Digits after a star operand without a terminating $ (%*2d, %.*2f) are now fatal like gawk (no `$' supplied for positional field width or precision) instead of printing the specifier verbatim after consuming an argument. Both gawk-verified. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/backend/AVM.java | 22 +++++++-- src/main/java/io/jawk/jrt/AwkPrintf.java | 59 ++++++++++++++++++++++++ src/test/java/io/jawk/PrintfTest.java | 19 ++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index bb45747e..4b97e5fa 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -80,6 +80,7 @@ import io.jawk.intermediate.UninitializedObject; import io.jawk.intermediate.UntypedObject; import io.jawk.jrt.AssocArray; +import io.jawk.jrt.AwkPrintf; import io.jawk.jrt.AwkRuntimeException; import io.jawk.jrt.AwkSink; import io.jawk.jrt.BlockManager; @@ -2830,7 +2831,7 @@ private void execPrintToPipe(CountTuple tuple) throws IOException { private void execPrintf(CountTuple tuple) throws IOException { long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = jrt.toAwkString(pop()); + String format = checkPosixFormat(jrt.toAwkString(pop())); jrt.printfDefault(format, values); } @@ -2838,7 +2839,7 @@ private void execPrintfToFile(CountAndAppendTuple tuple) throws IOException { String key = jrt.toAwkString(pop()); long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = jrt.toAwkString(pop()); + String format = checkPosixFormat(jrt.toAwkString(pop())); jrt.printfToFile(key, tuple.isAppend(), format, values); } @@ -2846,7 +2847,7 @@ private void execPrintfToPipe(CountTuple tuple) throws IOException { String cmd = jrt.toAwkString(pop()); long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = jrt.toAwkString(pop()); + String format = checkPosixFormat(jrt.toAwkString(pop())); jrt.printfToProcess(cmd, format, values); } @@ -3032,7 +3033,7 @@ private Object invokeIndirectBuiltin( .getAwkSink() .sprintfWithConvFmt( jrt.getCONVFMTString(), - jrt.toAwkString(args[0]), + checkPosixFormat(jrt.toAwkString(args[0])), Arrays.copyOfRange(args, 1, args.length)); case SQRT: requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber); @@ -3784,10 +3785,21 @@ private Object[] popArguments(long numArgs) { */ private String sprintfFunction(long numArgs) { Object[] argArray = popArguments(numArgs - 1); - String fmt = jrt.toAwkString(pop()); + String fmt = checkPosixFormat(jrt.toAwkString(pop())); return jrt.getAwkSink().sprintfWithConvFmt(jrt.getCONVFMTString(), fmt, argArray); } + /** + * Rejects gawk positional argument references in strict POSIX mode, like + * {@code gawk --posix}. + */ + private String checkPosixFormat(String format) { + if (settings.isPosix() && AwkPrintf.usesPositionalArguments(format)) { + throw new AwkRuntimeException("`$' is not permitted in awk formats"); + } + return format; + } + private void setNumOnJRT(long fieldNum, double num) { String numString = jrt.toAwkString(Double.valueOf(num)); diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index 659b6c8a..afe3fd58 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -124,6 +124,52 @@ public static String sprintf(final Locale locale, final String convfmt, final St return new AwkPrintfFormatter(actualLocale, actualConvfmt, format, actualArgs).format(); } + /** + * Returns whether a format string uses gawk positional argument + * references ({@code %n$} or {@code *n$}), which strict POSIX mode must + * reject: {@code gawk --posix} fails with + * `$' is not permitted in awk formats. + * + * @param format AWK format string + * @return {@code true} when the format references arguments by position + */ + public static boolean usesPositionalArguments(final String format) { + int length = format.length(); + int i = 0; + while (i < length) { + if (format.charAt(i) != '%') { + i++; + continue; + } + i++; + if (i < length && format.charAt(i) == '%') { + i++; + continue; + } + // Inside a specifier, a positional reference can appear right + // after '%' or right after '*'. + boolean positionAllowed = true; + while (i < length) { + char c = format.charAt(i); + if (positionAllowed && isAsciiDigit(c)) { + int digitsEnd = i; + while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) { + digitsEnd++; + } + if (digitsEnd < length && format.charAt(digitsEnd) == '$') { + return true; + } + } + positionAllowed = c == '*'; + if (c == '%' || CONVERSION_CHARS.indexOf(c) >= 0) { + break; + } + i++; + } + } + return false; + } + /** * Converts a value to a string using AWK's number-to-string rules. *

@@ -322,6 +368,7 @@ private int formatSpecifier(int start) { if (i < length && format.charAt(i) == '*') { i++; int starArgEnd = starPositionEnd(i); + requireStarPositionTerminated(i, starArgEnd); long dynamicWidth; if (starArgEnd > i) { int starPosition = parseInt(format, i, starArgEnd - 1); @@ -358,6 +405,7 @@ private int formatSpecifier(int start) { if (i < length && format.charAt(i) == '*') { i++; int starArgEnd = starPositionEnd(i); + requireStarPositionTerminated(i, starArgEnd); long dynamicPrecision; if (starArgEnd > i) { int starPosition = parseInt(format, i, starArgEnd - 1); @@ -433,6 +481,17 @@ private int formatSpecifier(int start) { return i; } + /** + * Rejects digits after a star operand that are not terminated by + * {@code $}, like gawk: {@code %*2d} is fatal rather than literal. + */ + private void requireStarPositionTerminated(int i, int starArgEnd) { + if (starArgEnd == i && i < format.length() && isAsciiDigit(format.charAt(i))) { + throw new AwkRuntimeException( + "no `$' supplied for positional field width or precision in `" + format + "'"); + } + } + /** * Returns the index right after a {@code n$} sequence starting at * {@code i}, or {@code i} when there is no such sequence. diff --git a/src/test/java/io/jawk/PrintfTest.java b/src/test/java/io/jawk/PrintfTest.java index 5f2a2f0a..d50c677a 100644 --- a/src/test/java/io/jawk/PrintfTest.java +++ b/src/test/java/io/jawk/PrintfTest.java @@ -160,6 +160,25 @@ public void testPositionalSpecifiers() throws Exception { .runAndAssert(); } + @Test + public void testPosixModeRejectsPositionalSpecifiers() throws Exception { + AwkTestSupport + .cliTest("printf positional specifiers are rejected in POSIX mode") + .argument("--posix") + .script("BEGIN { printf \"%2$s %1$s\\n\", \"world\", \"hello\" }") + .expectThrow(AwkRuntimeException.class) + .runAndAssert(); + } + + @Test + public void testUnterminatedStarPositionIsFatal() throws Exception { + AwkTestSupport + .awkTest("printf digits after star without dollar are fatal") + .script("BEGIN { printf \"%*2d\\n\", 5, 42 }") + .expectThrow(AwkRuntimeException.class) + .runAndAssert(); + } + @Test public void testMixedPositionalSpecifiersAreFatal() throws Exception { AwkTestSupport From 6b2c6b85aaa50fe229aae037c1c822057a26f93c Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Wed, 12 Aug 2026 21:57:28 +0200 Subject: [PATCH 15/18] Lean printf API: CONVFMT in printf(), no compatibility bridges Apply maintainer feedback on the printf API surface: - AwkSink.printf(...) now receives CONVFMT as a parameter, right next to OFMT (printf(ofs, ors, ofmt, convfmt, format, values...)); the separate printfWithConvFmt() bridge is gone. - AwkSink.sprintf(convfmt, format, values...) is the single sprintf method and the customization point; sprintfWithConvFmt(), the reflection-based legacy-override bridge, and formatPrintfResult() are all removed. This is a breaking change for custom sinks, documented in behavior-changes.md for the next major version. - JRT encapsulates the sink and CONVFMT plumbing: AVM calls the new JRT.sprintf(format, values) instead of fetching the sink and CONVFMT separately. - The assertSprintf/assertSprintfThrows/assertToAwkString helpers are removed from AwkTestSupport, which is reserved for running AWK scripts and asserting their results; AwkPrintfTest calls AwkPrintf.sprintf directly again. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/backend/AVM.java | 6 +- .../java/io/jawk/jrt/AppendableAwkSink.java | 12 +- src/main/java/io/jawk/jrt/AwkSink.java | 104 +- src/main/java/io/jawk/jrt/JRT.java | 19 +- .../java/io/jawk/jrt/OutputStreamAwkSink.java | 9 +- src/site/markdown/behavior-changes.md | 13 +- src/site/markdown/java-output.md | 9 +- src/test/java/io/jawk/AwkTest.java | 2 +- src/test/java/io/jawk/AwkTestSupport.java | 69 -- src/test/java/io/jawk/jrt/AwkPrintfTest.java | 1087 ++++++++--------- 10 files changed, 571 insertions(+), 759 deletions(-) diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index 4b97e5fa..7bf3f06a 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -3030,9 +3030,7 @@ private Object invokeIndirectBuiltin( case SPRINTF: requireIndirectArgumentCount(builtin, args, 1, Integer.MAX_VALUE, lineNumber); return jrt - .getAwkSink() - .sprintfWithConvFmt( - jrt.getCONVFMTString(), + .sprintf( checkPosixFormat(jrt.toAwkString(args[0])), Arrays.copyOfRange(args, 1, args.length)); case SQRT: @@ -3786,7 +3784,7 @@ private Object[] popArguments(long numArgs) { private String sprintfFunction(long numArgs) { Object[] argArray = popArguments(numArgs - 1); String fmt = checkPosixFormat(jrt.toAwkString(pop())); - return jrt.getAwkSink().sprintfWithConvFmt(jrt.getCONVFMTString(), fmt, argArray); + return jrt.sprintf(fmt, argArray); } /** diff --git a/src/main/java/io/jawk/jrt/AppendableAwkSink.java b/src/main/java/io/jawk/jrt/AppendableAwkSink.java index 16d0a0bd..a362ab83 100644 --- a/src/main/java/io/jawk/jrt/AppendableAwkSink.java +++ b/src/main/java/io/jawk/jrt/AppendableAwkSink.java @@ -89,18 +89,10 @@ public void print(String ofs, String ors, String ofmt, Object... values) throws } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) throws IOException { synchronized (lock) { - appendable.append(formatPrintfResult(format, values)); - } - } - - @Override - public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) - throws IOException { - synchronized (lock) { - appendable.append(sprintfWithConvFmt(convfmt, format, values)); + appendable.append(sprintf(convfmt, format, values)); } } diff --git a/src/main/java/io/jawk/jrt/AwkSink.java b/src/main/java/io/jawk/jrt/AwkSink.java index 40e4d865..1cd11b01 100644 --- a/src/main/java/io/jawk/jrt/AwkSink.java +++ b/src/main/java/io/jawk/jrt/AwkSink.java @@ -84,36 +84,15 @@ public final Locale getLocale() { * @param ofs output field separator * @param ors output record separator * @param ofmt numeric output format available to the sink + * @param convfmt number-to-string conversion format ({@code CONVFMT}), + * used by {@code %s} to convert numeric values the way AWK does * @param format format string passed to {@code printf} * @param values arguments supplied after the format string * @throws IOException if the sink cannot write the output */ - public abstract void printf(String ofs, String ors, String ofmt, String format, Object... values) + public abstract void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) throws IOException; - /** - * Writes one AWK {@code printf} operation, with the current {@code CONVFMT} - * value so that {@code %s} can convert numeric values the way AWK does. - *

- * The default implementation ignores {@code convfmt} and delegates to - * {@link #printf(String, String, String, String, Object...)}, which keeps - * existing custom sinks working unchanged. The built-in sinks override this - * method so that {@code %s} honors the script's {@code CONVFMT} value. - *

- * - * @param ofs output field separator - * @param ors output record separator - * @param ofmt numeric output format available to the sink - * @param convfmt number-to-string conversion format ({@code CONVFMT}) - * @param format format string passed to {@code printf} - * @param values arguments supplied after the format string - * @throws IOException if the sink cannot write the output - */ - public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) - throws IOException { - printf(ofs, ors, ofmt, format, values); - } - /** * Flushes any buffered output held by this sink. * @@ -146,7 +125,7 @@ public PrintStream getPrintStream() { *

* This singleton is safe to share across all JRT/AVM instances because * its {@link #print(String, String, String, Object...)}, - * {@link #printf(String, String, String, String, Object...)}, and + * {@link #printf(String, String, String, String, String, Object...)}, and * {@link #flush()} operations are all no-ops. */ public static final AwkSink NOP_SINK = new NoOpAwkSink(); @@ -163,7 +142,7 @@ public void print(String ofs, String ors, String ofmt, Object... values) { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { // discard } } @@ -303,26 +282,6 @@ protected final Object normalizePrintArgument(Object value) { } } - /** - * Formats a string in the same way as AWK's {@code sprintf()} built-in, - * using the default {@code CONVFMT} value ({@code "%.6g"}). - *

- * The default implementation invokes the built-in formatting engine - * directly, so an override may safely decorate {@code super.sprintf(...)} - * results. The runtime routes script {@code printf}/{@code sprintf} calls - * through {@link #sprintfWithConvFmt(String, String, Object...)}, which - * honors overrides of either method. - *

- * - * @param format format string - * @param values arguments supplied after the format string - * @return formatted text - */ - public String sprintf(String format, Object... values) { - Object[] safeValues = values == null ? new Object[0] : values; - return AwkPrintf.sprintf(locale, AwkPrintf.DEFAULT_CONVFMT, format, safeValues); - } - /** * Formats a string in the same way as AWK's {@code sprintf()} built-in, * converting numeric {@code %s} operands with the supplied {@code CONVFMT} @@ -330,17 +289,9 @@ public String sprintf(String format, Object... values) { *

* Subclasses may override this method to customize formatting. The default * implementation delegates to - * {@link AwkPrintf#sprintf(Locale, String, String, Object...)}. Because the - * runtime routes both {@code printf} and {@code sprintf} through this - * method, overriding it ensures that both produce consistent output. - *

- *

- * For compatibility, a sink class that overrides the historical - * {@link #sprintf(String, Object...)} method but not this one keeps its - * customization: the default implementation detects such overrides and - * routes through them. {@code CONVFMT} cannot reach that legacy signature, - * so those sinks convert {@code %s} operands with the default - * {@code CONVFMT}. + * {@link AwkPrintf#sprintf(Locale, String, String, Object...)}. The + * built-in sinks render {@code printf} output through this method, so + * overriding it keeps {@code printf} and {@code sprintf} consistent. *

* * @param convfmt number-to-string conversion format ({@code CONVFMT}) @@ -348,48 +299,11 @@ public String sprintf(String format, Object... values) { * @param values arguments supplied after the format string * @return formatted text */ - public String sprintfWithConvFmt(String convfmt, String format, Object... values) { + public String sprintf(String convfmt, String format, Object... values) { Object[] safeValues = values == null ? new Object[0] : values; - if (overridesLegacySprintf()) { - return sprintf(format, safeValues); - } return AwkPrintf.sprintf(locale, convfmt, format, safeValues); } - /** - * Lazily computed flag: whether this sink's class overrides the historical - * {@link #sprintf(String, Object...)} customization point. - */ - private volatile Boolean legacySprintfOverride; - - private boolean overridesLegacySprintf() { - Boolean overridden = legacySprintfOverride; - if (overridden == null) { - try { - overridden = Boolean - .valueOf( - getClass() - .getMethod("sprintf", String.class, Object[].class) - .getDeclaringClass() != AwkSink.class); - } catch (NoSuchMethodException e) { - overridden = Boolean.FALSE; - } - legacySprintfOverride = overridden; - } - return overridden.booleanValue(); - } - - /** - * Formats one {@code printf} result string using this sink's locale. - * - * @param format format string passed to {@code printf} - * @param values arguments supplied after the format string - * @return formatted text - */ - protected final String formatPrintfResult(String format, Object... values) { - return sprintf(format, values); - } - /** * Formats one already-normalized AWK output value. * diff --git a/src/main/java/io/jawk/jrt/JRT.java b/src/main/java/io/jawk/jrt/JRT.java index f1459f8a..a4395f49 100644 --- a/src/main/java/io/jawk/jrt/JRT.java +++ b/src/main/java/io/jawk/jrt/JRT.java @@ -2520,7 +2520,20 @@ public void printToProcess(String cmd, Object[] values) throws IOException { * @throws IOException if the sink cannot be written to */ public void printfDefault(String format, Object[] values) throws IOException { - awkSink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); + awkSink.printf(ofs, ors, ofmt, convfmt, format, values); + } + + /** + * Formats a string in the same way as AWK's {@code sprintf()} built-in, + * through the default output sink and with the current {@code CONVFMT} + * value. + * + * @param format format string passed to {@code sprintf} + * @param values arguments supplied after the format string + * @return formatted text + */ + public String sprintf(String format, Object... values) { + return awkSink.sprintf(convfmt, format, values); } /** @@ -2535,7 +2548,7 @@ public void printfDefault(String format, Object[] values) throws IOException { public void printfToFile(String fileNameParam, boolean append, String format, Object[] values) throws IOException { AwkSink sink = getFileAwkSink(fileNameParam, append); - sink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); + sink.printf(ofs, ors, ofmt, convfmt, format, values); } /** @@ -2548,7 +2561,7 @@ public void printfToFile(String fileNameParam, boolean append, String format, Ob */ public void printfToProcess(String cmd, String format, Object[] values) throws IOException { AwkSink sink = getPipeAwkSink(cmd); - sink.printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values); + sink.printf(ofs, ors, ofmt, convfmt, format, values); sink.flush(); } diff --git a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java index e89ed0b6..1047d125 100644 --- a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java +++ b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java @@ -98,13 +98,8 @@ public void print(String ofs, String ors, String ofmt, Object... values) { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { - printStream.print(formatPrintfResult(format, values)); - } - - @Override - public void printfWithConvFmt(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { - printStream.print(sprintfWithConvFmt(convfmt, format, values)); + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { + printStream.print(sprintf(convfmt, format, values)); } @Override diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index db92d508..21ecac35 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -51,13 +51,12 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. prints the full decimal expansion `1267650600228229401496703205376` (previously `9223372036854775807`), and `int()` preserves such values ([#528](https://github.com/jawkio/jawk/issues/528)). -- For Java embedders: `AwkSink` gains `printfWithConvFmt(...)` and `sprintfWithConvFmt(...)`, - which receive the script's current `CONVFMT` value; the runtime now routes `printf` and - `sprintf` through these methods. Custom sinks that override the historical - `sprintf(String, Object...)` keep their customization (the default `sprintfWithConvFmt` - detects and routes through such overrides); override - `sprintfWithConvFmt(String, String, Object...)` to also receive the script's `CONVFMT`. - The `org.metricshub:printf4j` dependency has been removed; its formatting logic now lives in +- Breaking change for Java embedders: `AwkSink.printf(...)` now receives the script's current + `CONVFMT` value as a parameter (between `ofmt` and `format`), just like it already received + `OFMT`, and `AwkSink.sprintf(...)` now takes `CONVFMT` as its first parameter + (`sprintf(convfmt, format, values...)`). Custom sinks must be updated to the new signatures; + overriding `sprintf` still customizes both `printf` and `sprintf` output. The + `org.metricshub:printf4j` dependency has been removed; its formatting logic now lives in `io.jawk.jrt.AwkPrintf` ([#528](https://github.com/jawkio/jawk/issues/528)). ## [v7.0.01](https://github.com/jawkio/jawk/releases/tag/v7.0.01) (2026-07-31) diff --git a/src/site/markdown/java-output.md b/src/site/markdown/java-output.md index d98ed45c..fe8ef66f 100644 --- a/src/site/markdown/java-output.md +++ b/src/site/markdown/java-output.md @@ -66,7 +66,7 @@ public final class CollectingSink extends AwkSink { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { // store format + values however your application wants } @@ -100,14 +100,9 @@ public final class CollectingSink extends AwkSink { > | `ofs` | `OFS` | Output Field Separator, inserted between values | > | `ors` | `ORS` | Output Record Separator, appended after the record | > | `ofmt` | `OFMT` | Default numeric output format | -> | `convfmt` | `CONVFMT` | Number-to-string conversion format used by `%s` (only in `printfWithConvFmt(...)`) | +> | `convfmt` | `CONVFMT` | Number-to-string conversion format used by `%s` | > | `format` | — | The AWK format string | > | `values` | — | The AWK values to be formatted | -> -> The runtime invokes `printfWithConvFmt(ofs, ors, ofmt, convfmt, format, values...)`, whose -> default implementation drops `convfmt` and delegates to `printf(...)`, so existing sinks keep -> working. Override `printfWithConvFmt(...)` when your sink formats output itself and should -> honor the script's `CONVFMT` value. ### getPrintStream diff --git a/src/test/java/io/jawk/AwkTest.java b/src/test/java/io/jawk/AwkTest.java index 056b7ea0..4067d9e9 100644 --- a/src/test/java/io/jawk/AwkTest.java +++ b/src/test/java/io/jawk/AwkTest.java @@ -2052,7 +2052,7 @@ public void print(String ofs, String ors, String ofmt, Object... values) { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { printfFormats.add(format); printfValues.add(Arrays.asList(Arrays.copyOf(values, values.length))); } diff --git a/src/test/java/io/jawk/AwkTestSupport.java b/src/test/java/io/jawk/AwkTestSupport.java index 8af1e7cb..1b257179 100644 --- a/src/test/java/io/jawk/AwkTestSupport.java +++ b/src/test/java/io/jawk/AwkTestSupport.java @@ -120,75 +120,6 @@ public static Path sharedTempDirectory() { return SHARED_TEMP_DIR; } - /** - * Asserts that AWK's {@code sprintf()} formatting engine - * ({@link io.jawk.jrt.AwkPrintf}) produces the expected text with the - * default {@link Locale#US} locale and default {@code CONVFMT}. This is the - * standard helper for formatter-level unit tests, mirroring what a script - * calling {@code sprintf(format, args...)} would produce. - * - * @param expected the expected formatted text - * @param format AWK format string - * @param args arguments supplied after the format string - */ - public static void assertSprintf(String expected, String format, Object... args) { - org.junit.Assert - .assertEquals( - "sprintf(\"" + format + "\")", - expected, - io.jawk.jrt.AwkPrintf.sprintf(format, args)); - } - - /** - * Asserts that AWK's {@code sprintf()} formatting engine - * ({@link io.jawk.jrt.AwkPrintf}) produces the expected text with an - * explicit locale and {@code CONVFMT} value. - * - * @param expected the expected formatted text - * @param locale locale used for numeric formatting - * @param convfmt number-to-string conversion format ({@code CONVFMT}) - * @param format AWK format string - * @param args arguments supplied after the format string - */ - public static void assertSprintf(String expected, Locale locale, String convfmt, String format, Object... args) { - org.junit.Assert - .assertEquals( - "sprintf(\"" + format + "\") with locale " + locale + " and CONVFMT \"" + convfmt + "\"", - expected, - io.jawk.jrt.AwkPrintf.sprintf(locale, convfmt, format, args)); - } - - /** - * Asserts that AWK's {@code sprintf()} formatting engine raises the given - * exception, as it does for a format string with too few arguments. - * - * @param expectedThrowable the exception type expected from the call - * @param format AWK format string - * @param args arguments supplied after the format string - */ - public static void assertSprintfThrows( - Class expectedThrowable, - String format, - Object... args) { - org.junit.Assert.assertThrows(expectedThrowable, () -> io.jawk.jrt.AwkPrintf.sprintf(format, args)); - } - - /** - * Asserts AWK's number-to-string conversion - * ({@link io.jawk.jrt.AwkPrintf#toAwkString(Object, String, Locale)}) with - * the default {@link Locale#US} locale and default {@code CONVFMT}. - * - * @param expected the expected AWK string value - * @param value the value to convert - */ - public static void assertToAwkString(String expected, Object value) { - org.junit.Assert - .assertEquals( - "toAwkString(" + value + ")", - expected, - io.jawk.jrt.AwkPrintf.toAwkString(value, io.jawk.jrt.AwkPrintf.DEFAULT_CONVFMT, Locale.US)); - } - /** * Represents a fully configured test case produced by one of the builders. * Implementations know how to prepare the execution environment, run the diff --git a/src/test/java/io/jawk/jrt/AwkPrintfTest.java b/src/test/java/io/jawk/jrt/AwkPrintfTest.java index af7c7fbc..66109e1f 100644 --- a/src/test/java/io/jawk/jrt/AwkPrintfTest.java +++ b/src/test/java/io/jawk/jrt/AwkPrintfTest.java @@ -23,18 +23,14 @@ */ import static org.junit.Assert.assertEquals; -import static io.jawk.AwkTestSupport.assertSprintf; -import static io.jawk.AwkTestSupport.assertSprintfThrows; -import static io.jawk.AwkTestSupport.assertToAwkString; +import static org.junit.Assert.assertThrows; +import static io.jawk.jrt.AwkPrintf.sprintf; import java.util.Locale; import org.junit.Test; /** - * Unit tests for {@link AwkPrintf}, written with the - * {@code io.jawk.AwkTestSupport} formatter assertion helpers - * ({@code assertSprintf}, {@code assertSprintfThrows}, - * {@code assertToAwkString}). + * Unit tests for {@link AwkPrintf}. *

* This suite incorporates the complete unit test suite of the former * Printf4J project, @@ -47,461 +43,463 @@ public class AwkPrintfTest { @Test public void testPlus() { - assertSprintf("+42", "%+d", 42); - assertSprintf("-42", "%+d", -42); - assertSprintf(" +42", "%+5d", 42); - assertSprintf(" -42", "%+5d", -42); - assertSprintf(" +42", "%+15d", 42); - assertSprintf(" -42", "%+15d", -42); - assertSprintf("Hello testing", "%+s", "Hello testing"); - assertSprintf("+1024", "%+d", 1024); - assertSprintf("-1024", "%+d", -1024); - assertSprintf("+1024", "%+i", 1024); - assertSprintf("-1024", "%+i", -1024); - assertSprintf("1024", "%+u", 1024); - assertSprintf("4294966272", "%+u", 4294966272L); - assertSprintf("777", "%+o", 511); - assertSprintf("37777777001", "%+o", 4294966785L); - assertSprintf("1234abcd", "%+x", 305441741); - assertSprintf("edcb5433", "%+x", 3989525555L); - assertSprintf("1234ABCD", "%+X", 305441741); - assertSprintf("EDCB5433", "%+X", 3989525555L); - assertSprintf("x", "%+c", 'x'); + assertEquals("+42", sprintf("%+d", 42)); + assertEquals("-42", sprintf("%+d", -42)); + assertEquals(" +42", sprintf("%+5d", 42)); + assertEquals(" -42", sprintf("%+5d", -42)); + assertEquals(" +42", sprintf("%+15d", 42)); + assertEquals(" -42", sprintf("%+15d", -42)); + assertEquals("Hello testing", sprintf("%+s", "Hello testing")); + assertEquals("+1024", sprintf("%+d", 1024)); + assertEquals("-1024", sprintf("%+d", -1024)); + assertEquals("+1024", sprintf("%+i", 1024)); + assertEquals("-1024", sprintf("%+i", -1024)); + assertEquals("1024", sprintf("%+u", 1024)); + assertEquals("4294966272", sprintf("%+u", 4294966272L)); + assertEquals("777", sprintf("%+o", 511)); + assertEquals("37777777001", sprintf("%+o", 4294966785L)); + assertEquals("1234abcd", sprintf("%+x", 305441741)); + assertEquals("edcb5433", sprintf("%+x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%+X", 305441741)); + assertEquals("EDCB5433", sprintf("%+X", 3989525555L)); + assertEquals("x", sprintf("%+c", 'x')); // Was commented out in Printf4J expecting "0": gawk prints nothing for // a zero value with an explicit zero precision, even with sign flags. - assertSprintf("", "%+.0d", 0); + assertEquals("", sprintf("%+.0d", 0)); } @Test public void testBlank() { - assertSprintf(" 42", "% d", 42); - assertSprintf("-42", "% d", -42); - assertSprintf(" 42", "% 5d", 42); - assertSprintf(" -42", "% 5d", -42); - assertSprintf(" 42", "% 15d", 42); - assertSprintf(" -42", "% 15d", -42); - assertSprintf(" -42", "% 15d", -42); - assertSprintf(" -42.987", "% 15.3f", -42.987); - assertSprintf(" 42.987", "% 15.3f", 42.987); - assertSprintf("Hello testing", "% s", "Hello testing"); - assertSprintf(" 1024", "% d", 1024); - assertSprintf("-1024", "% d", -1024); - assertSprintf(" 1024", "% i", 1024); - assertSprintf("-1024", "% i", -1024); - assertSprintf("1024", "% u", 1024); - assertSprintf("4294966272", "% u", 4294966272L); - assertSprintf("777", "% o", 511); - assertSprintf("37777777001", "% o", 4294966785L); - assertSprintf("1234abcd", "% x", 305441741); - assertSprintf("edcb5433", "% x", 3989525555L); - assertSprintf("1234ABCD", "% X", 305441741); - assertSprintf("EDCB5433", "% X", 3989525555L); - assertSprintf("x", "% c", 'x'); + assertEquals(" 42", sprintf("% d", 42)); + assertEquals("-42", sprintf("% d", -42)); + assertEquals(" 42", sprintf("% 5d", 42)); + assertEquals(" -42", sprintf("% 5d", -42)); + assertEquals(" 42", sprintf("% 15d", 42)); + assertEquals(" -42", sprintf("% 15d", -42)); + assertEquals(" -42", sprintf("% 15d", -42)); + assertEquals(" -42.987", sprintf("% 15.3f", -42.987)); + assertEquals(" 42.987", sprintf("% 15.3f", 42.987)); + assertEquals("Hello testing", sprintf("% s", "Hello testing")); + assertEquals(" 1024", sprintf("% d", 1024)); + assertEquals("-1024", sprintf("% d", -1024)); + assertEquals(" 1024", sprintf("% i", 1024)); + assertEquals("-1024", sprintf("% i", -1024)); + assertEquals("1024", sprintf("% u", 1024)); + assertEquals("4294966272", sprintf("% u", 4294966272L)); + assertEquals("777", sprintf("% o", 511)); + assertEquals("37777777001", sprintf("% o", 4294966785L)); + assertEquals("1234abcd", sprintf("% x", 305441741)); + assertEquals("edcb5433", sprintf("% x", 3989525555L)); + assertEquals("1234ABCD", sprintf("% X", 305441741)); + assertEquals("EDCB5433", sprintf("% X", 3989525555L)); + assertEquals("x", sprintf("% c", 'x')); } @Test public void testZero() { - assertSprintf("42", "%0d", 42); - assertSprintf("42", "%0ld", 42L); - assertSprintf("-42", "%0d", -42); - assertSprintf("00042", "%05d", 42); - assertSprintf("-0042", "%05d", -42); - assertSprintf("000000000000042", "%015d", 42); - assertSprintf("-00000000000042", "%015d", -42); - assertSprintf("000000000042.12", "%015.2f", 42.1234); - assertSprintf("00000000042.988", "%015.3f", 42.9876); - assertSprintf("-00000042.98760", "%015.5f", -42.9876); + assertEquals("42", sprintf("%0d", 42)); + assertEquals("42", sprintf("%0ld", 42L)); + assertEquals("-42", sprintf("%0d", -42)); + assertEquals("00042", sprintf("%05d", 42)); + assertEquals("-0042", sprintf("%05d", -42)); + assertEquals("000000000000042", sprintf("%015d", 42)); + assertEquals("-00000000000042", sprintf("%015d", -42)); + assertEquals("000000000042.12", sprintf("%015.2f", 42.1234)); + assertEquals("00000000042.988", sprintf("%015.3f", 42.9876)); + assertEquals("-00000042.98760", sprintf("%015.5f", -42.9876)); } @Test public void testMinus() { - assertSprintf("42", "%-d", 42); - assertSprintf("-42", "%-d", -42); - assertSprintf("42 ", "%-5d", 42); - assertSprintf("-42 ", "%-5d", -42); - assertSprintf("42 ", "%-15d", 42); - assertSprintf("-42 ", "%-15d", -42); - assertSprintf("42", "%-0d", 42); - assertSprintf("-42", "%-0d", -42); - assertSprintf("42 ", "%-05d", 42); - assertSprintf("-42 ", "%-05d", -42); - assertSprintf("42 ", "%-015d", 42); - assertSprintf("-42 ", "%-015d", -42); - assertSprintf("42", "%0-d", 42); - assertSprintf("-42", "%0-d", -42); - assertSprintf("42 ", "%0-5d", 42); - assertSprintf("-42 ", "%0-5d", -42); - assertSprintf("42 ", "%0-15d", 42); - assertSprintf("-42 ", "%0-15d", -42); - assertSprintf("-4.200e+01 ", "%0-15.3e", -42.); + assertEquals("42", sprintf("%-d", 42)); + assertEquals("-42", sprintf("%-d", -42)); + assertEquals("42 ", sprintf("%-5d", 42)); + assertEquals("-42 ", sprintf("%-5d", -42)); + assertEquals("42 ", sprintf("%-15d", 42)); + assertEquals("-42 ", sprintf("%-15d", -42)); + assertEquals("42", sprintf("%-0d", 42)); + assertEquals("-42", sprintf("%-0d", -42)); + assertEquals("42 ", sprintf("%-05d", 42)); + assertEquals("-42 ", sprintf("%-05d", -42)); + assertEquals("42 ", sprintf("%-015d", 42)); + assertEquals("-42 ", sprintf("%-015d", -42)); + assertEquals("42", sprintf("%0-d", 42)); + assertEquals("-42", sprintf("%0-d", -42)); + assertEquals("42 ", sprintf("%0-5d", 42)); + assertEquals("-42 ", sprintf("%0-5d", -42)); + assertEquals("42 ", sprintf("%0-15d", 42)); + assertEquals("-42 ", sprintf("%0-15d", -42)); + assertEquals("-4.200e+01 ", sprintf("%0-15.3e", -42.)); // Printf4J expected "-42.0 ": AWK's %g removes trailing // zeros, so gawk prints "-42 ". - assertSprintf("-42 ", "%0-15.3g", -42.); + assertEquals("-42 ", sprintf("%0-15.3g", -42.)); } @Test public void testHash() { // Printf4J expected "" here, but gawk prints "0" for a zero value // with '#' and a zero precision on %x. - assertSprintf("0", "%#.0x", 0); + assertEquals("0", sprintf("%#.0x", 0)); // Printf4J had this assertion commented out as "the real expected // behavior, which is wrong IMO" (it returned "0x0" instead): C and // gawk agree on "0", which is what AwkPrintf now produces. - assertSprintf("0", "%#.1x", 0); + assertEquals("0", sprintf("%#.1x", 0)); // "%#.0llx" is invalid in gawk: doubled length modifiers make the // whole specifier print verbatim, without consuming an argument. - assertSprintf("%#.0llx", "%#.0llx", 0); - assertSprintf("0x0000614e", "%#.8x", 0x614e); + assertEquals("%#.0llx", sprintf("%#.0llx", 0)); + assertEquals("0x0000614e", sprintf("%#.8x", 0x614e)); // Was commented out in Printf4J ("binary is not supported for now"): // %b is not an AWK conversion, so gawk prints the specifier verbatim. - assertSprintf("%#b", "%#b", 6); + assertEquals("%#b", sprintf("%#b", 6)); // gawk-verified: the '#' prefix depends on the original value, so a // nonzero fraction that truncates to zero keeps the prefix. - assertSprintf("0x0", "%#.0x", 0.1); - assertSprintf("0x0", "%#x", 0.5); + assertEquals("0x0", sprintf("%#.0x", 0.1)); + assertEquals("0x0", sprintf("%#x", 0.5)); // gawk-verified: '#' with %o always adds its leading zero on nonzero // values, in addition to any precision padding. - assertSprintf("00", "%#o", 0.5); - assertSprintf("00", "%#.0o", 0.2); - assertSprintf("000001", "%#.5o", 1); - assertSprintf("0010", "%#.3o", 8); - assertSprintf("010", "%#o", 8); + assertEquals("00", sprintf("%#o", 0.5)); + assertEquals("00", sprintf("%#.0o", 0.2)); + assertEquals("000001", sprintf("%#.5o", 1)); + assertEquals("0010", sprintf("%#.3o", 8)); + assertEquals("010", sprintf("%#o", 8)); } @Test public void testSpecifier() { - assertSprintf("Hello testing", "Hello testing"); - assertSprintf("Hello testing", "%s", "Hello testing"); - assertSprintf("1024", "%d", 1024); - assertSprintf("-1024", "%d", -1024); - assertSprintf("1024", "%i", 1024); - assertSprintf("-1024", "%i", -1024); - assertSprintf("1024", "%u", 1024); - assertSprintf("4294966272", "%u", 4294966272L); - assertSprintf("777", "%o", 511); - assertSprintf("37777777001", "%o", 4294966785L); - assertSprintf("1234abcd", "%x", 305441741); - assertSprintf("edcb5433", "%x", 3989525555L); - assertSprintf("1234ABCD", "%X", 305441741); - assertSprintf("EDCB5433", "%X", 3989525555L); - assertSprintf("%", "%%"); + assertEquals("Hello testing", sprintf("Hello testing")); + assertEquals("Hello testing", sprintf("%s", "Hello testing")); + assertEquals("1024", sprintf("%d", 1024)); + assertEquals("-1024", sprintf("%d", -1024)); + assertEquals("1024", sprintf("%i", 1024)); + assertEquals("-1024", sprintf("%i", -1024)); + assertEquals("1024", sprintf("%u", 1024)); + assertEquals("4294966272", sprintf("%u", 4294966272L)); + assertEquals("777", sprintf("%o", 511)); + assertEquals("37777777001", sprintf("%o", 4294966785L)); + assertEquals("1234abcd", sprintf("%x", 305441741)); + assertEquals("edcb5433", sprintf("%x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%X", 305441741)); + assertEquals("EDCB5433", sprintf("%X", 3989525555L)); + assertEquals("%", sprintf("%%")); // gawk-verified: a percent conversion ignores flags, width, and // precision. - assertSprintf("%", "%5%"); - assertSprintf("%", "%-3%"); - assertSprintf("%", "%0.2%"); + assertEquals("%", sprintf("%5%")); + assertEquals("%", sprintf("%-3%")); + assertEquals("%", sprintf("%0.2%")); // ...but an explicit position still pins the format to positional // mode, so mixing with a sequential conversion is fatal, like gawk. - assertSprintfThrows(AwkRuntimeException.class, "%1$%|%s", "a"); + assertThrows(AwkRuntimeException.class, () -> sprintf("%1$%|%s", "a")); // gawk validates the index of a positioned conversion even when it // consumes no argument. - assertSprintfThrows(AwkRuntimeException.class, "%2$%", 1); - assertSprintfThrows(AwkRuntimeException.class, "%2$q", 1); - assertSprintfThrows(AwkRuntimeException.class, "%1$%"); + assertThrows(AwkRuntimeException.class, () -> sprintf("%2$%", 1)); + assertThrows(AwkRuntimeException.class, () -> sprintf("%2$q", 1)); + assertThrows(AwkRuntimeException.class, () -> sprintf("%1$%")); // gawk-verified: a zero-indexed star operand means the value zero // without consuming an argument, while an out-of-range one is fatal. - assertSprintf("7|42", "%*0$d|%d", 7, 42); - assertSprintf("3|42", "%.*0$f|%d", 3.14159, 42); - assertSprintfThrows(AwkRuntimeException.class, "%*5$d|%d", 7, 42); + assertEquals("7|42", sprintf("%*0$d|%d", 7, 42)); + assertEquals("3|42", sprintf("%.*0$f|%d", 3.14159, 42)); + assertThrows(AwkRuntimeException.class, () -> sprintf("%*5$d|%d", 7, 42)); } @Test public void testWidth() { - assertSprintf("Hello testing", "%1s", "Hello testing"); - assertSprintf("1024", "%1d", 1024); - assertSprintf("-1024", "%1d", -1024); - assertSprintf("1024", "%1i", 1024); - assertSprintf("-1024", "%1i", -1024); - assertSprintf("1024", "%1u", 1024); - assertSprintf("4294966272", "%1u", 4294966272L); - assertSprintf("777", "%1o", 511); - assertSprintf("37777777001", "%1o", 4294966785L); - assertSprintf("1234abcd", "%1x", 305441741); - assertSprintf("edcb5433", "%1x", 3989525555L); - assertSprintf("1234ABCD", "%1X", 305441741); - assertSprintf("EDCB5433", "%1X", 3989525555L); - assertSprintf("x", "%1c", 'x'); + assertEquals("Hello testing", sprintf("%1s", "Hello testing")); + assertEquals("1024", sprintf("%1d", 1024)); + assertEquals("-1024", sprintf("%1d", -1024)); + assertEquals("1024", sprintf("%1i", 1024)); + assertEquals("-1024", sprintf("%1i", -1024)); + assertEquals("1024", sprintf("%1u", 1024)); + assertEquals("4294966272", sprintf("%1u", 4294966272L)); + assertEquals("777", sprintf("%1o", 511)); + assertEquals("37777777001", sprintf("%1o", 4294966785L)); + assertEquals("1234abcd", sprintf("%1x", 305441741)); + assertEquals("edcb5433", sprintf("%1x", 3989525555L)); + assertEquals("1234ABCD", sprintf("%1X", 305441741)); + assertEquals("EDCB5433", sprintf("%1X", 3989525555L)); + assertEquals("x", sprintf("%1c", 'x')); } @Test public void testWidth20() { - assertSprintf(" Hello", "%20s", "Hello"); - assertSprintf(" 1024", "%20d", 1024); - assertSprintf(" -1024", "%20d", -1024); - assertSprintf(" 1024", "%20i", 1024); - assertSprintf(" -1024", "%20i", -1024); - assertSprintf(" 1024", "%20u", 1024); - assertSprintf(" 4294966272", "%20u", 4294966272L); - assertSprintf(" 777", "%20o", 511); - assertSprintf(" 37777777001", "%20o", 4294966785L); - assertSprintf(" 1234abcd", "%20x", 305441741); - assertSprintf(" edcb5433", "%20x", 3989525555L); - assertSprintf(" 1234ABCD", "%20X", 305441741); - assertSprintf(" EDCB5433", "%20X", 3989525555L); - assertSprintf(" x", "%20c", 'x'); + assertEquals(" Hello", sprintf("%20s", "Hello")); + assertEquals(" 1024", sprintf("%20d", 1024)); + assertEquals(" -1024", sprintf("%20d", -1024)); + assertEquals(" 1024", sprintf("%20i", 1024)); + assertEquals(" -1024", sprintf("%20i", -1024)); + assertEquals(" 1024", sprintf("%20u", 1024)); + assertEquals(" 4294966272", sprintf("%20u", 4294966272L)); + assertEquals(" 777", sprintf("%20o", 511)); + assertEquals(" 37777777001", sprintf("%20o", 4294966785L)); + assertEquals(" 1234abcd", sprintf("%20x", 305441741)); + assertEquals(" edcb5433", sprintf("%20x", 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%20X", 305441741)); + assertEquals(" EDCB5433", sprintf("%20X", 3989525555L)); + assertEquals(" x", sprintf("%20c", 'x')); } @Test public void testWidthStar20() { - assertSprintf(" Hello", "%*s", 20, "Hello"); - assertSprintf(" 1024", "%*d", 20, 1024); - assertSprintf(" -1024", "%*d", 20, -1024); - assertSprintf(" 1024", "%*i", 20, 1024); - assertSprintf(" -1024", "%*i", 20, -1024); - assertSprintf(" 1024", "%*u", 20, 1024); - assertSprintf(" 4294966272", "%*u", 20, 4294966272L); - assertSprintf(" 777", "%*o", 20, 511); - assertSprintf(" 37777777001", "%*o", 20, 4294966785L); - assertSprintf(" 1234abcd", "%*x", 20, 305441741); - assertSprintf(" edcb5433", "%*x", 20, 3989525555L); - assertSprintf(" 1234ABCD", "%*X", 20, 305441741); - assertSprintf(" EDCB5433", "%*X", 20, 3989525555L); - assertSprintf(" x", "%*c", 20, 'x'); + assertEquals(" Hello", sprintf("%*s", 20, "Hello")); + assertEquals(" 1024", sprintf("%*d", 20, 1024)); + assertEquals(" -1024", sprintf("%*d", 20, -1024)); + assertEquals(" 1024", sprintf("%*i", 20, 1024)); + assertEquals(" -1024", sprintf("%*i", 20, -1024)); + assertEquals(" 1024", sprintf("%*u", 20, 1024)); + assertEquals(" 4294966272", sprintf("%*u", 20, 4294966272L)); + assertEquals(" 777", sprintf("%*o", 20, 511)); + assertEquals(" 37777777001", sprintf("%*o", 20, 4294966785L)); + assertEquals(" 1234abcd", sprintf("%*x", 20, 305441741)); + assertEquals(" edcb5433", sprintf("%*x", 20, 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%*X", 20, 305441741)); + assertEquals(" EDCB5433", sprintf("%*X", 20, 3989525555L)); + assertEquals(" x", sprintf("%*c", 20, 'x')); } @Test public void testMinus20() { - assertSprintf("Hello ", "%-20s", "Hello"); - assertSprintf("1024 ", "%-20d", 1024); - assertSprintf("-1024 ", "%-20d", -1024); - assertSprintf("1024 ", "%-20i", 1024); - assertSprintf("-1024 ", "%-20i", -1024); - assertSprintf("1024 ", "%-20u", 1024); - assertSprintf("1024.1234 ", "%-20.4f", 1024.1234); - assertSprintf("4294966272 ", "%-20u", 4294966272L); - assertSprintf("777 ", "%-20o", 511); - assertSprintf("37777777001 ", "%-20o", 4294966785L); - assertSprintf("1234abcd ", "%-20x", 305441741); - assertSprintf("edcb5433 ", "%-20x", 3989525555L); - assertSprintf("1234ABCD ", "%-20X", 305441741); - assertSprintf("EDCB5433 ", "%-20X", 3989525555L); - assertSprintf("x ", "%-20c", 'x'); - assertSprintf("| 9| |9 | | 9|", "|%5d| |%-2d| |%5d|", 9, 9, 9); - assertSprintf("| 10| |10| | 10|", "|%5d| |%-2d| |%5d|", 10, 10, 10); - assertSprintf("| 9| |9 | | 9|", "|%5d| |%-12d| |%5d|", 9, 9, 9); - assertSprintf("| 10| |10 | | 10|", "|%5d| |%-12d| |%5d|", 10, 10, 10); + assertEquals("Hello ", sprintf("%-20s", "Hello")); + assertEquals("1024 ", sprintf("%-20d", 1024)); + assertEquals("-1024 ", sprintf("%-20d", -1024)); + assertEquals("1024 ", sprintf("%-20i", 1024)); + assertEquals("-1024 ", sprintf("%-20i", -1024)); + assertEquals("1024 ", sprintf("%-20u", 1024)); + assertEquals("1024.1234 ", sprintf("%-20.4f", 1024.1234)); + assertEquals("4294966272 ", sprintf("%-20u", 4294966272L)); + assertEquals("777 ", sprintf("%-20o", 511)); + assertEquals("37777777001 ", sprintf("%-20o", 4294966785L)); + assertEquals("1234abcd ", sprintf("%-20x", 305441741)); + assertEquals("edcb5433 ", sprintf("%-20x", 3989525555L)); + assertEquals("1234ABCD ", sprintf("%-20X", 305441741)); + assertEquals("EDCB5433 ", sprintf("%-20X", 3989525555L)); + assertEquals("x ", sprintf("%-20c", 'x')); + assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-2d| |%5d|", 9, 9, 9)); + assertEquals("| 10| |10| | 10|", sprintf("|%5d| |%-2d| |%5d|", 10, 10, 10)); + assertEquals("| 9| |9 | | 9|", sprintf("|%5d| |%-12d| |%5d|", 9, 9, 9)); + assertEquals("| 10| |10 | | 10|", sprintf("|%5d| |%-12d| |%5d|", 10, 10, 10)); } @Test public void testZeroMinus20() { - assertSprintf("Hello ", "%0-20s", "Hello"); - assertSprintf("1024 ", "%0-20d", 1024); - assertSprintf("-1024 ", "%0-20d", -1024); - assertSprintf("1024 ", "%0-20i", 1024); - assertSprintf("-1024 ", "%0-20i", -1024); - assertSprintf("1024 ", "%0-20u", 1024); - assertSprintf("4294966272 ", "%0-20u", 4294966272L); - assertSprintf("777 ", "%0-20o", 511); - assertSprintf("37777777001 ", "%0-20o", 4294966785L); - assertSprintf("1234abcd ", "%0-20x", 305441741); - assertSprintf("edcb5433 ", "%0-20x", 3989525555L); - assertSprintf("1234ABCD ", "%0-20X", 305441741); - assertSprintf("EDCB5433 ", "%0-20X", 3989525555L); - assertSprintf("x ", "%0-20c", 'x'); + assertEquals("Hello ", sprintf("%0-20s", "Hello")); + assertEquals("1024 ", sprintf("%0-20d", 1024)); + assertEquals("-1024 ", sprintf("%0-20d", -1024)); + assertEquals("1024 ", sprintf("%0-20i", 1024)); + assertEquals("-1024 ", sprintf("%0-20i", -1024)); + assertEquals("1024 ", sprintf("%0-20u", 1024)); + assertEquals("4294966272 ", sprintf("%0-20u", 4294966272L)); + assertEquals("777 ", sprintf("%0-20o", 511)); + assertEquals("37777777001 ", sprintf("%0-20o", 4294966785L)); + assertEquals("1234abcd ", sprintf("%0-20x", 305441741)); + assertEquals("edcb5433 ", sprintf("%0-20x", 3989525555L)); + assertEquals("1234ABCD ", sprintf("%0-20X", 305441741)); + assertEquals("EDCB5433 ", sprintf("%0-20X", 3989525555L)); + assertEquals("x ", sprintf("%0-20c", 'x')); } @Test public void testPadding20() { - assertSprintf("00000000000000001024", "%020d", 1024); - assertSprintf("-0000000000000001024", "%020d", -1024); - assertSprintf("00000000000000001024", "%020i", 1024); - assertSprintf("-0000000000000001024", "%020i", -1024); - assertSprintf("00000000000000001024", "%020u", 1024); - assertSprintf("00000000004294966272", "%020u", 4294966272L); - assertSprintf("00000000000000000777", "%020o", 511); - assertSprintf("00000000037777777001", "%020o", 4294966785L); - assertSprintf("0000000000001234abcd", "%020x", 305441741); - assertSprintf("000000000000edcb5433", "%020x", 3989525555L); - assertSprintf("0000000000001234ABCD", "%020X", 305441741); - assertSprintf("000000000000EDCB5433", "%020X", 3989525555L); + assertEquals("00000000000000001024", sprintf("%020d", 1024)); + assertEquals("-0000000000000001024", sprintf("%020d", -1024)); + assertEquals("00000000000000001024", sprintf("%020i", 1024)); + assertEquals("-0000000000000001024", sprintf("%020i", -1024)); + assertEquals("00000000000000001024", sprintf("%020u", 1024)); + assertEquals("00000000004294966272", sprintf("%020u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%020o", 511)); + assertEquals("00000000037777777001", sprintf("%020o", 4294966785L)); + assertEquals("0000000000001234abcd", sprintf("%020x", 305441741)); + assertEquals("000000000000edcb5433", sprintf("%020x", 3989525555L)); + assertEquals("0000000000001234ABCD", sprintf("%020X", 305441741)); + assertEquals("000000000000EDCB5433", sprintf("%020X", 3989525555L)); } @Test public void testPaddingPrecision20() { - assertSprintf("00000000000000001024", "%.20d", 1024); - assertSprintf("-00000000000000001024", "%.20d", -1024); - assertSprintf("00000000000000001024", "%.20i", 1024); - assertSprintf("-00000000000000001024", "%.20i", -1024); - assertSprintf("00000000000000001024", "%.20u", 1024); - assertSprintf("00000000004294966272", "%.20u", 4294966272L); - assertSprintf("00000000000000000777", "%.20o", 511); - assertSprintf("00000000037777777001", "%.20o", 4294966785L); - assertSprintf("0000000000001234abcd", "%.20x", 305441741); - assertSprintf("000000000000edcb5433", "%.20x", 3989525555L); - assertSprintf("0000000000001234ABCD", "%.20X", 305441741); - assertSprintf("000000000000EDCB5433", "%.20X", 3989525555L); + assertEquals("00000000000000001024", sprintf("%.20d", 1024)); + assertEquals("-00000000000000001024", sprintf("%.20d", -1024)); + assertEquals("00000000000000001024", sprintf("%.20i", 1024)); + assertEquals("-00000000000000001024", sprintf("%.20i", -1024)); + assertEquals("00000000000000001024", sprintf("%.20u", 1024)); + assertEquals("00000000004294966272", sprintf("%.20u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%.20o", 511)); + assertEquals("00000000037777777001", sprintf("%.20o", 4294966785L)); + assertEquals("0000000000001234abcd", sprintf("%.20x", 305441741)); + assertEquals("000000000000edcb5433", sprintf("%.20x", 3989525555L)); + assertEquals("0000000000001234ABCD", sprintf("%.20X", 305441741)); + assertEquals("000000000000EDCB5433", sprintf("%.20X", 3989525555L)); } @Test public void testPaddingHashZero20() { - assertSprintf("00000000000000001024", "%#020d", 1024); - assertSprintf("-0000000000000001024", "%#020d", -1024); - assertSprintf("00000000000000001024", "%#020i", 1024); - assertSprintf("-0000000000000001024", "%#020i", -1024); - assertSprintf("00000000000000001024", "%#020u", 1024); - assertSprintf("00000000004294966272", "%#020u", 4294966272L); - assertSprintf("00000000000000000777", "%#020o", 511); - assertSprintf("00000000037777777001", "%#020o", 4294966785L); - assertSprintf("0x00000000001234abcd", "%#020x", 305441741); - assertSprintf("0x0000000000edcb5433", "%#020x", 3989525555L); - assertSprintf("0X00000000001234ABCD", "%#020X", 305441741); - assertSprintf("0X0000000000EDCB5433", "%#020X", 3989525555L); + assertEquals("00000000000000001024", sprintf("%#020d", 1024)); + assertEquals("-0000000000000001024", sprintf("%#020d", -1024)); + assertEquals("00000000000000001024", sprintf("%#020i", 1024)); + assertEquals("-0000000000000001024", sprintf("%#020i", -1024)); + assertEquals("00000000000000001024", sprintf("%#020u", 1024)); + assertEquals("00000000004294966272", sprintf("%#020u", 4294966272L)); + assertEquals("00000000000000000777", sprintf("%#020o", 511)); + assertEquals("00000000037777777001", sprintf("%#020o", 4294966785L)); + assertEquals("0x00000000001234abcd", sprintf("%#020x", 305441741)); + assertEquals("0x0000000000edcb5433", sprintf("%#020x", 3989525555L)); + assertEquals("0X00000000001234ABCD", sprintf("%#020X", 305441741)); + assertEquals("0X0000000000EDCB5433", sprintf("%#020X", 3989525555L)); } @Test public void testPaddingHash20() { - assertSprintf(" 1024", "%#20d", 1024); - assertSprintf(" -1024", "%#20d", -1024); - assertSprintf(" 1024", "%#20i", 1024); - assertSprintf(" -1024", "%#20i", -1024); - assertSprintf(" 1024", "%#20u", 1024); - assertSprintf(" 4294966272", "%#20u", 4294966272L); + assertEquals(" 1024", sprintf("%#20d", 1024)); + assertEquals(" -1024", sprintf("%#20d", -1024)); + assertEquals(" 1024", sprintf("%#20i", 1024)); + assertEquals(" -1024", sprintf("%#20i", -1024)); + assertEquals(" 1024", sprintf("%#20u", 1024)); + assertEquals(" 4294966272", sprintf("%#20u", 4294966272L)); // The following assertions were commented out in Printf4J; they match // C and gawk, and now pass. - assertSprintf(" 0777", "%#20o", 511); - assertSprintf(" 037777777001", "%#20o", 4294966785L); - assertSprintf(" 0x1234abcd", "%#20x", 305441741); - assertSprintf(" 0xedcb5433", "%#20x", 3989525555L); - assertSprintf(" 0X1234ABCD", "%#20X", 305441741); - assertSprintf(" 0XEDCB5433", "%#20X", 3989525555L); + assertEquals(" 0777", sprintf("%#20o", 511)); + assertEquals(" 037777777001", sprintf("%#20o", 4294966785L)); + assertEquals(" 0x1234abcd", sprintf("%#20x", 305441741)); + assertEquals(" 0xedcb5433", sprintf("%#20x", 3989525555L)); + assertEquals(" 0X1234ABCD", sprintf("%#20X", 305441741)); + assertEquals(" 0XEDCB5433", sprintf("%#20X", 3989525555L)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testPadding20Dot5() { - assertSprintf(" 01024", "%20.5d", 1024); - assertSprintf(" -01024", "%20.5d", -1024); - assertSprintf(" 01024", "%20.5i", 1024); - assertSprintf(" -01024", "%20.5i", -1024); - assertSprintf(" 01024", "%20.5u", 1024); - assertSprintf(" 4294966272", "%20.5u", 4294966272L); - assertSprintf(" 00777", "%20.5o", 511); - assertSprintf(" 37777777001", "%20.5o", 4294966785L); - assertSprintf(" 1234abcd", "%20.5x", 305441741); - assertSprintf(" 00edcb5433", "%20.10x", 3989525555L); - assertSprintf(" 1234ABCD", "%20.5X", 305441741); - assertSprintf(" 00EDCB5433", "%20.10X", 3989525555L); + assertEquals(" 01024", sprintf("%20.5d", 1024)); + assertEquals(" -01024", sprintf("%20.5d", -1024)); + assertEquals(" 01024", sprintf("%20.5i", 1024)); + assertEquals(" -01024", sprintf("%20.5i", -1024)); + assertEquals(" 01024", sprintf("%20.5u", 1024)); + assertEquals(" 4294966272", sprintf("%20.5u", 4294966272L)); + assertEquals(" 00777", sprintf("%20.5o", 511)); + assertEquals(" 37777777001", sprintf("%20.5o", 4294966785L)); + assertEquals(" 1234abcd", sprintf("%20.5x", 305441741)); + assertEquals(" 00edcb5433", sprintf("%20.10x", 3989525555L)); + assertEquals(" 1234ABCD", sprintf("%20.5X", 305441741)); + assertEquals(" 00EDCB5433", sprintf("%20.10X", 3989525555L)); } // Was @Disabled in Printf4J; matches C and gawk. @Test public void testPaddingNegativeNumbers() { // space padding - assertSprintf("-5", "% 1d", -5); - assertSprintf("-5", "% 2d", -5); - assertSprintf(" -5", "% 3d", -5); - assertSprintf(" -5", "% 4d", -5); + assertEquals("-5", sprintf("% 1d", -5)); + assertEquals("-5", sprintf("% 2d", -5)); + assertEquals(" -5", sprintf("% 3d", -5)); + assertEquals(" -5", sprintf("% 4d", -5)); // zero padding - assertSprintf("-5", "%01d", -5); - assertSprintf("-5", "%02d", -5); - assertSprintf("-05", "%03d", -5); - assertSprintf("-005", "%04d", -5); + assertEquals("-5", sprintf("%01d", -5)); + assertEquals("-5", sprintf("%02d", -5)); + assertEquals("-05", sprintf("%03d", -5)); + assertEquals("-005", sprintf("%04d", -5)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testPaddingNegativeFloat() { // space padding - assertSprintf("-5.0", "% 3.1f", -5.); - assertSprintf("-5.0", "% 4.1f", -5.); - assertSprintf(" -5.0", "% 5.1f", -5.); - assertSprintf(" -5", "% 6.1g", -5.); - assertSprintf("-5.0e+00", "% 6.1e", -5.); - assertSprintf(" -5.0e+00", "% 10.1e", -5.); + assertEquals("-5.0", sprintf("% 3.1f", -5.)); + assertEquals("-5.0", sprintf("% 4.1f", -5.)); + assertEquals(" -5.0", sprintf("% 5.1f", -5.)); + assertEquals(" -5", sprintf("% 6.1g", -5.)); + assertEquals("-5.0e+00", sprintf("% 6.1e", -5.)); + assertEquals(" -5.0e+00", sprintf("% 10.1e", -5.)); // zero padding - assertSprintf("-5.0", "%03.1f", -5.); - assertSprintf("-5.0", "%04.1f", -5.); - assertSprintf("-05.0", "%05.1f", -5.); + assertEquals("-5.0", sprintf("%03.1f", -5.)); + assertEquals("-5.0", sprintf("%04.1f", -5.)); + assertEquals("-05.0", sprintf("%05.1f", -5.)); // zero padding no decimal point - assertSprintf("-5", "%01.0f", -5.); - assertSprintf("-5", "%02.0f", -5.); - assertSprintf("-05", "%03.0f", -5.); - assertSprintf("-005.0e+00", "%010.1e", -5.); - assertSprintf("-05E+00", "%07.0E", -5.); - assertSprintf("-05", "%03.0g", -5.); + assertEquals("-5", sprintf("%01.0f", -5.)); + assertEquals("-5", sprintf("%02.0f", -5.)); + assertEquals("-05", sprintf("%03.0f", -5.)); + assertEquals("-005.0e+00", sprintf("%010.1e", -5.)); + assertEquals("-05E+00", sprintf("%07.0E", -5.)); + assertEquals("-05", sprintf("%03.0g", -5.)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testLength() { - assertSprintf("", "%.0s", "Hello testing"); - assertSprintf(" ", "%20.0s", "Hello testing"); - assertSprintf("", "%.s", "Hello testing"); - assertSprintf(" ", "%20.s", "Hello testing"); - assertSprintf(" 1024", "%20.0d", 1024); - assertSprintf(" -1024", "%20.0d", -1024); - assertSprintf(" ", "%20.d", 0); - assertSprintf(" 1024", "%20.0i", 1024); - assertSprintf(" -1024", "%20.i", -1024); - assertSprintf(" ", "%20.i", 0); - assertSprintf(" 1024", "%20.u", 1024); - assertSprintf(" 4294966272", "%20.0u", 4294966272L); - assertSprintf(" ", "%20.u", 0L); - assertSprintf(" 777", "%20.o", 511); - assertSprintf(" 37777777001", "%20.0o", 4294966785L); - assertSprintf(" ", "%20.o", 0L); - assertSprintf(" 1234abcd", "%20.x", 305441741); - assertSprintf(" 1234abcd", "%50.x", 305441741); - assertSprintf(" 1234abcd 12345", "%50.x%10.u", 305441741, 12345); - assertSprintf(" edcb5433", "%20.0x", 3989525555L); - assertSprintf(" ", "%20.x", 0L); - assertSprintf(" 1234ABCD", "%20.X", 305441741); - assertSprintf(" EDCB5433", "%20.0X", 3989525555L); - assertSprintf(" ", "%20.X", 0L); - assertSprintf(" ", "%02.0u", 0L); - assertSprintf(" ", "%02.0d", 0); + assertEquals("", sprintf("%.0s", "Hello testing")); + assertEquals(" ", sprintf("%20.0s", "Hello testing")); + assertEquals("", sprintf("%.s", "Hello testing")); + assertEquals(" ", sprintf("%20.s", "Hello testing")); + assertEquals(" 1024", sprintf("%20.0d", 1024)); + assertEquals(" -1024", sprintf("%20.0d", -1024)); + assertEquals(" ", sprintf("%20.d", 0)); + assertEquals(" 1024", sprintf("%20.0i", 1024)); + assertEquals(" -1024", sprintf("%20.i", -1024)); + assertEquals(" ", sprintf("%20.i", 0)); + assertEquals(" 1024", sprintf("%20.u", 1024)); + assertEquals(" 4294966272", sprintf("%20.0u", 4294966272L)); + assertEquals(" ", sprintf("%20.u", 0L)); + assertEquals(" 777", sprintf("%20.o", 511)); + assertEquals(" 37777777001", sprintf("%20.0o", 4294966785L)); + assertEquals(" ", sprintf("%20.o", 0L)); + assertEquals(" 1234abcd", sprintf("%20.x", 305441741)); + assertEquals(" 1234abcd", sprintf("%50.x", 305441741)); + assertEquals( + " 1234abcd 12345", + sprintf("%50.x%10.u", 305441741, 12345)); + assertEquals(" edcb5433", sprintf("%20.0x", 3989525555L)); + assertEquals(" ", sprintf("%20.x", 0L)); + assertEquals(" 1234ABCD", sprintf("%20.X", 305441741)); + assertEquals(" EDCB5433", sprintf("%20.0X", 3989525555L)); + assertEquals(" ", sprintf("%20.X", 0L)); + assertEquals(" ", sprintf("%02.0u", 0L)); + assertEquals(" ", sprintf("%02.0d", 0)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testFloat() { // test special-case floats - assertSprintf(" nan", "%8f", Float.NaN); - assertSprintf(" inf", "%8f", Float.POSITIVE_INFINITY); - assertSprintf("-inf ", "%-8f", Float.NEGATIVE_INFINITY); - assertSprintf(" +inf", "%+8e", Float.POSITIVE_INFINITY); - assertSprintf("3.1415", "%.4f", 3.1415354); - assertSprintf("30343.142", "%.3f", 30343.1415354); - assertSprintf("34", "%.0f", 34.1415354); - assertSprintf("1", "%.0f", 1.3); - assertSprintf("2", "%.0f", 1.55); - assertSprintf("1.6", "%.1f", 1.64); - assertSprintf("42.90", "%.2f", 42.8952); - assertSprintf("42.895200000", "%.9f", 42.8952); - assertSprintf("42.8952230000", "%.10f", 42.895223); + assertEquals(" nan", sprintf("%8f", Float.NaN)); + assertEquals(" inf", sprintf("%8f", Float.POSITIVE_INFINITY)); + assertEquals("-inf ", sprintf("%-8f", Float.NEGATIVE_INFINITY)); + assertEquals(" +inf", sprintf("%+8e", Float.POSITIVE_INFINITY)); + assertEquals("3.1415", sprintf("%.4f", 3.1415354)); + assertEquals("30343.142", sprintf("%.3f", 30343.1415354)); + assertEquals("34", sprintf("%.0f", 34.1415354)); + assertEquals("1", sprintf("%.0f", 1.3)); + assertEquals("2", sprintf("%.0f", 1.55)); + assertEquals("1.6", sprintf("%.1f", 1.64)); + assertEquals("42.90", sprintf("%.2f", 42.8952)); + assertEquals("42.895200000", sprintf("%.9f", 42.8952)); + assertEquals("42.8952230000", sprintf("%.10f", 42.895223)); // Printf4J expected "42.895223123000" and "42.895223877000" here // because its reference implementation truncated to 9 significant // fraction digits; gawk prints the correctly rounded values. - assertSprintf("42.895223123457", "%.12f", 42.89522312345678); - assertSprintf("42.895223876543", "%.12f", 42.89522387654321); - assertSprintf(" 42.90", "%6.2f", 42.8952); - assertSprintf("+42.90", "%+6.2f", 42.8952); - assertSprintf("+42.9", "%+5.1f", 42.9252); - assertSprintf("42.500000", "%f", 42.5); - assertSprintf("42.5", "%.1f", 42.5); - assertSprintf("42167.000000", "%f", 42167.0); - assertSprintf("-12345.987654321", "%.9f", -12345.987654321); - assertSprintf("4.0", "%.1f", 3.999); - assertSprintf("4", "%.0f", 3.5); - assertSprintf("4", "%.0f", 4.5); - assertSprintf("3", "%.0f", 3.49); - assertSprintf("3.5", "%.1f", 3.49); - assertSprintf("a0.5 ", "a%-5.1f", 0.5); - assertSprintf("a0.5 end", "a%-5.1fend", 0.5); - assertSprintf("12345.7", "%G", 12345.678); - assertSprintf("12345.68", "%.7G", 12345.678); - assertSprintf("1.2346E+08", "%.5G", 123456789.); + assertEquals("42.895223123457", sprintf("%.12f", 42.89522312345678)); + assertEquals("42.895223876543", sprintf("%.12f", 42.89522387654321)); + assertEquals(" 42.90", sprintf("%6.2f", 42.8952)); + assertEquals("+42.90", sprintf("%+6.2f", 42.8952)); + assertEquals("+42.9", sprintf("%+5.1f", 42.9252)); + assertEquals("42.500000", sprintf("%f", 42.5)); + assertEquals("42.5", sprintf("%.1f", 42.5)); + assertEquals("42167.000000", sprintf("%f", 42167.0)); + assertEquals("-12345.987654321", sprintf("%.9f", -12345.987654321)); + assertEquals("4.0", sprintf("%.1f", 3.999)); + assertEquals("4", sprintf("%.0f", 3.5)); + assertEquals("4", sprintf("%.0f", 4.5)); + assertEquals("3", sprintf("%.0f", 3.49)); + assertEquals("3.5", sprintf("%.1f", 3.49)); + assertEquals("a0.5 ", sprintf("a%-5.1f", 0.5)); + assertEquals("a0.5 end", sprintf("a%-5.1fend", 0.5)); + assertEquals("12345.7", sprintf("%G", 12345.678)); + assertEquals("12345.68", sprintf("%.7G", 12345.678)); + assertEquals("1.2346E+08", sprintf("%.5G", 123456789.)); // Printf4J expected "12345.0": AWK's %G removes trailing zeros. - assertSprintf("12345", "%.6G", 12345.); - assertSprintf(" +1.235e+08", "%+12.4g", 123456789.); - assertSprintf("0.0012", "%.2G", 0.001234); - assertSprintf(" +0.001234", "%+10.4G", 0.001234); - assertSprintf("+001.234e-05", "%+012.4g", 0.00001234); - assertSprintf("-1.23e-308", "%.3g", -1.2345e-308); - assertSprintf("+1.230E+308", "%+.3E", 1.23e+308); + assertEquals("12345", sprintf("%.6G", 12345.)); + assertEquals(" +1.235e+08", sprintf("%+12.4g", 123456789.)); + assertEquals("0.0012", sprintf("%.2G", 0.001234)); + assertEquals(" +0.001234", sprintf("%+10.4G", 0.001234)); + assertEquals("+001.234e-05", sprintf("%+012.4g", 0.00001234)); + assertEquals("-1.23e-308", sprintf("%.3g", -1.2345e-308)); + assertEquals("+1.230E+308", sprintf("%+.3E", 1.23e+308)); // Printf4J expected "1.0e+20" (its reference implementation switched // to exponential notation out of range); gawk prints the full value. - assertSprintf("100000000000000000000.0", "%.1f", 1E20); + assertEquals("100000000000000000000.0", sprintf("%.1f", 1E20)); } // Was @Disabled in Printf4J; expected values verified against gawk 5, @@ -509,130 +507,130 @@ public void testFloat() { // prints any other modifier combination verbatim. @Test public void testTypes() { - assertSprintf("0", "%i", 0); - assertSprintf("1234", "%i", 1234); - assertSprintf("32767", "%i", 32767); - assertSprintf("-32767", "%i", -32767); - assertSprintf("30", "%li", 30L); - assertSprintf("-2147483647", "%li", -2147483647L); - assertSprintf("2147483647", "%li", 2147483647L); + assertEquals("0", sprintf("%i", 0)); + assertEquals("1234", sprintf("%i", 1234)); + assertEquals("32767", sprintf("%i", 32767)); + assertEquals("-32767", sprintf("%i", -32767)); + assertEquals("30", sprintf("%li", 30L)); + assertEquals("-2147483647", sprintf("%li", -2147483647L)); + assertEquals("2147483647", sprintf("%li", 2147483647L)); // Doubled modifiers ("ll", "hh") and the "q" modifier are not valid // in gawk: the specifier prints verbatim and consumes no argument. - assertSprintf("%lli", "%lli", 30L); - assertSprintf("%lli", "%lli", -9223372036854775807L); - assertSprintf("%lli", "%lli", 9223372036854775807L); - assertSprintf("100000", "%lu", 100000L); - assertSprintf("4294967295", "%lu", 0xFFFFFFFFL); - assertSprintf("%llu", "%llu", 281474976710656L); - assertSprintf("%llu", "%llu", Long.parseUnsignedLong("18446744073709551615")); + assertEquals("%lli", sprintf("%lli", 30L)); + assertEquals("%lli", sprintf("%lli", -9223372036854775807L)); + assertEquals("%lli", sprintf("%lli", 9223372036854775807L)); + assertEquals("100000", sprintf("%lu", 100000L)); + assertEquals("4294967295", sprintf("%lu", 0xFFFFFFFFL)); + assertEquals("%llu", sprintf("%llu", 281474976710656L)); + assertEquals("%llu", sprintf("%llu", Long.parseUnsignedLong("18446744073709551615"))); // Single j, z, and t modifiers are accepted and ignored, like h, l, // and L (gawk 5.2+). - assertSprintf("2147483647", "%zu", 2147483647L); - assertSprintf("2147483647", "%zd", 2147483647L); - assertSprintf("-2147483647", "%zi", -2147483647L); - assertSprintf("5", "%jd", 5); - assertSprintf("6", "%td", 6); + assertEquals("2147483647", sprintf("%zu", 2147483647L)); + assertEquals("2147483647", sprintf("%zd", 2147483647L)); + assertEquals("-2147483647", sprintf("%zi", -2147483647L)); + assertEquals("5", sprintf("%jd", 5)); + assertEquals("6", sprintf("%td", 6)); // Distinct modifiers may stack; only repeats are invalid. - assertSprintf("42", "%lhd", 42); + assertEquals("42", sprintf("%lhd", 42)); // %b is not an AWK conversion: printed verbatim, like gawk. - assertSprintf("%b", "%b", 60000); - assertSprintf("%lb", "%lb", 12345678L); - assertSprintf("165140", "%o", 60000); - assertSprintf("57060516", "%lo", 12345678L); - assertSprintf("12345678", "%lx", 0x12345678L); - assertSprintf("%llx", "%llx", 0x1234567891234567L); - assertSprintf("abcdefab", "%lx", 0xabcdefabL); - assertSprintf("ABCDEFAB", "%lX", 0xabcdefabL); - assertSprintf("v", "%c", 'v'); - assertSprintf("wv", "%cv", 'w'); - assertSprintf("A Test", "%s", "A Test"); + assertEquals("%b", sprintf("%b", 60000)); + assertEquals("%lb", sprintf("%lb", 12345678L)); + assertEquals("165140", sprintf("%o", 60000)); + assertEquals("57060516", sprintf("%lo", 12345678L)); + assertEquals("12345678", sprintf("%lx", 0x12345678L)); + assertEquals("%llx", sprintf("%llx", 0x1234567891234567L)); + assertEquals("abcdefab", sprintf("%lx", 0xabcdefabL)); + assertEquals("ABCDEFAB", sprintf("%lX", 0xabcdefabL)); + assertEquals("v", sprintf("%c", 'v')); + assertEquals("wv", sprintf("%cv", 'w')); + assertEquals("A Test", sprintf("%s", "A Test")); // gawk ignores the single 'h' modifier without truncating the value, // and prints the invalid "hh" specifiers verbatim. - assertSprintf("%hhu", "%hhu", 0xFFFFL); - assertSprintf("13398", "%hu", 13398); - assertSprintf("1193046", "%hu", 0x123456L); - assertSprintf("Test%hhi 10000", "%s%hhi %hu", "Test", 10000, 0xFFFFFFFFL); + assertEquals("%hhu", sprintf("%hhu", 0xFFFFL)); + assertEquals("13398", sprintf("%hu", 13398)); + assertEquals("1193046", sprintf("%hu", 0x123456L)); + assertEquals("Test%hhi 10000", sprintf("%s%hhi %hu", "Test", 10000, 0xFFFFFFFFL)); } // Was @Disabled in Printf4J, which expected "kmarco": gawk prints the // unknown "%k" specifier verbatim. @Test public void testUnknown() { - assertSprintf("%kmarco", "%kmarco", 42, 37); + assertEquals("%kmarco", sprintf("%kmarco", 42, 37)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testStringLength() { - assertSprintf("This", "%.4s", "This is a test"); - assertSprintf("test", "%.4s", "test"); - assertSprintf("123", "%.7s", "123"); - assertSprintf("", "%.7s", ""); - assertSprintf("1234ab", "%.4s%.2s", "123456", "abcdef"); + assertEquals("This", sprintf("%.4s", "This is a test")); + assertEquals("test", sprintf("%.4s", "test")); + assertEquals("123", sprintf("%.7s", "123")); + assertEquals("", sprintf("%.7s", "")); + assertEquals("1234ab", sprintf("%.4s%.2s", "123456", "abcdef")); // Printf4J expected ".2s": gawk prints the whole invalid specifier // verbatim. - assertSprintf("%.4.2s", "%.4.2s", "123456"); - assertSprintf("123", "%.*s", 3, "123456"); + assertEquals("%.4.2s", sprintf("%.4.2s", "123456")); + assertEquals("123", sprintf("%.*s", 3, "123456")); // The precision counts characters, so it never splits a surrogate // pair, like gawk in a multibyte locale. - assertSprintf("😀", "%.1s", "😀x"); - assertSprintf("😀x", "%.2s", "😀x"); + assertEquals("😀", sprintf("%.1s", "😀x")); + assertEquals("😀x", sprintf("%.2s", "😀x")); // The field width also counts characters: a supplementary character // fills one column (gawk pads %s the same way; its %c padding counts // bytes, a C-locale artifact that Jawk does not reproduce). - assertSprintf(" 😀", "%3s", "😀"); - assertSprintf("😀 ", "%-3s", "😀"); - assertSprintf(" 😀", "%3c", 0x1F600); + assertEquals(" 😀", sprintf("%3s", "😀")); + assertEquals("😀 ", sprintf("%-3s", "😀")); + assertEquals(" 😀", sprintf("%3c", 0x1F600)); } // Was @Disabled in Printf4J; expected values verified against gawk 5. @Test public void testMisc() { - assertSprintf("53000atest-20 bit", "%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit"); - assertSprintf("0.33", "%.*f", 2, 0.33333333); - assertSprintf("1", "%.*d", -1, 1); - assertSprintf("foo", "%.3s", "foobar"); + assertEquals("53000atest-20 bit", sprintf("%u%u%ctest%d %s", 5, 3000, 'a', -20, "bit")); + assertEquals("0.33", sprintf("%.*f", 2, 0.33333333)); + assertEquals("1", sprintf("%.*d", -1, 1)); + assertEquals("foo", sprintf("%.3s", "foobar")); // Printf4J expected " " (glibc behavior): gawk prints nothing at all // for a zero value with zero precision, even with the space flag. - assertSprintf("", "% .0d", 0); - assertSprintf(" 00004", "%10.5d", 4); - assertSprintf("hi x", "%*sx", -3, "hi"); - assertSprintf("0.33", "%.*g", 2, 0.33333333); - assertSprintf("3.33e-01", "%.*e", 2, 0.33333333); + assertEquals("", sprintf("% .0d", 0)); + assertEquals(" 00004", sprintf("%10.5d", 4)); + assertEquals("hi x", sprintf("%*sx", -3, "hi")); + assertEquals("0.33", sprintf("%.*g", 2, 0.33333333)); + assertEquals("3.33e-01", sprintf("%.*e", 2, 0.33333333)); } @Test public void testChar() { - assertSprintf("A", "%c", 65); - assertSprintf("A", "%c", 65L); - assertSprintf("A", "%c", 65.0); - assertSprintf("A", "%c", 65.1); - assertSprintf("A", "%c", Integer.valueOf(65)); - assertSprintf("A", "%c", Long.valueOf(65)); - assertSprintf("A", "%c", Float.valueOf(65)); - assertSprintf("A", "%c", Double.valueOf(65)); - assertSprintf("6", "%c", "65"); + assertEquals("A", sprintf("%c", 65)); + assertEquals("A", sprintf("%c", 65L)); + assertEquals("A", sprintf("%c", 65.0)); + assertEquals("A", sprintf("%c", 65.1)); + assertEquals("A", sprintf("%c", Integer.valueOf(65))); + assertEquals("A", sprintf("%c", Long.valueOf(65))); + assertEquals("A", sprintf("%c", Float.valueOf(65))); + assertEquals("A", sprintf("%c", Double.valueOf(65))); + assertEquals("6", sprintf("%c", "65")); Object nothing = null; - assertSprintf("\0", "%c", nothing); + assertEquals("\0", sprintf("%c", nothing)); } // Ported from Printf4J's testToChar; AwkPrintf converts values for %c // internally, so the equivalent assertions go through sprintf(). @Test public void testToChar() { - assertSprintf("A", "%c", 65); - assertSprintf("A", "%c", 65L); - assertSprintf("A", "%c", 65.0); - assertSprintf("A", "%c", 65.1); - assertSprintf("A", "%c", 65.9); - assertSprintf("A", "%c", Integer.valueOf(65)); - assertSprintf("A", "%c", Long.valueOf(65)); - assertSprintf("A", "%c", Float.valueOf(65)); - assertSprintf("A", "%c", Double.valueOf(65)); - assertSprintf("6", "%c", "65"); - assertSprintf("\0", "%c", ""); + assertEquals("A", sprintf("%c", 65)); + assertEquals("A", sprintf("%c", 65L)); + assertEquals("A", sprintf("%c", 65.0)); + assertEquals("A", sprintf("%c", 65.1)); + assertEquals("A", sprintf("%c", 65.9)); + assertEquals("A", sprintf("%c", Integer.valueOf(65))); + assertEquals("A", sprintf("%c", Long.valueOf(65))); + assertEquals("A", sprintf("%c", Float.valueOf(65))); + assertEquals("A", sprintf("%c", Double.valueOf(65))); + assertEquals("6", sprintf("%c", "65")); + assertEquals("\0", sprintf("%c", "")); Object nothing = null; - assertSprintf("\0", "%c", nothing); + assertEquals("\0", sprintf("%c", nothing)); } // Ported from Printf4J's testToLong: the same conversion now lives in @@ -688,85 +686,85 @@ public void testToDouble() { public void testStringConversionUsesAwkNumberToStringRules() { // The symptom from issue #528: an integral double prints without a // fractional part. - assertSprintf("1", "%s", 1.0); - assertSprintf("x[1]", "x[%s]", 1.0); + assertEquals("1", sprintf("%s", 1.0)); + assertEquals("x[1]", sprintf("x[%s]", 1.0)); // Non-integral values use CONVFMT. - assertSprintf("3.14159", "%s", 3.14159265); - assertSprintf("3.1", Locale.US, "%.2g", "%s", 3.14159265); + assertEquals("3.14159", sprintf("%s", 3.14159265)); + assertEquals("3.1", sprintf(Locale.US, "%.2g", "%s", 3.14159265)); // CONVFMT that is not a %g-style format is honored verbatim. - assertSprintf("3.14", Locale.US, "%.2f", "%s", 3.14159265); + assertEquals("3.14", sprintf(Locale.US, "%.2f", "%s", 3.14159265)); // An explicitly empty CONVFMT converts non-integral numbers to the // empty string, like gawk; integral values still print as integers. - assertSprintf("", Locale.US, "", "%s", 1.5); - assertSprintf("1", Locale.US, "", "%s", 1.0); + assertEquals("", sprintf(Locale.US, "", "%s", 1.5)); + assertEquals("1", sprintf(Locale.US, "", "%s", 1.0)); // Integral values beyond the 64-bit range print in full. - assertSprintf("100000000000000000000", "%s", 1e20); + assertEquals("100000000000000000000", sprintf("%s", 1e20)); // Exact long values are preserved. - assertSprintf("9223372036854775807", "%s", Long.MAX_VALUE); + assertEquals("9223372036854775807", sprintf("%s", Long.MAX_VALUE)); } @Test public void testCharConversion() { // A numeric value selects the corresponding code point. - assertSprintf("é", "%c", 233); + assertEquals("é", sprintf("%c", 233)); // A code point beyond the BMP produces the full character. - assertSprintf(new String(Character.toChars(0x1F600)), "%c", 0x1F600); + assertEquals(new String(Character.toChars(0x1F600)), sprintf("%c", 0x1F600)); // A string value uses its first character. - assertSprintf("X", "%c", "XYZ"); + assertEquals("X", sprintf("%c", "XYZ")); // Width applies to %c like any other conversion. - assertSprintf(" A", "%5c", 65); - assertSprintf("A ", "%-5c", 65); + assertEquals(" A", sprintf("%5c", 65)); + assertEquals("A ", sprintf("%-5c", 65)); } @Test public void testDynamicWidthAndPrecision() { - assertSprintf(" 3.14", "%*.*f", 8, 2, 3.14159); - assertSprintf(" 3.14159", "%9s", 3.14159); + assertEquals(" 3.14", sprintf("%*.*f", 8, 2, 3.14159)); + assertEquals(" 3.14159", sprintf("%9s", 3.14159)); // A negative dynamic width means left justification. - assertSprintf("42 ", "%*d", -6, 42); + assertEquals("42 ", sprintf("%*d", -6, 42)); // Width and precision arguments are converted like AWK numbers. - assertSprintf(" 3.14", "%*.*f", "6", "2", 3.14159); + assertEquals(" 3.14", sprintf("%*.*f", "6", "2", 3.14159)); } @Test public void testPositionalSpecifiers() { - assertSprintf("b a", "%2$s %1$s", "a", "b"); - assertSprintf("a b a", "%1$s %2$s %1$s", "a", "b"); + assertEquals("b a", sprintf("%2$s %1$s", "a", "b")); + assertEquals("a b a", sprintf("%1$s %2$s %1$s", "a", "b")); // Mixing positional and sequential specifiers is fatal, like gawk. - assertSprintfThrows(AwkRuntimeException.class, "%2$s %s", "a", "b"); + assertThrows(AwkRuntimeException.class, () -> sprintf("%2$s %s", "a", "b")); // A zero positional index is fatal, like gawk. - assertSprintfThrows(AwkRuntimeException.class, "%0$s", "a"); + assertThrows(AwkRuntimeException.class, () -> sprintf("%0$s", "a")); // gawk-verified: an explicitly positioned star operand may accompany // sequential conversions. - assertSprintf(" a|5", "%*2$s|%s", "a", 5); - assertSprintf("a 5", "%1$s %2$*3$d", "a", 5, 6); + assertEquals(" a|5", sprintf("%*2$s|%s", "a", 5)); + assertEquals("a 5", sprintf("%1$s %2$*3$d", "a", 5, 6)); // gawk-verified: a sequential star operand with a positional // conversion is a mixed-mode fatal error... - assertSprintfThrows(AwkRuntimeException.class, "%2$*d", 5, 12); + assertThrows(AwkRuntimeException.class, () -> sprintf("%2$*d", 5, 12)); // ...and an explicitly positioned unknown specifier pins the format // to positional mode even though it prints verbatim. - assertSprintfThrows(AwkRuntimeException.class, "%2$q|%d", 5, 12); + assertThrows(AwkRuntimeException.class, () -> sprintf("%2$q|%d", 5, 12)); } @Test public void testOutOfRangeFallbackKeepsSignFlagsAndPrecision() { // gawk-verified: the %g fallback for out-of-range %u/%o/%x/%X keeps // the sign, the precision, and the zero and '#' flags. - assertSprintf("-1.26765e+30", "%u", -Math.pow(2, 100)); - assertSprintf("1.2676506e+30", "%.10x", Math.pow(2, 100)); - assertSprintf("1.27e+30", "%#.3x", Math.pow(2, 100)); - assertSprintf("0000000001.26765e+30", "%020u", Math.pow(2, 100)); + assertEquals("-1.26765e+30", sprintf("%u", -Math.pow(2, 100))); + assertEquals("1.2676506e+30", sprintf("%.10x", Math.pow(2, 100))); + assertEquals("1.27e+30", sprintf("%#.3x", Math.pow(2, 100))); + assertEquals("0000000001.26765e+30", sprintf("%020u", Math.pow(2, 100))); } @Test public void testAlternateFormKeepsDecimalPoint() { // gawk-verified: '#' forces a decimal point even when no fractional // digits remain. - assertSprintf("1.", "%#.1g", 1); - assertSprintf("1.e+04", "%#.1g", 12345); - assertSprintf("1.2e+04", "%#.2g", 12345); - assertSprintf("1.e+04", "%#.0e", 12345); - assertSprintf("1.00000", "%#g", 1); + assertEquals("1.", sprintf("%#.1g", 1)); + assertEquals("1.e+04", sprintf("%#.1g", 12345)); + assertEquals("1.2e+04", sprintf("%#.2g", 12345)); + assertEquals("1.e+04", sprintf("%#.0e", 12345)); + assertEquals("1.00000", sprintf("%#g", 1)); } @Test @@ -774,121 +772,120 @@ public void testZeroPrecisionZeroValue() { // gawk-verified: unsigned conversions print "0" when a nonzero value // truncates to zero, or when the '#' flag is given; signed %d prints // nothing in both zero cases. - assertSprintf("0", "%.0x", 0.1); - assertSprintf("0", "%.0u", 0.1); - assertSprintf("0", "%.0o", 0.1); - assertSprintf("", "%.0d", 0.1); - assertSprintf("0", "%#.0u", 0); - assertSprintf("", "%.0u", 0); - assertSprintf("", "%.0x", 0); + assertEquals("0", sprintf("%.0x", 0.1)); + assertEquals("0", sprintf("%.0u", 0.1)); + assertEquals("0", sprintf("%.0o", 0.1)); + assertEquals("", sprintf("%.0d", 0.1)); + assertEquals("0", sprintf("%#.0u", 0)); + assertEquals("", sprintf("%.0u", 0)); + assertEquals("", sprintf("%.0x", 0)); } @Test public void testIntegerTruncationAndConversion() { // %d truncates toward zero. - assertSprintf("42", "%d", 42.7); - assertSprintf("-42", "%d", -42.7); + assertEquals("42", sprintf("%d", 42.7)); + assertEquals("-42", sprintf("%d", -42.7)); // Strings convert with AWK's number rules (leading/trailing spaces, // exponent notation, numeric prefixes). - assertSprintf("1000", "%d", "1e3"); - assertSprintf("42", "%d", " 42 "); - assertSprintf("3", "%d", "+3.9"); - assertSprintf("0", "%d", "abc"); - assertSprintf("0", "%x", "abc"); + assertEquals("1000", sprintf("%d", "1e3")); + assertEquals("42", sprintf("%d", " 42 ")); + assertEquals("3", sprintf("%d", "+3.9")); + assertEquals("0", sprintf("%d", "abc")); + assertEquals("0", sprintf("%x", "abc")); } @Test public void testOutOfRangeIntegerConversions() { // Negative values wrap to unsigned 64-bit for %u, %o, %x. - assertSprintf("18446744073709551615", "%u", -1); - assertSprintf("ffffffffffffffff", "%x", -1); - assertSprintf("1777777777777777777777", "%o", -1); + assertEquals("18446744073709551615", sprintf("%u", -1)); + assertEquals("ffffffffffffffff", sprintf("%x", -1)); + assertEquals("1777777777777777777777", sprintf("%o", -1)); // 2^63 is out of the signed range but fits unsigned. - assertSprintf("9223372036854775808", "%d", 9.223372036854775808e18); + assertEquals("9223372036854775808", sprintf("%d", 9.223372036854775808e18)); // %d beyond 64 bits prints the full decimal expansion, like gawk. - assertSprintf("1267650600228229401496703205376", "%d", Math.pow(2, 100)); - assertSprintf("-1267650600228229401496703205376", "%d", -Math.pow(2, 100)); + assertEquals("1267650600228229401496703205376", sprintf("%d", Math.pow(2, 100))); + assertEquals("-1267650600228229401496703205376", sprintf("%d", -Math.pow(2, 100))); // %u, %o, and %x beyond 64 bits fall back to %g notation, like gawk. - assertSprintf("1.26765e+30", "%x", Math.pow(2, 100)); - assertSprintf("1.26765e+30", "%u", Math.pow(2, 100)); - assertSprintf("1.26765e+30", "%o", Math.pow(2, 100)); + assertEquals("1.26765e+30", sprintf("%x", Math.pow(2, 100))); + assertEquals("1.26765e+30", sprintf("%u", Math.pow(2, 100))); + assertEquals("1.26765e+30", sprintf("%o", Math.pow(2, 100))); } @Test public void testHexFloat() { // %a uses Java's hexadecimal float notation (gawk documents %a as // C-library dependent); the 0x prefix stays ahead of zero padding. - assertSprintf("0x1.34ap10", "%a", 1234.5); - assertSprintf("0x00000000001.34ap10", "%020a", 1234.5); - assertSprintf("-0x1.34ap10", "%a", -1234.5); + assertEquals("0x1.34ap10", sprintf("%a", 1234.5)); + assertEquals("0x00000000001.34ap10", sprintf("%020a", 1234.5)); + assertEquals("-0x1.34ap10", sprintf("%a", -1234.5)); } @Test public void testNonFiniteValues() { - assertSprintf("nan", "%d", Double.NaN); - assertSprintf("inf", "%d", Double.POSITIVE_INFINITY); - assertSprintf("-inf", "%f", Double.NEGATIVE_INFINITY); - assertSprintf("INF", "%E", Double.POSITIVE_INFINITY); - assertSprintf("NAN", "%G", Double.NaN); - assertSprintf("nan", "%s", Double.NaN); - assertSprintf("inf", "%s", Double.POSITIVE_INFINITY); - assertSprintf("-inf", "%s", Double.NEGATIVE_INFINITY); + assertEquals("nan", sprintf("%d", Double.NaN)); + assertEquals("inf", sprintf("%d", Double.POSITIVE_INFINITY)); + assertEquals("-inf", sprintf("%f", Double.NEGATIVE_INFINITY)); + assertEquals("INF", sprintf("%E", Double.POSITIVE_INFINITY)); + assertEquals("NAN", sprintf("%G", Double.NaN)); + assertEquals("nan", sprintf("%s", Double.NaN)); + assertEquals("inf", sprintf("%s", Double.POSITIVE_INFINITY)); + assertEquals("-inf", sprintf("%s", Double.NEGATIVE_INFINITY)); } @Test public void testUnknownSpecifiersDoNotConsumeArguments() { // The unknown %q prints verbatim and its argument feeds %d instead. - assertSprintf("%q1", "%q%d", 1, 2); + assertEquals("%q1", sprintf("%q%d", 1, 2)); // %n is not an AWK conversion (Printf4J used to print a newline). - assertSprintf("a%nb", "a%nb"); + assertEquals("a%nb", sprintf("a%nb")); // A dangling % prints verbatim. - assertSprintf("abc%", "abc%"); + assertEquals("abc%", sprintf("abc%")); } @Test public void testNotEnoughArgumentsIsFatal() { - assertSprintfThrows(AwkRuntimeException.class, "%d %s", 1); - assertSprintfThrows(AwkRuntimeException.class, "%5s"); - assertSprintfThrows(AwkRuntimeException.class, "%*d", 5); + assertThrows(AwkRuntimeException.class, () -> sprintf("%d %s", 1)); + assertThrows(AwkRuntimeException.class, () -> sprintf("%5s")); + assertThrows(AwkRuntimeException.class, () -> sprintf("%*d", 5)); } @Test public void testExtraArgumentsAreIgnored() { - assertSprintf("a b", "%s %s", "a", "b", "c"); + assertEquals("a b", sprintf("%s %s", "a", "b", "c")); } @Test public void testGroupingFlag() { - assertSprintf("1,234,567", "%'d", 1234567); - assertSprintf("1,234,567", "%'u", 1234567); - assertSprintf("1,234,567.89", "%'.2f", 1234567.891); - assertSprintf("1.234.567", Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567); + assertEquals("1,234,567", sprintf("%'d", 1234567)); + assertEquals("1,234,567", sprintf("%'u", 1234567)); + assertEquals("1,234,567.89", sprintf("%'.2f", 1234567.891)); + assertEquals("1.234.567", sprintf(Locale.GERMANY, AwkPrintf.DEFAULT_CONVFMT, "%'d", 1234567)); // gawk-verified: %g groups in fixed notation only, and octal and // hexadecimal output is never grouped. - assertSprintf("12,345", "%'g", 12345); - assertSprintf("1,234,567.25", "%'.10g", 1234567.25); - assertSprintf("1.234567e+06", "%'e", 1234567); - assertSprintf("2540be400", "%'x", 10000000000L); - assertSprintf("1747", "%'o", 999); + assertEquals("12,345", sprintf("%'g", 12345)); + assertEquals("1,234,567.25", sprintf("%'.10g", 1234567.25)); + assertEquals("1.234567e+06", sprintf("%'e", 1234567)); + assertEquals("2540be400", sprintf("%'x", 10000000000L)); + assertEquals("1747", sprintf("%'o", 999)); } @Test public void testLocaleDecimalSeparator() { - assertSprintf("3,14", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159); - assertSprintf("3,14159", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159); + assertEquals("3,14", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%.2f", 3.14159)); + assertEquals("3,14159", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%g", 3.14159)); // The '#' decimal point follows the locale as well. - assertSprintf("1,", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0f", 1); - assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0e", 12345); - assertSprintf("1,e+04", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.1g", 12345); - assertSprintf("0,00000", Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#g", 0); + assertEquals("1,", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0f", 1)); + assertEquals("1,e+04", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.0e", 12345)); + assertEquals("1,e+04", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#.1g", 12345)); + assertEquals("0,00000", sprintf(Locale.FRANCE, AwkPrintf.DEFAULT_CONVFMT, "%#g", 0)); } @Test - public void testLegacySprintfOverrideIsPreserved() { - // A sink that overrides the historical sprintf(String, Object...) - // customization point keeps working when the runtime routes through - // sprintfWithConvFmt. - AwkSink legacySink = new AwkSink() { + public void testAwkSinkSprintf() { + // The sink-level sprintf converts %s operands with the supplied + // CONVFMT, and overriding it customizes formatting. + AwkSink plainSink = new AwkSink() { @Override public void print(String ofs, String ors, String ofmt, Object... values) { @@ -896,20 +893,13 @@ public void print(String ofs, String ors, String ofmt, Object... values) { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { // not needed for this test } - - @Override - public String sprintf(String format, Object... values) { - return "custom:" + format; - } }; - assertEquals("custom:%s", legacySink.sprintfWithConvFmt("%.2g", "%s", 1.5)); + assertEquals("3.1", plainSink.sprintf("%.2g", "%s", 3.14159265)); - // A legacy override that decorates super.sprintf(...) must reach the - // base formatter without being redispatched into itself. - AwkSink decoratingSink = new AwkSink() { + AwkSink customSink = new AwkSink() { @Override public void print(String ofs, String ors, String ofmt, Object... values) { @@ -917,44 +907,29 @@ public void print(String ofs, String ors, String ofmt, Object... values) { } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { - // not needed for this test - } - - @Override - public String sprintf(String format, Object... values) { - return "[" + super.sprintf(format, values) + "]"; - } - }; - assertEquals("[1.5]", decoratingSink.sprintfWithConvFmt("%.2g", "%s", 1.5)); - - // A sink without the legacy override uses the CONVFMT-aware engine. - AwkSink plainSink = new AwkSink() { - - @Override - public void print(String ofs, String ors, String ofmt, Object... values) { + public void printf(String ofs, String ors, String ofmt, String convfmt, String format, Object... values) { // not needed for this test } @Override - public void printf(String ofs, String ors, String ofmt, String format, Object... values) { - // not needed for this test + public String sprintf(String convfmt, String format, Object... values) { + return "[" + super.sprintf(convfmt, format, values) + "]"; } }; - assertEquals("3.1", plainSink.sprintfWithConvFmt("%.2g", "%s", 3.14159265)); + assertEquals("[3.1]", customSink.sprintf("%.2g", "%s", 3.14159265)); } @Test public void testToAwkString() { - assertToAwkString("", null); - assertToAwkString("text", "text"); - assertToAwkString("1", 1.0); - assertToAwkString("0.1", 0.1); - assertToAwkString("3.14159", 3.14159265); - assertToAwkString("100000000000000000000", 1e20); - assertToAwkString("9223372036854775807", Long.MAX_VALUE); - assertToAwkString("nan", Double.NaN); - assertToAwkString("inf", Double.POSITIVE_INFINITY); - assertToAwkString("-inf", Double.NEGATIVE_INFINITY); + assertEquals("", AwkPrintf.toAwkString(null, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("text", AwkPrintf.toAwkString("text", AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("1", AwkPrintf.toAwkString(1.0, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("0.1", AwkPrintf.toAwkString(0.1, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("3.14159", AwkPrintf.toAwkString(3.14159265, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("100000000000000000000", AwkPrintf.toAwkString(1e20, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("9223372036854775807", AwkPrintf.toAwkString(Long.MAX_VALUE, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("nan", AwkPrintf.toAwkString(Double.NaN, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("inf", AwkPrintf.toAwkString(Double.POSITIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); + assertEquals("-inf", AwkPrintf.toAwkString(Double.NEGATIVE_INFINITY, AwkPrintf.DEFAULT_CONVFMT, Locale.US)); } } From c174b09c1e0cccad617388af5b1d2be1ee6138cf Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 13 Aug 2026 13:21:22 +0200 Subject: [PATCH 16/18] Simplify printf plumbing and add JavaStringFormatAwkSink Apply maintainer feedback: - Positional printf arguments (%2$s) are now allowed in strict POSIX mode too: a user writing that syntax wants positional arguments, and refusing them buys nothing. The AVM checkPosixFormat() gate and AwkPrintf.usesPositionalArguments() scanner are removed. - New JavaStringFormatAwkSink: a sink whose printf/sprintf use Java's standard String.format(...), giving AWK scripts access to Java-only conversions (%,d grouping, %tY date/time, ...) and faster formatting. OutputStreamAwkSink is now extensible to support it. - JRT.toAwkString() calls AwkPrintf.toAwkString() directly; the redundant AwkSink.formatOutputValue() middle layer and the deprecated normalizePrintArgument() legacy helper are removed. - AwkPrintf.parseInt() uses Integer.parseInt() with an explicit clamp for absurd digit runs, and zeros()/appendSpaces() build their padding with a single Arrays.fill() instead of char-by-char loops. Co-Authored-By: Claude Fable 5 --- src/main/java/io/jawk/backend/AVM.java | 22 +--- src/main/java/io/jawk/jrt/AwkPrintf.java | 80 ++++--------- src/main/java/io/jawk/jrt/AwkSink.java | 44 +------ src/main/java/io/jawk/jrt/JRT.java | 2 +- .../io/jawk/jrt/JavaStringFormatAwkSink.java | 111 ++++++++++++++++++ .../java/io/jawk/jrt/OutputStreamAwkSink.java | 3 +- src/site/markdown/behavior-changes.md | 4 + src/site/markdown/java-output.md | 14 ++- src/test/java/io/jawk/PrintfTest.java | 26 ++-- 9 files changed, 173 insertions(+), 133 deletions(-) create mode 100644 src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java index 7bf3f06a..296185ea 100644 --- a/src/main/java/io/jawk/backend/AVM.java +++ b/src/main/java/io/jawk/backend/AVM.java @@ -80,7 +80,6 @@ import io.jawk.intermediate.UninitializedObject; import io.jawk.intermediate.UntypedObject; import io.jawk.jrt.AssocArray; -import io.jawk.jrt.AwkPrintf; import io.jawk.jrt.AwkRuntimeException; import io.jawk.jrt.AwkSink; import io.jawk.jrt.BlockManager; @@ -2831,7 +2830,7 @@ private void execPrintToPipe(CountTuple tuple) throws IOException { private void execPrintf(CountTuple tuple) throws IOException { long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = checkPosixFormat(jrt.toAwkString(pop())); + String format = jrt.toAwkString(pop()); jrt.printfDefault(format, values); } @@ -2839,7 +2838,7 @@ private void execPrintfToFile(CountAndAppendTuple tuple) throws IOException { String key = jrt.toAwkString(pop()); long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = checkPosixFormat(jrt.toAwkString(pop())); + String format = jrt.toAwkString(pop()); jrt.printfToFile(key, tuple.isAppend(), format, values); } @@ -2847,7 +2846,7 @@ private void execPrintfToPipe(CountTuple tuple) throws IOException { String cmd = jrt.toAwkString(pop()); long numArgs = tuple.getCount(); Object[] values = popArguments(numArgs - 1); - String format = checkPosixFormat(jrt.toAwkString(pop())); + String format = jrt.toAwkString(pop()); jrt.printfToProcess(cmd, format, values); } @@ -3031,7 +3030,7 @@ private Object invokeIndirectBuiltin( requireIndirectArgumentCount(builtin, args, 1, Integer.MAX_VALUE, lineNumber); return jrt .sprintf( - checkPosixFormat(jrt.toAwkString(args[0])), + jrt.toAwkString(args[0]), Arrays.copyOfRange(args, 1, args.length)); case SQRT: requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber); @@ -3783,21 +3782,10 @@ private Object[] popArguments(long numArgs) { */ private String sprintfFunction(long numArgs) { Object[] argArray = popArguments(numArgs - 1); - String fmt = checkPosixFormat(jrt.toAwkString(pop())); + String fmt = jrt.toAwkString(pop()); return jrt.sprintf(fmt, argArray); } - /** - * Rejects gawk positional argument references in strict POSIX mode, like - * {@code gawk --posix}. - */ - private String checkPosixFormat(String format) { - if (settings.isPosix() && AwkPrintf.usesPositionalArguments(format)) { - throw new AwkRuntimeException("`$' is not permitted in awk formats"); - } - return format; - } - private void setNumOnJRT(long fieldNum, double num) { String numString = jrt.toAwkString(Double.valueOf(num)); diff --git a/src/main/java/io/jawk/jrt/AwkPrintf.java b/src/main/java/io/jawk/jrt/AwkPrintf.java index afe3fd58..841ee3dd 100644 --- a/src/main/java/io/jawk/jrt/AwkPrintf.java +++ b/src/main/java/io/jawk/jrt/AwkPrintf.java @@ -27,6 +27,7 @@ import java.math.MathContext; import java.math.RoundingMode; import java.text.DecimalFormatSymbols; +import java.util.Arrays; import java.util.IllegalFormatException; import java.util.Locale; @@ -124,52 +125,6 @@ public static String sprintf(final Locale locale, final String convfmt, final St return new AwkPrintfFormatter(actualLocale, actualConvfmt, format, actualArgs).format(); } - /** - * Returns whether a format string uses gawk positional argument - * references ({@code %n$} or {@code *n$}), which strict POSIX mode must - * reject: {@code gawk --posix} fails with - * `$' is not permitted in awk formats. - * - * @param format AWK format string - * @return {@code true} when the format references arguments by position - */ - public static boolean usesPositionalArguments(final String format) { - int length = format.length(); - int i = 0; - while (i < length) { - if (format.charAt(i) != '%') { - i++; - continue; - } - i++; - if (i < length && format.charAt(i) == '%') { - i++; - continue; - } - // Inside a specifier, a positional reference can appear right - // after '%' or right after '*'. - boolean positionAllowed = true; - while (i < length) { - char c = format.charAt(i); - if (positionAllowed && isAsciiDigit(c)) { - int digitsEnd = i; - while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) { - digitsEnd++; - } - if (digitsEnd < length && format.charAt(digitsEnd) == '$') { - return true; - } - } - positionAllowed = c == '*'; - if (c == '%' || CONVERSION_CHARS.indexOf(c) >= 0) { - break; - } - i++; - } - } - return false; - } - /** * Converts a value to a string using AWK's number-to-string rules. *

@@ -968,9 +923,7 @@ private void appendPadded(String body, boolean leftJustify, boolean zeroPad, int } private void appendSpaces(int count) { - for (int i = 0; i < count; i++) { - out.append(' '); - } + out.append(repeat(' ', count)); } } @@ -997,22 +950,29 @@ private static boolean isAsciiDigit(char c) { return c >= '0' && c <= '9'; } + /** + * Parses a run of decimal digits, clamping absurd widths, precisions, and + * argument positions to {@link Integer#MAX_VALUE} instead of failing the + * way {@link Integer#parseInt(String)} would. + */ private static int parseInt(String s, int from, int to) { - long value = 0; - for (int i = from; i < to; i++) { - value = value * 10 + s.charAt(i) - '0'; - if (value > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } + try { + return Integer.parseInt(s.substring(from, to)); + } catch (NumberFormatException e) { + return Integer.MAX_VALUE; } - return (int) value; } private static String zeros(int count) { - StringBuilder sb = new StringBuilder(Math.max(count, 0)); - for (int i = 0; i < count; i++) { - sb.append('0'); + return repeat('0', count); + } + + private static String repeat(char c, int count) { + if (count <= 0) { + return ""; } - return sb.toString(); + char[] chars = new char[count]; + Arrays.fill(chars, c); + return new String(chars); } } diff --git a/src/main/java/io/jawk/jrt/AwkSink.java b/src/main/java/io/jawk/jrt/AwkSink.java index 1cd11b01..f7669b90 100644 --- a/src/main/java/io/jawk/jrt/AwkSink.java +++ b/src/main/java/io/jawk/jrt/AwkSink.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; -import java.math.BigDecimal; import java.util.Locale; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -250,36 +249,7 @@ public static AwkSink from(Appendable appendable, Locale locale) { * @return the textual representation AWK would print for this operand */ protected final String formatPrintArgument(Object value, String ofmt) { - return formatOutputValue(value, ofmt, locale); - } - - /** - * Converts a {@code print} operand that renders as a numeric string into a - * numeric form. - *

- * Historical versions of Jawk applied this conversion in plain {@code print}, - * which is not what POSIX AWK does: {@code print} outputs string values - * verbatim, and only actual numbers are formatted with {@code OFMT}. The - * built-in sinks therefore no longer call this helper; it is preserved only - * for compatibility with custom sinks compiled against earlier releases. - *

- * - * @param value operand to normalize - * @return the normalized value, either unchanged or converted to a numeric form - * @deprecated Plain {@code print} does not numerically coerce string values; - * use the operand as-is, or {@link #formatPrintArgument(Object, String)} - * to render it the way {@code print} would. - */ - @Deprecated - protected final Object normalizePrintArgument(Object value) { - if (value == null || value instanceof Number) { - return value; - } - try { - return Double.valueOf(new BigDecimal(value.toString()).doubleValue()); - } catch (NumberFormatException e) { - return value; - } + return AwkPrintf.toAwkString(value, ofmt, locale); } /** @@ -303,16 +273,4 @@ public String sprintf(String convfmt, String format, Object... values) { Object[] safeValues = values == null ? new Object[0] : values; return AwkPrintf.sprintf(locale, convfmt, format, safeValues); } - - /** - * Formats one already-normalized AWK output value. - * - * @param value value to format - * @param ofmt numeric output format - * @param locale locale used for numeric formatting - * @return textual output for {@code value} - */ - public static String formatOutputValue(Object value, String ofmt, Locale locale) { - return AwkPrintf.toAwkString(value, ofmt, locale); - } } diff --git a/src/main/java/io/jawk/jrt/JRT.java b/src/main/java/io/jawk/jrt/JRT.java index a4395f49..8bdf5231 100644 --- a/src/main/java/io/jawk/jrt/JRT.java +++ b/src/main/java/io/jawk/jrt/JRT.java @@ -596,7 +596,7 @@ public String getAwkStringEntry(Map map, Object key) { * @return A String representation of o. */ public String toAwkString(Object o) { - return AwkSink.formatOutputValue(o, this.convfmt, this.locale); + return AwkPrintf.toAwkString(o, this.convfmt, this.locale); } /** diff --git a/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java b/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java new file mode 100644 index 00000000..15cd513f --- /dev/null +++ b/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java @@ -0,0 +1,111 @@ +package io.jawk.jrt; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * Jawk + * ჻჻჻჻჻჻ + * Copyright (C) 2006 - 2026 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Locale; +import io.jawk.intermediate.UninitializedObject; + +/** + * Text {@link AwkSink} whose {@code printf} and {@code sprintf} use Java's + * standard {@link String#format(Locale, String, Object...)} instead of AWK's + * formatting rules. + *

+ * This gives AWK scripts access to Java's formatting capabilities, such as + * {@code %,d} grouping, {@code %(d} negative parentheses, date/time + * conversions ({@code %tY}), and generally faster formatting. In exchange, + * the format string follows {@link java.util.Formatter} semantics, not POSIX + * AWK: conversions must match the value's Java type. AWK integral numbers + * reach the sink as {@link Long}, other numbers as {@link Double}, and text + * as {@link String}, so {@code %d} requires an integral value and {@code %f} + * a floating-point one; a mismatch raises Java's + * {@link java.util.IllegalFormatException}. Input-derived values and + * uninitialized variables are passed as plain strings. + *

+ */ +public class JavaStringFormatAwkSink extends OutputStreamAwkSink { + + /** + * Creates a sink backed by an {@link OutputStream}. + * + * @param outputStream stream that should receive AWK output + */ + public JavaStringFormatAwkSink(OutputStream outputStream) { + super(outputStream); + } + + /** + * Creates a sink backed by an {@link OutputStream}. + * + * @param outputStream stream that should receive AWK output + * @param locale locale used for formatting + */ + public JavaStringFormatAwkSink(OutputStream outputStream, Locale locale) { + super(outputStream, locale); + } + + /** + * Creates a sink backed directly by a {@link PrintStream}. + * + * @param printStream stream that should receive AWK output + */ + public JavaStringFormatAwkSink(PrintStream printStream) { + super(printStream); + } + + /** + * Creates a sink backed directly by a {@link PrintStream}. + * + * @param printStream stream that should receive AWK output + * @param locale locale used for formatting + */ + public JavaStringFormatAwkSink(PrintStream printStream, Locale locale) { + super(printStream, locale); + } + + /** + * Formats with Java's standard {@link String#format(Locale, String, Object...)} + * instead of AWK's rules. {@code convfmt} is not used: number-to-string + * conversion follows the Java conversion in the format string. + * + * @param convfmt number-to-string conversion format ({@code CONVFMT}), unused + * @param format {@link java.util.Formatter}-style format string + * @param values arguments supplied after the format string + * @return formatted text + */ + @Override + public String sprintf(String convfmt, String format, Object... values) { + Object[] safeValues = values == null ? new Object[0] : values; + Object[] javaValues = new Object[safeValues.length]; + for (int i = 0; i < safeValues.length; i++) { + Object value = safeValues[i]; + // Jawk-internal scalar types are exposed as plain strings. + if (value instanceof StrNum || value instanceof UninitializedObject) { + value = value.toString(); + } + javaValues[i] = value; + } + return String.format(getLocale(), format, javaValues); + } +} diff --git a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java index 1047d125..3a5f04cf 100644 --- a/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java +++ b/src/main/java/io/jawk/jrt/OutputStreamAwkSink.java @@ -32,7 +32,8 @@ /** * Text {@link AwkSink} backed by a {@link PrintStream}. */ -public final class OutputStreamAwkSink extends AwkSink { +@SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "No security-sensitive state; finalizer attacks are not a concern here.") +public class OutputStreamAwkSink extends AwkSink { private final PrintStream printStream; diff --git a/src/site/markdown/behavior-changes.md b/src/site/markdown/behavior-changes.md index 21ecac35..234bab39 100644 --- a/src/site/markdown/behavior-changes.md +++ b/src/site/markdown/behavior-changes.md @@ -51,6 +51,10 @@ released version automatically via .github/scripts/stamp-behavior-changes.sh. prints the full decimal expansion `1267650600228229401496703205376` (previously `9223372036854775807`), and `int()` preserves such values ([#528](https://github.com/jawkio/jawk/issues/528)). +- New `JavaStringFormatAwkSink` for Java embedders: a sink whose `printf`/`sprintf` use Java's + standard `String.format(...)` instead of AWK's formatting rules, giving scripts access to + Java-only conversions such as `%,d` grouping and `%tY` date/time + ([#528](https://github.com/jawkio/jawk/issues/528)). - Breaking change for Java embedders: `AwkSink.printf(...)` now receives the script's current `CONVFMT` value as a parameter (between `ofmt` and `format`), just like it already received `OFMT`, and `AwkSink.sprintf(...)` now takes `CONVFMT` as its first parameter diff --git a/src/site/markdown/java-output.md b/src/site/markdown/java-output.md index fe8ef66f..7dc0c6f1 100644 --- a/src/site/markdown/java-output.md +++ b/src/site/markdown/java-output.md @@ -127,10 +127,22 @@ awk.script("{ print $1, $2 }") ### Built-In Sink Implementations -Jawk provides two built-in `AwkSink` implementations: +Jawk provides three built-in `AwkSink` implementations: - **`AwkSink.from(PrintStream)`** / **`AwkSink.from(PrintStream, Locale)`** creates a sink that renders output to a `PrintStream`. This is the default behavior. - **`AwkSink.from(Appendable)`** / **`AwkSink.from(Appendable, Locale)`** renders output to any `Appendable` such as `StringBuilder` or `StringWriter`. +- **`JavaStringFormatAwkSink`** renders `printf`/`sprintf` with Java's standard + `String.format(...)` instead of AWK's formatting rules, giving scripts access to Java-only + conversions (`%,d` grouping, `%(d` negative parentheses, `%tY` date/time, etc.) and faster + formatting. Conversions must match the value's Java type: AWK integral numbers arrive as + `Long`, other numbers as `Double`, and text as `String`, so `%d` requires an integral value + and `%f` a floating-point one. + + ```java + awk.script("BEGIN { printf \"%,d\\n\", 1234567 }") + .execute(new JavaStringFormatAwkSink(System.out)); + // prints: 1,234,567 + ``` The overloads without a `Locale` parameter default to `Locale.US`. diff --git a/src/test/java/io/jawk/PrintfTest.java b/src/test/java/io/jawk/PrintfTest.java index d50c677a..cebed9cb 100644 --- a/src/test/java/io/jawk/PrintfTest.java +++ b/src/test/java/io/jawk/PrintfTest.java @@ -22,7 +22,12 @@ * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ */ +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import io.jawk.jrt.AwkRuntimeException; +import io.jawk.jrt.JavaStringFormatAwkSink; import org.junit.Test; /** @@ -160,16 +165,6 @@ public void testPositionalSpecifiers() throws Exception { .runAndAssert(); } - @Test - public void testPosixModeRejectsPositionalSpecifiers() throws Exception { - AwkTestSupport - .cliTest("printf positional specifiers are rejected in POSIX mode") - .argument("--posix") - .script("BEGIN { printf \"%2$s %1$s\\n\", \"world\", \"hello\" }") - .expectThrow(AwkRuntimeException.class) - .runAndAssert(); - } - @Test public void testUnterminatedStarPositionIsFatal() throws Exception { AwkTestSupport @@ -206,6 +201,17 @@ public void testSprintfRoundHalfEven() throws Exception { .runAndAssert(); } + @Test + public void testJavaStringFormatSink() throws Exception { + // JavaStringFormatAwkSink formats with Java's String.format, giving + // scripts access to Java-only conversions such as %,d grouping. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + new Awk() + .script("BEGIN { printf \"%,d|%05.1f|%s\\n\", 1234567, 3.5, \"ok\" }") + .execute(new JavaStringFormatAwkSink(new PrintStream(output, true))); + assertEquals("1,234,567|003.5|ok\n", output.toString()); + } + @Test public void testPrintfToFileHonorsConvfmt() throws Exception { AwkTestSupport From ec8dbc537d2678db02956f976bc1cd6fa23a932d Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 13 Aug 2026 13:34:10 +0200 Subject: [PATCH 17/18] Map AWK values to natural Java types in JavaStringFormatAwkSink Uninitialized variables reach String.format as Java null (rendered "null" by java.util.Formatter), per maintainer feedback, and all values are normalized with JRT.toJavaScalar(): integral doubles from runtime arithmetic become Long, so printf "%d", x + 1 works instead of throwing IllegalFormatConversionException (Codex catch). Co-Authored-By: Claude Fable 5 --- .../io/jawk/jrt/JavaStringFormatAwkSink.java | 25 ++++++++++++------- src/test/java/io/jawk/PrintfTest.java | 6 +++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java b/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java index 15cd513f..eec6ab04 100644 --- a/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java +++ b/src/main/java/io/jawk/jrt/JavaStringFormatAwkSink.java @@ -36,12 +36,13 @@ * {@code %,d} grouping, {@code %(d} negative parentheses, date/time * conversions ({@code %tY}), and generally faster formatting. In exchange, * the format string follows {@link java.util.Formatter} semantics, not POSIX - * AWK: conversions must match the value's Java type. AWK integral numbers - * reach the sink as {@link Long}, other numbers as {@link Double}, and text - * as {@link String}, so {@code %d} requires an integral value and {@code %f} - * a floating-point one; a mismatch raises Java's - * {@link java.util.IllegalFormatException}. Input-derived values and - * uninitialized variables are passed as plain strings. + * AWK: conversions must match the value's Java type. AWK numbers holding an + * integral value reach {@code String.format} as {@link Long}, other numbers + * as {@link Double}, and text as {@link String}, so {@code %d} requires an + * integral value and {@code %f} a floating-point one; a mismatch raises Java's + * {@link java.util.IllegalFormatException}. Input-derived values are passed + * as plain strings, and uninitialized variables as Java {@code null} (which + * {@link java.util.Formatter} renders as {@code "null"}). *

*/ public class JavaStringFormatAwkSink extends OutputStreamAwkSink { @@ -100,9 +101,15 @@ public String sprintf(String convfmt, String format, Object... values) { Object[] javaValues = new Object[safeValues.length]; for (int i = 0; i < safeValues.length; i++) { Object value = safeValues[i]; - // Jawk-internal scalar types are exposed as plain strings. - if (value instanceof StrNum || value instanceof UninitializedObject) { - value = value.toString(); + // Jawk-internal scalar types are mapped to their natural Java + // counterparts: input-derived text becomes a plain String, an + // uninitialized variable becomes null, and numbers become Long + // when they hold an integral value (runtime arithmetic yields + // doubles even for integral results). + if (value instanceof UninitializedObject) { + value = null; + } else { + value = JRT.toJavaScalar(value); } javaValues[i] = value; } diff --git a/src/test/java/io/jawk/PrintfTest.java b/src/test/java/io/jawk/PrintfTest.java index cebed9cb..a909cd17 100644 --- a/src/test/java/io/jawk/PrintfTest.java +++ b/src/test/java/io/jawk/PrintfTest.java @@ -207,9 +207,11 @@ public void testJavaStringFormatSink() throws Exception { // scripts access to Java-only conversions such as %,d grouping. ByteArrayOutputStream output = new ByteArrayOutputStream(); new Awk() - .script("BEGIN { printf \"%,d|%05.1f|%s\\n\", 1234567, 3.5, \"ok\" }") + .script("BEGIN { n = 1234566; printf \"%,d|%05.1f|%s|%s\\n\", n + 1, 3.5, \"ok\", novalue }") .execute(new JavaStringFormatAwkSink(new PrintStream(output, true))); - assertEquals("1,234,567|003.5|ok\n", output.toString()); + // The integral arithmetic result (a double at runtime) reaches + // String.format as a Long, and the uninitialized variable as null. + assertEquals("1,234,567|003.5|ok|null\n", output.toString()); } @Test From e47c80caf286eeafd726c9d0e19aab40be22d04d Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Thu, 13 Aug 2026 13:48:14 +0200 Subject: [PATCH 18/18] Clarify AGENTS.md: AwkTestSupport is for script-running tests only AwkTestSupport is reserved for tests that run AWK scripts and assess their results; unit tests of regular Java methods use plain JUnit assertions directly. Also fix the stale package name (io.jawk). Co-Authored-By: Claude Fable 5 --- AGENTS.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8d39de71..d076bcbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,10 +20,13 @@ Whenever required, when you add code or when you modify code that is not covered Compatibility tests are run with `mvn verify` to assess the compatibility with other implementations of AWK. These tests are run with the Maven failsafe plugin and results are stored in the ./target/failsafe-reports directory. -All new or updated unit tests must use the helper methods in -`org.metricshub.jawk.AwkTestSupport`. The builders in that class encapsulate the -correct Jawk setup, assertion flow, and temporary file handling, so reusing -them keeps the test suite consistent and reliable. +All new or updated unit tests that run AWK scripts must use the helper methods +in `io.jawk.AwkTestSupport`. The builders in that class encapsulate the correct +Jawk setup, assertion flow, and temporary file handling, so reusing them keeps +the test suite consistent and reliable. `AwkTestSupport` is ONLY for running +AWK scripts and assessing their results: unit tests for regular Java methods +call those methods directly with plain JUnit assertions, and no non-script +assertion helpers may be added to `AwkTestSupport`. ## Code quality reports