View Javadoc
1   package io.jawk.jrt;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2026 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  import java.math.BigDecimal;
26  import java.math.BigInteger;
27  import java.math.MathContext;
28  import java.math.RoundingMode;
29  import java.text.DecimalFormatSymbols;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.IllegalFormatException;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.concurrent.ConcurrentHashMap;
36  
37  /**
38   * AWK's {@code printf}/{@code sprintf} formatting engine.
39   * <p>
40   * This class implements the POSIX AWK formatting semantics (as implemented by
41   * gawk), which differ from both C's {@code printf()} and
42   * {@link java.lang.String#format(String, Object...)} in several ways:
43   * </p>
44   * <ul>
45   * <li>{@code %s} converts numeric values to strings with AWK's number-to-string
46   * rules: integral values are printed without a fractional part, and other
47   * values are formatted with {@code CONVFMT};</li>
48   * <li>{@code %c} prints the character for a numeric code point, or the first
49   * character of a string value;</li>
50   * <li>{@code %i} is an alias for {@code %d}, and {@code %u} prints the value
51   * as an unsigned 64-bit integer;</li>
52   * <li>dynamic field width and precision ({@code *}) consume arguments, and
53   * gawk-style positional specifiers ({@code %n$}) are honored;</li>
54   * <li>integer conversions of values that exceed the 64-bit range fall back to
55   * the full decimal expansion ({@code %d}/{@code %i}) or {@code %g} notation
56   * ({@code %u}/{@code %o}/{@code %x}/{@code %X}), like gawk;</li>
57   * <li>NaN and infinities print as {@code nan}, {@code inf}, and {@code -inf};</li>
58   * <li>{@code %e}, {@code %f}, and {@code %g} round halfway cases to even, like
59   * the C library used by gawk;</li>
60   * <li>unknown conversion specifiers are printed verbatim without consuming an
61   * argument, and a fatal {@link AwkRuntimeException} is raised when there are
62   * not enough arguments to satisfy the format string.</li>
63   * </ul>
64   * <p>
65   * This formatting logic was originally externalized in the
66   * <a href="https://github.com/metricshub/printf4j">Printf4J</a> project, which
67   * emulated glibc's {@code printf()}. It has been reincorporated into Jawk and
68   * adapted to AWK's semantics.
69   * </p>
70   */
71  public final class AwkPrintf {
72  
73  	/** Default AWK number-to-string conversion format ({@code CONVFMT}). */
74  	public static final String DEFAULT_CONVFMT = "%.6g";
75  
76  	/** Conversion characters recognized as AWK format specifiers. */
77  	private static final String CONVERSION_CHARS = "diouxXeEfFgGaAcs";
78  
79  	/** Length modifier characters accepted (and ignored) like gawk. */
80  	private static final String LENGTH_MODIFIERS = "hjlLtz";
81  
82  	/** A one-character string holding the NUL character, printed by {@code %c} for empty values. */
83  	private static final String NUL_STRING = Character.toString((char) 0);
84  
85  	/** 2^63 as a double, the first value beyond the signed 64-bit range. */
86  	private static final double TWO_POW_63 = 9.223372036854775808e18;
87  
88  	/** 2^64 as a {@link BigInteger}, used for unsigned wrapping checks. */
89  	private static final BigInteger TWO_POW_64 = BigInteger.ONE.shiftLeft(64);
90  
91  	/** No width or precision operand in the specifier. */
92  	private static final int OPERAND_NONE = 0;
93  
94  	/** Width or precision given as literal digits in the format string. */
95  	private static final int OPERAND_FIXED = 1;
96  
97  	/** Width or precision given as {@code *}, consuming a sequential argument. */
98  	private static final int OPERAND_STAR = 2;
99  
100 	/** Width or precision given as {@code *n$}, referencing a positional argument. */
101 	private static final int OPERAND_STAR_POSITIONAL = 3;
102 
103 	/** Shared flag set with every flag cleared, for segments that ignore flags. */
104 	private static final Flags NO_FLAGS = new Flags(false, false, false, false, false, false);
105 
106 	/** Maximum number of parsed format strings kept in {@link #FORMAT_CACHE}. */
107 	private static final int FORMAT_CACHE_LIMIT = 256;
108 
109 	/**
110 	 * Cache of parsed format strings. Parsing depends only on the format text,
111 	 * not on the locale or the arguments, and AWK programs typically reuse a
112 	 * handful of format strings (including {@code CONVFMT}) for every record.
113 	 */
114 	private static final ConcurrentHashMap<String, Segment[]> FORMAT_CACHE = new ConcurrentHashMap<String, Segment[]>();
115 
116 	/** Maximum number of locales kept in {@link #LOCALE_SYMBOLS_CACHE}. */
117 	private static final int LOCALE_SYMBOLS_CACHE_LIMIT = 64;
118 
119 	/**
120 	 * Per-locale decimal and grouping separator characters.
121 	 * {@link DecimalFormatSymbols#getInstance(Locale)} clones the symbols on
122 	 * every call, which is far too expensive to repeat for every conversion.
123 	 */
124 	private static final ConcurrentHashMap<Locale, LocaleSymbols> LOCALE_SYMBOLS_CACHE = new ConcurrentHashMap<Locale, LocaleSymbols>();
125 
126 	private AwkPrintf() {
127 		throw new UnsupportedOperationException();
128 	}
129 
130 	/**
131 	 * Formats the given arguments with AWK's {@code sprintf()} semantics, using
132 	 * {@link Locale#US} and the default {@code CONVFMT} ({@code "%.6g"}).
133 	 *
134 	 * @param format AWK format string
135 	 * @param args arguments supplied after the format string
136 	 * @return the formatted text
137 	 * @throws AwkRuntimeException when there are not enough arguments to
138 	 *         satisfy the format string
139 	 */
140 	public static String sprintf(final String format, final Object... args) {
141 		return sprintf(Locale.US, DEFAULT_CONVFMT, format, args);
142 	}
143 
144 	/**
145 	 * Formats the given arguments with AWK's {@code sprintf()} semantics.
146 	 *
147 	 * @param locale locale used for numeric formatting (decimal separator,
148 	 *        grouping separator for the {@code '} flag)
149 	 * @param convfmt number-to-string conversion format ({@code CONVFMT}) used
150 	 *        by {@code %s} for non-integral numeric values
151 	 * @param format AWK format string
152 	 * @param args arguments supplied after the format string
153 	 * @return the formatted text
154 	 * @throws AwkRuntimeException when there are not enough arguments to
155 	 *         satisfy the format string
156 	 */
157 	public static String sprintf(final Locale locale, final String convfmt, final String format, final Object... args) {
158 		Locale actualLocale = locale == null ? Locale.US : locale;
159 		// An explicitly empty CONVFMT stays empty, like gawk; only a null
160 		// (absent) format selects the default.
161 		String actualConvfmt = convfmt == null ? DEFAULT_CONVFMT : convfmt;
162 		Object[] actualArgs = args == null ? new Object[0] : args;
163 		return new AwkPrintfFormatter(actualLocale, actualConvfmt, format, actualArgs).format();
164 	}
165 
166 	/**
167 	 * Converts a value to a string using AWK's number-to-string rules.
168 	 * <p>
169 	 * Non-numeric values are converted with {@code toString()}. Numeric values
170 	 * holding an integral value are printed without a fractional part (using
171 	 * the full decimal expansion when the value exceeds the 64-bit range), NaN
172 	 * and infinities print as {@code nan}, {@code inf}, and {@code -inf}, and
173 	 * all other numeric values are formatted with the supplied conversion
174 	 * format ({@code CONVFMT} or {@code OFMT}).
175 	 * </p>
176 	 *
177 	 * @param value value to convert
178 	 * @param conversionFormat number-to-string conversion format
179 	 * @param locale locale used for numeric formatting
180 	 * @return the AWK string value of {@code value}
181 	 */
182 	public static String toAwkString(final Object value, final String conversionFormat, final Locale locale) {
183 		if (value == null) {
184 			return "";
185 		}
186 		if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) {
187 			// Preserve exact 64-bit values that a double round-trip would corrupt.
188 			return Long.toString(((Number) value).longValue());
189 		}
190 		if (!(value instanceof Number)) {
191 			return value.toString();
192 		}
193 		return numberToAwkString(((Number) value).doubleValue(), conversionFormat, locale);
194 	}
195 
196 	private static String numberToAwkString(final double number, final String conversionFormat, final Locale locale) {
197 		if (Double.isNaN(number)) {
198 			return "nan";
199 		}
200 		if (Double.isInfinite(number)) {
201 			return number > 0 ? "inf" : "-inf";
202 		}
203 		if (JRT.isActuallyLong(number)) {
204 			double rounded = Math.rint(number);
205 			if (rounded >= -TWO_POW_63 && rounded < TWO_POW_63) {
206 				return Long.toString((long) rounded);
207 			}
208 			// The exact binary value of the double is intended: it makes rounding match gawk's C library.
209 			return new BigDecimal(rounded).toBigInteger().toString(); // NOPMD
210 		}
211 		// An explicitly empty CONVFMT/OFMT stays empty, like gawk; only a
212 		// null (absent) format selects the default.
213 		String fmt = conversionFormat == null ? DEFAULT_CONVFMT : conversionFormat;
214 		return sprintf(locale, DEFAULT_CONVFMT, fmt, Double.valueOf(number));
215 	}
216 
217 	/**
218 	 * Immutable set of conversion flags parsed from one format specifier.
219 	 */
220 	private static final class Flags {
221 
222 		private final boolean leftJustify;
223 		private final boolean plusSign;
224 		private final boolean spaceSign;
225 		private final boolean zeroPad;
226 		private final boolean alternate;
227 		private final boolean grouping;
228 
229 		Flags(boolean leftJustify, boolean plusSign, boolean spaceSign, boolean zeroPad, boolean alternate,
230 				boolean grouping) {
231 			this.leftJustify = leftJustify;
232 			this.plusSign = plusSign;
233 			this.spaceSign = spaceSign;
234 			this.zeroPad = zeroPad;
235 			this.alternate = alternate;
236 			this.grouping = grouping;
237 		}
238 
239 		/**
240 		 * Returns these flags with the {@code '} grouping flag cleared, for
241 		 * conversions that gawk never groups (octal and hexadecimal).
242 		 *
243 		 * @return an equivalent flag set without grouping
244 		 */
245 		Flags withoutGrouping() {
246 			return grouping ? new Flags(leftJustify, plusSign, spaceSign, zeroPad, alternate, false) : this;
247 		}
248 
249 		/**
250 		 * Returns these flags with the {@code -} flag set, as a negative
251 		 * dynamic field width requires.
252 		 *
253 		 * @return an equivalent flag set with left justification
254 		 */
255 		Flags withLeftJustify() {
256 			return leftJustify ? this : new Flags(true, plusSign, spaceSign, zeroPad, alternate, grouping);
257 		}
258 	}
259 
260 	/**
261 	 * Decimal and grouping separator characters of one locale.
262 	 */
263 	private static final class LocaleSymbols {
264 
265 		private final char decimalSeparator;
266 		private final char groupingSeparator;
267 
268 		LocaleSymbols(char decimalSeparator, char groupingSeparator) {
269 			this.decimalSeparator = decimalSeparator;
270 			this.groupingSeparator = groupingSeparator;
271 		}
272 	}
273 
274 	/**
275 	 * One parsed piece of a format string: either verbatim text or one
276 	 * conversion specifier. Segments are immutable and shared between all
277 	 * calls using the same format string, so rendering replays argument
278 	 * consumption, argument-mode tracking, and fatal parse errors in exactly
279 	 * the order the previous single-pass implementation produced them.
280 	 */
281 	private static final class Segment {
282 
283 		/** Verbatim text to append, or {@code null} for a conversion. */
284 		private final String literal;
285 
286 		/** Positional ({@code n$}) argument index, or 0 for sequential. */
287 		private final int argPosition;
288 
289 		private final Flags flags;
290 
291 		/** Width operand kind, one of the {@code OPERAND_*} constants. */
292 		private final int widthKind;
293 
294 		/** Fixed width value, or the {@code *n$} width argument position. */
295 		private final int width;
296 
297 		/** Precision operand kind, one of the {@code OPERAND_*} constants. */
298 		private final int precisionKind;
299 
300 		/** Fixed precision value, or the {@code *n$} precision argument position. */
301 		private final int precision;
302 
303 		/** Conversion character, or {@code '\0'} for literal segments. */
304 		private final char conversion;
305 
306 		/**
307 		 * Fatal parse error raised when rendering reaches this segment (after
308 		 * the width operand has consumed its argument), or {@code null}.
309 		 */
310 		private final String parseError;
311 
312 		Segment(String literal, int argPosition, Flags flags, int widthKind, int width, int precisionKind,
313 				int precision, char conversion, String parseError) {
314 			this.literal = literal;
315 			this.argPosition = argPosition;
316 			this.flags = flags;
317 			this.widthKind = widthKind;
318 			this.width = width;
319 			this.precisionKind = precisionKind;
320 			this.precision = precision;
321 			this.conversion = conversion;
322 			this.parseError = parseError;
323 		}
324 	}
325 
326 	/**
327 	 * Returns the decimal and grouping separators of the given locale, from a
328 	 * bounded cache.
329 	 */
330 	private static LocaleSymbols localeSymbolsFor(Locale locale) {
331 		LocaleSymbols symbols = LOCALE_SYMBOLS_CACHE.get(locale);
332 		if (symbols == null) {
333 			DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale);
334 			symbols = new LocaleSymbols(dfs.getDecimalSeparator(), dfs.getGroupingSeparator());
335 			if (LOCALE_SYMBOLS_CACHE.size() >= LOCALE_SYMBOLS_CACHE_LIMIT) {
336 				LOCALE_SYMBOLS_CACHE.clear();
337 			}
338 			LOCALE_SYMBOLS_CACHE.putIfAbsent(locale, symbols);
339 		}
340 		return symbols;
341 	}
342 
343 	/**
344 	 * Returns the parsed form of the given format string, from a bounded
345 	 * cache.
346 	 */
347 	private static Segment[] compiledFormat(String format) {
348 		Segment[] segments = FORMAT_CACHE.get(format);
349 		if (segments == null) {
350 			segments = compileFormat(format);
351 			if (FORMAT_CACHE.size() >= FORMAT_CACHE_LIMIT) {
352 				// Rare: a program cycling through many generated format
353 				// strings. Dropping the whole cache keeps the bound simple
354 				// and the hit path lock-free.
355 				FORMAT_CACHE.clear();
356 			}
357 			Segment[] winner = FORMAT_CACHE.putIfAbsent(format, segments);
358 			if (winner != null) {
359 				return winner;
360 			}
361 		}
362 		return segments;
363 	}
364 
365 	/**
366 	 * Parses a format string into segments. This method never throws: fatal
367 	 * parse errors are recorded as error segments so that rendering raises
368 	 * them only once every preceding conversion has consumed its arguments,
369 	 * preserving the error precedence of single-pass processing.
370 	 */
371 	private static Segment[] compileFormat(String format) {
372 		List<Segment> segments = new ArrayList<Segment>();
373 		StringBuilder literal = new StringBuilder();
374 		int length = format.length();
375 		int i = 0;
376 		while (i < length) {
377 			char c = format.charAt(i);
378 			if (c != '%') {
379 				literal.append(c);
380 				i++;
381 				continue;
382 			}
383 			if (i + 1 >= length) {
384 				// Dangling '%' at the end of the format: print it verbatim.
385 				literal.append('%');
386 				break;
387 			}
388 			if (format.charAt(i + 1) == '%') {
389 				literal.append('%');
390 				i += 2;
391 				continue;
392 			}
393 			i = compileSpecifier(format, i, segments, literal);
394 			if (i < 0) {
395 				// A fatal parse error segment was emitted; the rest of the
396 				// format string is unreachable.
397 				break;
398 			}
399 		}
400 		flushLiteral(segments, literal);
401 		return segments.toArray(new Segment[0]);
402 	}
403 
404 	/**
405 	 * Parses one format specifier starting at {@code start} (which points at
406 	 * the {@code '%'}), emitting segments, and returns the index of the first
407 	 * character after the specifier, or a negative value when a fatal parse
408 	 * error segment was emitted.
409 	 */
410 	private static int compileSpecifier(String format, int start, List<Segment> segments, StringBuilder literal) {
411 		int length = format.length();
412 		int i = start + 1;
413 
414 		// gawk-style positional specifier: %n$...
415 		int argPosition = 0;
416 		int digitsEnd = i;
417 		while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) {
418 			digitsEnd++;
419 		}
420 		if (digitsEnd > i && digitsEnd < length && format.charAt(digitsEnd) == '$') {
421 			argPosition = parseInt(format, i, digitsEnd);
422 			if (argPosition <= 0) {
423 				emitError(
424 						segments,
425 						literal,
426 						OPERAND_NONE,
427 						0,
428 						"argument index with `$' must be > 0 in `" + format + "'");
429 				return -1;
430 			}
431 			i = digitsEnd + 1;
432 		}
433 
434 		// Flags, in any order and possibly repeated.
435 		boolean leftJustify = false;
436 		boolean plusSign = false;
437 		boolean spaceSign = false;
438 		boolean zeroPad = false;
439 		boolean alternate = false;
440 		boolean grouping = false;
441 		flagLoop: while (i < length) {
442 			switch (format.charAt(i)) {
443 			case '-':
444 				leftJustify = true;
445 				break;
446 			case '+':
447 				plusSign = true;
448 				break;
449 			case ' ':
450 				spaceSign = true;
451 				break;
452 			case '0':
453 				zeroPad = true;
454 				break;
455 			case '#':
456 				alternate = true;
457 				break;
458 			case '\'':
459 				grouping = true;
460 				break;
461 			default:
462 				break flagLoop;
463 			}
464 			i++;
465 		}
466 
467 		// Field width: digits, or '*' (optionally '*n$').
468 		int widthKind = OPERAND_NONE;
469 		int width = -1;
470 		if (i < length && format.charAt(i) == '*') {
471 			i++;
472 			int starArgEnd = starPositionEnd(format, i);
473 			if (starArgEnd == i && i < length && isAsciiDigit(format.charAt(i))) {
474 				// Digits after a star operand not terminated by '$' are
475 				// fatal rather than literal, like gawk: "%*2d".
476 				emitError(
477 						segments,
478 						literal,
479 						OPERAND_NONE,
480 						0,
481 						"no `$' supplied for positional field width or precision in `" + format + "'");
482 				return -1;
483 			}
484 			if (starArgEnd > i) {
485 				widthKind = OPERAND_STAR_POSITIONAL;
486 				width = parseInt(format, i, starArgEnd - 1);
487 				i = starArgEnd;
488 			} else {
489 				widthKind = OPERAND_STAR;
490 			}
491 		} else {
492 			int widthEnd = i;
493 			while (widthEnd < length && isAsciiDigit(format.charAt(widthEnd))) {
494 				widthEnd++;
495 			}
496 			if (widthEnd > i) {
497 				widthKind = OPERAND_FIXED;
498 				width = parseInt(format, i, widthEnd);
499 				i = widthEnd;
500 			}
501 		}
502 
503 		// Precision: '.' followed by digits (empty means 0), or '.*'.
504 		int precisionKind = OPERAND_NONE;
505 		int precision = -1;
506 		if (i < length && format.charAt(i) == '.') {
507 			i++;
508 			if (i < length && format.charAt(i) == '*') {
509 				i++;
510 				int starArgEnd = starPositionEnd(format, i);
511 				if (starArgEnd == i && i < length && isAsciiDigit(format.charAt(i))) {
512 					// The width operand (if any) still consumes its argument
513 					// before this error is raised, like gawk: "%*.*2d".
514 					emitError(
515 							segments,
516 							literal,
517 							widthKind,
518 							width,
519 							"no `$' supplied for positional field width or precision in `" + format + "'");
520 					return -1;
521 				}
522 				if (starArgEnd > i) {
523 					precisionKind = OPERAND_STAR_POSITIONAL;
524 					precision = parseInt(format, i, starArgEnd - 1);
525 					i = starArgEnd;
526 				} else {
527 					precisionKind = OPERAND_STAR;
528 				}
529 			} else {
530 				int precisionEnd = i;
531 				while (precisionEnd < length && isAsciiDigit(format.charAt(precisionEnd))) {
532 					precisionEnd++;
533 				}
534 				precisionKind = OPERAND_FIXED;
535 				precision = precisionEnd == i ? 0 : parseInt(format, i, precisionEnd);
536 				i = precisionEnd;
537 			}
538 		}
539 
540 		// Length modifiers (h, j, l, L, t, z) are each accepted at most
541 		// once and ignored, like gawk. A repeated modifier such as "ll"
542 		// or "hh" makes the whole specifier invalid, also like gawk.
543 		int modifierMask = 0;
544 		while (i < length) {
545 			int modifierIndex = LENGTH_MODIFIERS.indexOf(format.charAt(i));
546 			if (modifierIndex < 0 || (modifierMask & 1 << modifierIndex) != 0) {
547 				break;
548 			}
549 			modifierMask |= 1 << modifierIndex;
550 			i++;
551 		}
552 
553 		if (i < length && format.charAt(i) == '%') {
554 			// A percent conversion reached through flags, width, or
555 			// precision prints a plain '%' and ignores them all, like
556 			// gawk ("%5%" prints "%"). Star operands still consume their
557 			// arguments, and an explicit position still pins the format to
558 			// positional mode, also like gawk.
559 			emitVerbatim(
560 					segments,
561 					literal,
562 					new Segment(
563 							"%",
564 							argPosition,
565 							NO_FLAGS,
566 							widthKind,
567 							width,
568 							precisionKind,
569 							precision,
570 							(char) 0,
571 							null));
572 			return i + 1;
573 		}
574 
575 		if (i >= length || CONVERSION_CHARS.indexOf(format.charAt(i)) < 0) {
576 			// Unknown or unterminated conversion: print the specifier
577 			// verbatim (including the offending character) without
578 			// consuming a value argument, like gawk. Star operands still
579 			// consume their arguments, and an explicit position still pins
580 			// the format to positional mode, also like gawk.
581 			int end = i < length ? i + 1 : length;
582 			emitVerbatim(
583 					segments,
584 					literal,
585 					new Segment(
586 							format.substring(start, end),
587 							argPosition,
588 							NO_FLAGS,
589 							widthKind,
590 							width,
591 							precisionKind,
592 							precision,
593 							(char) 0,
594 							null));
595 			return end;
596 		}
597 
598 		char conversion = format.charAt(i);
599 		i++;
600 
601 		flushLiteral(segments, literal);
602 		segments
603 				.add(
604 						new Segment(
605 								null,
606 								argPosition,
607 								new Flags(leftJustify, plusSign, spaceSign, zeroPad, alternate, grouping),
608 								widthKind,
609 								width,
610 								precisionKind,
611 								precision,
612 								conversion,
613 								null));
614 		return i;
615 	}
616 
617 	/**
618 	 * Emits verbatim specifier text (a {@code %} conversion or an unknown
619 	 * conversion). When the specifier has no side effects (no star operand to
620 	 * consume, no positional index to validate), the text is merged into the
621 	 * pending literal run; otherwise a standalone segment replays those side
622 	 * effects before appending the text.
623 	 */
624 	private static void emitVerbatim(List<Segment> segments, StringBuilder literal, Segment segment) {
625 		boolean sideEffectFree = segment.argPosition == 0
626 				&& segment.widthKind != OPERAND_STAR
627 				&& segment.widthKind != OPERAND_STAR_POSITIONAL
628 				&& segment.precisionKind != OPERAND_STAR
629 				&& segment.precisionKind != OPERAND_STAR_POSITIONAL;
630 		if (sideEffectFree) {
631 			literal.append(segment.literal);
632 			return;
633 		}
634 		flushLiteral(segments, literal);
635 		segments.add(segment);
636 	}
637 
638 	/**
639 	 * Emits a fatal parse error segment, preceded by any width operand whose
640 	 * argument consumption must be replayed before the error is raised.
641 	 */
642 	private static void emitError(
643 			List<Segment> segments,
644 			StringBuilder literal,
645 			int widthKind,
646 			int width,
647 			String message) {
648 		flushLiteral(segments, literal);
649 		segments.add(new Segment(null, 0, NO_FLAGS, widthKind, width, OPERAND_NONE, -1, (char) 0, message));
650 	}
651 
652 	/** Emits the pending literal text run as a segment, if any. */
653 	private static void flushLiteral(List<Segment> segments, StringBuilder literal) {
654 		if (literal.length() > 0) {
655 			segments
656 					.add(
657 							new Segment(
658 									literal.toString(),
659 									0,
660 									NO_FLAGS,
661 									OPERAND_NONE,
662 									-1,
663 									OPERAND_NONE,
664 									-1,
665 									(char) 0,
666 									null));
667 			literal.setLength(0);
668 		}
669 	}
670 
671 	/**
672 	 * Returns the index right after a {@code n$} sequence starting at
673 	 * {@code i}, or {@code i} when there is no such sequence.
674 	 */
675 	private static int starPositionEnd(String format, int i) {
676 		int length = format.length();
677 		int digitsEnd = i;
678 		while (digitsEnd < length && isAsciiDigit(format.charAt(digitsEnd))) {
679 			digitsEnd++;
680 		}
681 		if (digitsEnd > i && digitsEnd < length && format.charAt(digitsEnd) == '$') {
682 			return digitsEnd + 1;
683 		}
684 		return i;
685 	}
686 
687 	/**
688 	 * Stateful renderer for one {@code sprintf()} call, walking the parsed
689 	 * segments of the format string.
690 	 */
691 	private static final class AwkPrintfFormatter {
692 
693 		private final Locale locale;
694 		private final String convfmt;
695 		private final String format;
696 		private final Object[] args;
697 		private final StringBuilder out;
698 		private final char decimalSeparator;
699 		private final char groupingSeparator;
700 
701 		/** Index of the next sequential argument to consume. */
702 		private int argIndex;
703 
704 		/** Whether a positional ({@code n$}) argument reference was seen. */
705 		private boolean sawPositional;
706 
707 		/** Whether a sequential argument reference was seen. */
708 		private boolean sawSequential;
709 
710 		AwkPrintfFormatter(Locale locale, String convfmt, String format, Object[] args) {
711 			this.locale = locale;
712 			this.convfmt = convfmt;
713 			this.format = format;
714 			this.args = args;
715 			this.out = new StringBuilder(format.length() + 16);
716 			LocaleSymbols symbols = localeSymbolsFor(locale);
717 			this.decimalSeparator = symbols.decimalSeparator;
718 			this.groupingSeparator = symbols.groupingSeparator;
719 		}
720 
721 		String format() {
722 			for (Segment segment : compiledFormat(format)) {
723 				renderSegment(segment);
724 			}
725 			return out.toString();
726 		}
727 
728 		/**
729 		 * Renders one segment: evaluates the width and precision operands
730 		 * (consuming their arguments), raises any recorded parse error, and
731 		 * appends the literal text or the converted value.
732 		 */
733 		private void renderSegment(Segment segment) {
734 			Flags flags = segment.flags;
735 			int width = -1;
736 			if (segment.widthKind != OPERAND_NONE) {
737 				if (segment.widthKind == OPERAND_FIXED) {
738 					width = segment.width;
739 				} else {
740 					long dynamicWidth;
741 					if (segment.widthKind == OPERAND_STAR) {
742 						// A sequential star operand pins the format to
743 						// sequential mode; an explicitly positioned one is
744 						// neutral.
745 						recordArgumentMode(false);
746 						dynamicWidth = (long) JRT.toDouble(nextArg());
747 					} else {
748 						// gawk treats a zero-indexed star operand ("%*0$d")
749 						// as the value zero, without consuming an argument.
750 						dynamicWidth = segment.width == 0 ? 0 : (long) JRT.toDouble(argAt(segment.width));
751 					}
752 					if (dynamicWidth < 0) {
753 						flags = flags.withLeftJustify();
754 						dynamicWidth = -dynamicWidth;
755 					}
756 					width = (int) Math.min(dynamicWidth, Integer.MAX_VALUE);
757 				}
758 			}
759 			if (segment.parseError != null) {
760 				throw new AwkRuntimeException(segment.parseError);
761 			}
762 			int precision = -1;
763 			if (segment.precisionKind != OPERAND_NONE) {
764 				if (segment.precisionKind == OPERAND_FIXED) {
765 					precision = segment.precision;
766 				} else {
767 					long dynamicPrecision;
768 					if (segment.precisionKind == OPERAND_STAR) {
769 						// Same sequential-mode tracking as the width operand.
770 						recordArgumentMode(false);
771 						dynamicPrecision = (long) JRT.toDouble(nextArg());
772 					} else {
773 						// Same zero-index rule as the width operand.
774 						dynamicPrecision = segment.precision == 0 ? 0 : (long) JRT.toDouble(argAt(segment.precision));
775 					}
776 					// A negative dynamic precision means "no precision" in C.
777 					if (dynamicPrecision >= 0) {
778 						precision = (int) Math.min(dynamicPrecision, Integer.MAX_VALUE);
779 					}
780 				}
781 			}
782 			if (segment.literal != null) {
783 				// Verbatim text; a positional specifier still pins the format
784 				// to positional mode and validates its index, like gawk.
785 				if (segment.argPosition > 0) {
786 					recordArgumentMode(true);
787 					requireArgumentIndex(segment.argPosition);
788 				}
789 				out.append(segment.literal);
790 				return;
791 			}
792 			recordArgumentMode(segment.argPosition > 0);
793 			Object arg = segment.argPosition > 0 ? argAt(segment.argPosition) : nextArg();
794 			render(segment.conversion, flags, width, precision, arg);
795 		}
796 
797 		private Object nextArg() {
798 			if (argIndex >= args.length) {
799 				throw new AwkRuntimeException("not enough arguments to satisfy format string `" + format + "'");
800 			}
801 			return args[argIndex++];
802 		}
803 
804 		private Object argAt(int position) {
805 			requireArgumentIndex(position);
806 			return args[position - 1];
807 		}
808 
809 		/**
810 		 * Validates a positional ({@code n$}) argument index against the
811 		 * supplied arguments, like gawk, which checks the index even for
812 		 * conversions that do not consume the referenced value.
813 		 */
814 		private void requireArgumentIndex(int position) {
815 			if (position <= 0) {
816 				throw new AwkRuntimeException("argument index with `$' must be > 0 in `" + format + "'");
817 			}
818 			if (position > args.length) {
819 				throw new AwkRuntimeException(
820 						"argument index " + position + " greater than total number of supplied arguments in `"
821 								+ format + "'");
822 			}
823 		}
824 
825 		/**
826 		 * Records how one conversion selects its value argument and rejects
827 		 * format strings that mix positional ({@code n$}) and sequential
828 		 * conversions, like gawk. Star width and precision operands are not
829 		 * tracked: gawk allows an explicitly positioned star operand
830 		 * ({@code %*2$s}) alongside sequential conversions.
831 		 *
832 		 * @param positional whether the conversion used an {@code n$} index
833 		 */
834 		private void recordArgumentMode(boolean positional) {
835 			if (positional) {
836 				sawPositional = true;
837 			} else {
838 				sawSequential = true;
839 			}
840 			if (sawPositional && sawSequential) {
841 				throw new AwkRuntimeException("must use `count$' on all formats or none in `" + format + "'");
842 			}
843 		}
844 
845 		private void render(char conversion, Flags flags, int width, int precision, Object arg) {
846 			switch (conversion) {
847 			case 'c':
848 				appendPadded(characterOf(arg), flags.leftJustify, false, width);
849 				break;
850 			case 's':
851 				String s = toAwkString(arg, convfmt, locale);
852 				if (precision >= 0 && s.codePointCount(0, s.length()) > precision) {
853 					// The precision counts characters (code points), so it
854 					// can never split a surrogate pair.
855 					s = s.substring(0, s.offsetByCodePoints(0, precision));
856 				}
857 				appendPadded(s, flags.leftJustify, false, width);
858 				break;
859 			case 'd':
860 			case 'i':
861 				renderSignedInteger(flags, width, precision, arg);
862 				break;
863 			case 'u':
864 			case 'o':
865 			case 'x':
866 			case 'X':
867 				renderUnsignedInteger(conversion, flags, width, precision, arg);
868 				break;
869 			case 'e':
870 			case 'E':
871 			case 'f':
872 			case 'F':
873 			case 'g':
874 			case 'G':
875 			case 'a':
876 			case 'A':
877 				renderFloat(conversion, flags, width, precision, arg);
878 				break;
879 			default:
880 				// Unreachable: the caller only passes known conversions.
881 				break;
882 			}
883 		}
884 
885 		/** Renders the {@code %c} character for the given argument. */
886 		private String characterOf(Object arg) {
887 			if (arg == null) {
888 				return NUL_STRING;
889 			}
890 			boolean numeric = arg instanceof Number || (arg instanceof StrNum && ((StrNum) arg).isNumber());
891 			if (numeric) {
892 				long code = (long) JRT.toDouble(arg);
893 				StringBuilder sb = new StringBuilder(2);
894 				if (code >= 0 && code <= Character.MAX_CODE_POINT) {
895 					sb.appendCodePoint((int) code);
896 				} else {
897 					sb.append((char) code);
898 				}
899 				return sb.toString();
900 			}
901 			String s = arg.toString();
902 			if (s.isEmpty()) {
903 				return NUL_STRING;
904 			}
905 			StringBuilder sb = new StringBuilder(2);
906 			sb.appendCodePoint(s.codePointAt(0));
907 			return sb.toString();
908 		}
909 
910 		private void renderSignedInteger(Flags flags, int width, int precision, Object arg) {
911 			double d = JRT.toDouble(arg);
912 			if (renderNonFinite('d', flags, width, d)) {
913 				return;
914 			}
915 
916 			boolean negative;
917 			String magnitude;
918 			if (arg instanceof Long || arg instanceof Integer || arg instanceof Short || arg instanceof Byte) {
919 				long v = ((Number) arg).longValue();
920 				negative = v < 0;
921 				magnitude = negative ? Long.toUnsignedString(-v) : Long.toString(v);
922 			} else if (d >= -TWO_POW_63 && d < TWO_POW_63) {
923 				long v = (long) d;
924 				negative = v < 0;
925 				magnitude = negative ? Long.toUnsignedString(-v) : Long.toString(v);
926 			} else {
927 				// Out of 64-bit range: print the full decimal expansion of the
928 				// (integral) double, like gawk.
929 				// The exact binary value of the double is intended: it makes rounding match gawk's C library.
930 				BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD
931 				negative = bi.signum() < 0;
932 				magnitude = bi.abs().toString();
933 			}
934 
935 			String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : "";
936 			appendInteger(sign, "", magnitude, flags, width, precision, isZeroMagnitude(magnitude));
937 		}
938 
939 		private void renderUnsignedInteger(char conversion, Flags flags, int width, int precision, Object arg) {
940 			double d = JRT.toDouble(arg);
941 			if (renderNonFinite(conversion, flags, width, d)) {
942 				return;
943 			}
944 
945 			int radix = conversion == 'o' ? 8 : conversion == 'u' ? 10 : 16;
946 			String magnitude;
947 			if (arg instanceof Long || arg instanceof Integer || arg instanceof Short || arg instanceof Byte) {
948 				magnitude = Long.toUnsignedString(((Number) arg).longValue(), radix);
949 			} else if (d >= -TWO_POW_63 && d < TWO_POW_63) {
950 				magnitude = Long.toUnsignedString((long) d, radix);
951 			} else {
952 				// The exact binary value of the double is intended: it makes rounding match gawk's C library.
953 				BigInteger bi = new BigDecimal(d).toBigInteger(); // NOPMD
954 				if (bi.signum() >= 0 && bi.compareTo(TWO_POW_64) < 0) {
955 					magnitude = bi.toString(radix);
956 				} else {
957 					// Out of the unsigned 64-bit range: fall back to %g
958 					// notation with the original sign, flags, precision, and
959 					// width, like gawk.
960 					renderFloat('g', flags, width, precision, Double.valueOf(d));
961 					return;
962 				}
963 			}
964 			if (conversion == 'X') {
965 				magnitude = magnitude.toUpperCase(Locale.ROOT);
966 			}
967 
968 			boolean zeroValue = d == 0;
969 			boolean zeroMagnitude = isZeroMagnitude(magnitude);
970 			int actualPrecision = precision;
971 			if (zeroMagnitude && precision == 0 && (flags.alternate || !zeroValue)) {
972 				// gawk prints "0" rather than nothing for a zero magnitude
973 				// with an explicit zero precision when the '#' flag is given,
974 				// or when the original value is nonzero and merely truncates
975 				// to zero.
976 				actualPrecision = 1;
977 			}
978 			// The '#' prefix depends on the original value, not the truncated
979 			// magnitude: gawk prints "0x0" for %#.0x with 0.1. For %o, gawk
980 			// always adds the alternate leading zero in addition to any
981 			// precision padding: %#.5o of 1 prints "000001".
982 			String prefix = "";
983 			if (flags.alternate && !zeroValue) {
984 				if (conversion == 'x') {
985 					prefix = "0x";
986 				} else if (conversion == 'X') {
987 					prefix = "0X";
988 				} else if (conversion == 'o') {
989 					prefix = "0";
990 				}
991 			}
992 			// gawk's ' flag groups decimal output only, never octal or
993 			// hexadecimal.
994 			Flags integerFlags = conversion == 'u' ? flags : flags.withoutGrouping();
995 			appendInteger("", prefix, magnitude, integerFlags, width, actualPrecision, zeroMagnitude);
996 		}
997 
998 		/**
999 		 * Applies precision, grouping, and width to an integer body and
1000 		 * appends it to the output.
1001 		 */
1002 		private void appendInteger(
1003 				String sign,
1004 				String prefix,
1005 				String magnitude,
1006 				Flags flags,
1007 				int width,
1008 				int precision,
1009 				boolean zeroMagnitude) {
1010 			String digits = magnitude;
1011 			if (precision == 0 && zeroMagnitude) {
1012 				// C: a zero value with an explicit zero precision prints no
1013 				// characters. gawk drops the sign flags as well.
1014 				appendPadded("", flags.leftJustify, false, width);
1015 				return;
1016 			}
1017 			if (precision > digits.length()) {
1018 				digits = zeros(precision - digits.length()) + digits;
1019 			}
1020 			if (flags.grouping) {
1021 				digits = groupDigits(digits);
1022 			}
1023 			String body = sign + prefix + digits;
1024 			if (width > body.length() && flags.zeroPad && !flags.leftJustify && precision < 0) {
1025 				// Zero padding goes between the sign/prefix and the digits.
1026 				out.append(sign).append(prefix);
1027 				out.append(zeros(width - body.length()));
1028 				out.append(digits);
1029 				return;
1030 			}
1031 			appendPadded(body, flags.leftJustify, false, width);
1032 		}
1033 
1034 		private void renderFloat(char conversion, Flags flags, int width, int precision, Object arg) {
1035 			double d = JRT.toDouble(arg);
1036 			if (renderNonFinite(conversion, flags, width, d)) {
1037 				return;
1038 			}
1039 			String magnitude = floatBody(conversion, flags, precision, d);
1040 			if (magnitude == null) {
1041 				return;
1042 			}
1043 			boolean negative = d < 0 || (d == 0 && Double.doubleToRawLongBits(d) != 0L);
1044 			String sign = negative ? "-" : flags.plusSign ? "+" : flags.spaceSign ? " " : "";
1045 			// The hexadecimal prefix of %a/%A stays ahead of any zero padding,
1046 			// like an integer prefix.
1047 			String prefix = "";
1048 			if (magnitude.startsWith("0x") || magnitude.startsWith("0X")) {
1049 				prefix = magnitude.substring(0, 2);
1050 				magnitude = magnitude.substring(2);
1051 			}
1052 			String full = sign + prefix + magnitude;
1053 			if (width > full.length() && flags.zeroPad && !flags.leftJustify) {
1054 				out.append(sign).append(prefix);
1055 				out.append(zeros(width - full.length()));
1056 				out.append(magnitude);
1057 				return;
1058 			}
1059 			appendPadded(full, flags.leftJustify, false, width);
1060 		}
1061 
1062 		/**
1063 		 * Renders the digits of a finite double for a floating-point
1064 		 * conversion, without sign and without width padding: the absolute
1065 		 * value is formatted and the caller applies the sign.
1066 		 */
1067 		private String floatBody(char conversion, Flags flags, int precision, double d) {
1068 			double abs = Math.abs(d);
1069 			switch (conversion) {
1070 			case 'f':
1071 			case 'F': {
1072 				int p = precision < 0 ? 6 : precision;
1073 				String s = decimalString(roundHalfEvenToScale(abs, p));
1074 				if (flags.alternate && p == 0) {
1075 					s = forceDecimalSeparator(s);
1076 				}
1077 				if (flags.grouping) {
1078 					s = groupDigits(s);
1079 				}
1080 				return s;
1081 			}
1082 			case 'e':
1083 			case 'E': {
1084 				int p = precision < 0 ? 6 : precision;
1085 				String s = scientific(abs, p);
1086 				if (flags.alternate && p == 0) {
1087 					int exponentStart = s.indexOf('e');
1088 					s = forceDecimalSeparator(s.substring(0, exponentStart)) + s.substring(exponentStart);
1089 				}
1090 				return conversion == 'E' ? s.toUpperCase(Locale.ROOT) : s;
1091 			}
1092 			case 'g':
1093 			case 'G': {
1094 				int p = precision < 0 ? 6 : precision == 0 ? 1 : precision;
1095 				String s = generalFloat(abs, p, flags.alternate, flags.grouping);
1096 				return conversion == 'G' ? s.toUpperCase(Locale.ROOT) : s;
1097 			}
1098 			case 'a':
1099 			case 'A':
1100 			default: {
1101 				// %a is C-library dependent in gawk; delegate to Java's
1102 				// hexadecimal float notation.
1103 				StringBuilder spec = new StringBuilder("%");
1104 				if (precision >= 0) {
1105 					spec.append('.').append(precision);
1106 				}
1107 				spec.append(conversion);
1108 				try {
1109 					String s = String.format(locale, spec.toString(), Double.valueOf(abs));
1110 					return s;
1111 				} catch (IllegalFormatException e) {
1112 					out.append(spec);
1113 					return null;
1114 				}
1115 			}
1116 			}
1117 		}
1118 
1119 		/** Formats {@code abs >= 0} in C's {@code %e} notation. */
1120 		private String scientific(double abs, int precision) {
1121 			BigDecimal mantissa;
1122 			int exponent;
1123 			if (abs == 0) {
1124 				mantissa = BigDecimal.ZERO.setScale(precision);
1125 				exponent = 0;
1126 			} else {
1127 				BigDecimal rounded = roundHalfEvenToSignificant(abs, precision + 1);
1128 				exponent = rounded.precision() - rounded.scale() - 1;
1129 				mantissa = rounded.movePointLeft(exponent).setScale(precision, RoundingMode.UNNECESSARY);
1130 			}
1131 			return decimalString(mantissa) + "e" + (exponent < 0 ? "-" : "+") + exponentDigits(Math.abs(exponent));
1132 		}
1133 
1134 		/** Formats {@code abs >= 0} in C's {@code %g} notation. */
1135 		private String generalFloat(double abs, int precision, boolean alternate, boolean grouping) {
1136 			if (abs == 0) {
1137 				return alternate ? forceDecimalSeparator("0") + zeros(precision - 1) : "0";
1138 			}
1139 			BigDecimal rounded = roundHalfEvenToSignificant(abs, precision);
1140 			int exponent = rounded.precision() - rounded.scale() - 1;
1141 			if (exponent >= -4 && exponent < precision) {
1142 				String s = decimalString(rounded.setScale(precision - 1 - exponent, RoundingMode.UNNECESSARY));
1143 				s = alternate ? forceDecimalSeparator(s) : stripTrailingFractionZeros(s);
1144 				// gawk groups %g in fixed notation, like %f, but never in
1145 				// exponential notation.
1146 				return grouping ? groupDigits(s) : s;
1147 			}
1148 			String mantissa = decimalString(
1149 					rounded.movePointLeft(exponent).setScale(precision - 1, RoundingMode.UNNECESSARY));
1150 			mantissa = alternate ? forceDecimalSeparator(mantissa) : stripTrailingFractionZeros(mantissa);
1151 			return mantissa + "e" + (exponent < 0 ? "-" : "+") + exponentDigits(Math.abs(exponent));
1152 		}
1153 
1154 		/**
1155 		 * Renders NaN and infinities for any numeric conversion, honoring the
1156 		 * sign flags and field width, and returns {@code true} when the value
1157 		 * was such a special value.
1158 		 */
1159 		private boolean renderNonFinite(char conversion, Flags flags, int width, double d) {
1160 			if (!Double.isNaN(d) && !Double.isInfinite(d)) {
1161 				return false;
1162 			}
1163 			String body;
1164 			if (Double.isNaN(d)) {
1165 				body = flags.plusSign ? "+nan" : flags.spaceSign ? " nan" : "nan";
1166 			} else if (d > 0) {
1167 				body = flags.plusSign ? "+inf" : flags.spaceSign ? " inf" : "inf";
1168 			} else {
1169 				body = "-inf";
1170 			}
1171 			if (isUpperCaseConversion(conversion)) {
1172 				body = body.toUpperCase(Locale.ROOT);
1173 			}
1174 			// The zero flag is ignored for non-finite values, like C.
1175 			appendPadded(body, flags.leftJustify, false, width);
1176 			return true;
1177 		}
1178 
1179 		/** Renders a {@link BigDecimal} using the locale's decimal separator. */
1180 		private String decimalString(BigDecimal value) {
1181 			String s = value.toPlainString();
1182 			return decimalSeparator == '.' ? s : s.replace('.', decimalSeparator);
1183 		}
1184 
1185 		/** Inserts locale grouping separators into the integer part of {@code s}. */
1186 		private String groupDigits(String s) {
1187 			int end = s.indexOf(decimalSeparator);
1188 			if (end < 0) {
1189 				end = s.length();
1190 			}
1191 			StringBuilder sb = new StringBuilder(s.length() + 8);
1192 			for (int i = 0; i < end; i++) {
1193 				sb.append(s.charAt(i));
1194 				int remaining = end - 1 - i;
1195 				if (remaining > 0 && remaining % 3 == 0 && isAsciiDigit(s.charAt(i))) {
1196 					sb.append(groupingSeparator);
1197 				}
1198 			}
1199 			sb.append(s, end, s.length());
1200 			return sb.toString();
1201 		}
1202 
1203 		/**
1204 		 * Appends the locale decimal separator when {@code s} has none, as
1205 		 * the '#' flag requires for {@code %g} results without fractional
1206 		 * digits.
1207 		 */
1208 		private String forceDecimalSeparator(String s) {
1209 			return s.indexOf(decimalSeparator) < 0 ? s + decimalSeparator : s;
1210 		}
1211 
1212 		private String stripTrailingFractionZeros(String s) {
1213 			if (s.indexOf(decimalSeparator) < 0) {
1214 				return s;
1215 			}
1216 			int end = s.length();
1217 			while (end > 0 && s.charAt(end - 1) == '0') {
1218 				end--;
1219 			}
1220 			if (end > 0 && s.charAt(end - 1) == decimalSeparator) {
1221 				end--;
1222 			}
1223 			return s.substring(0, end);
1224 		}
1225 
1226 		private void appendPadded(String body, boolean leftJustify, boolean zeroPad, int width) {
1227 			// The field width counts characters (code points), so that a
1228 			// supplementary character fills one column, not two.
1229 			int bodyLength = body.codePointCount(0, body.length());
1230 			if (width <= bodyLength) {
1231 				out.append(body);
1232 				return;
1233 			}
1234 			int padLength = width - bodyLength;
1235 			if (leftJustify) {
1236 				out.append(body);
1237 				appendSpaces(padLength);
1238 			} else if (zeroPad) {
1239 				out.append(zeros(padLength)).append(body);
1240 			} else {
1241 				appendSpaces(padLength);
1242 				out.append(body);
1243 			}
1244 		}
1245 
1246 		private void appendSpaces(int count) {
1247 			out.append(repeat(' ', count));
1248 		}
1249 	}
1250 
1251 	/**
1252 	 * Rounds the exact binary value of {@code abs} to {@code scale} fractional
1253 	 * digits with {@link RoundingMode#HALF_EVEN}, producing exactly the same
1254 	 * result as {@code new BigDecimal(abs).setScale(scale, HALF_EVEN)}.
1255 	 * <p>
1256 	 * When the shortest decimal representation of {@code abs} provably rounds
1257 	 * to the same value (see {@link #roundsLikeExact}), it is used instead of
1258 	 * the exact expansion, which is much cheaper for values like {@code 0.1}
1259 	 * whose exact binary expansion has dozens or hundreds of digits.
1260 	 * </p>
1261 	 */
1262 	private static BigDecimal roundHalfEvenToScale(double abs, int scale) {
1263 		BigDecimal shortest = shortestDecimal(abs);
1264 		if (shortest != null && roundsLikeExact(abs, shortest, shortest.scale() - scale, -scale)) {
1265 			return shortest.setScale(scale, RoundingMode.HALF_EVEN);
1266 		}
1267 		// The exact binary value of the double is intended: it makes rounding match gawk's C library.
1268 		return new BigDecimal(abs).setScale(scale, RoundingMode.HALF_EVEN); // NOPMD
1269 	}
1270 
1271 	/**
1272 	 * Rounds the exact binary value of {@code abs} to {@code digits}
1273 	 * significant digits with {@link RoundingMode#HALF_EVEN}, producing a
1274 	 * result numerically equal to
1275 	 * {@code new BigDecimal(abs).round(new MathContext(digits, HALF_EVEN))}.
1276 	 */
1277 	private static BigDecimal roundHalfEvenToSignificant(double abs, int digits) {
1278 		if (digits > 0) {
1279 			BigDecimal shortest = shortestDecimal(abs);
1280 			if (shortest != null) {
1281 				// Decimal exponent of the rounding unit: eS - digits + 1,
1282 				// where eS = precision - scale - 1.
1283 				int unitExponent = shortest.precision() - shortest.scale() - digits;
1284 				if (roundsLikeExact(abs, shortest, shortest.precision() - digits, unitExponent)) {
1285 					return shortest.round(new MathContext(digits, RoundingMode.HALF_EVEN));
1286 				}
1287 			}
1288 		}
1289 		// The exact binary value of the double is intended: it makes rounding match gawk's C library.
1290 		return new BigDecimal(abs).round(new MathContext(digits, RoundingMode.HALF_EVEN)); // NOPMD
1291 	}
1292 
1293 	/**
1294 	 * Returns the shortest decimal representation of {@code abs}, or
1295 	 * {@code null} when {@code abs} is zero, subnormal, or non-finite (those
1296 	 * always take the exact path).
1297 	 */
1298 	private static BigDecimal shortestDecimal(double abs) {
1299 		if (!(abs >= Double.MIN_NORMAL) || abs > Double.MAX_VALUE) {
1300 			return null;
1301 		}
1302 		return new BigDecimal(Double.toString(abs));
1303 	}
1304 
1305 	/**
1306 	 * Decides whether rounding the shortest decimal representation of
1307 	 * {@code abs} at the position of the unit {@code 10^unitExponent}
1308 	 * provably yields the same value as rounding the exact binary expansion.
1309 	 * <p>
1310 	 * The exact value differs from the shortest representation by at most
1311 	 * half an ulp (the shortest representation parses back to the same
1312 	 * double). Requiring the rounding unit to exceed 100 ulps and the first
1313 	 * dropped digit to be away from the halfway point guarantees that no
1314 	 * rounding boundary — and no power of ten, where the two values could
1315 	 * disagree on the position of the leading digit — lies between the two
1316 	 * values, so both round identically.
1317 	 * </p>
1318 	 *
1319 	 * @param abs the positive, normal double being rounded
1320 	 * @param shortest its shortest decimal representation
1321 	 * @param remainderDigits how many digits of {@code shortest} fall below
1322 	 *        the rounding unit
1323 	 * @param unitExponent decimal exponent of the rounding unit
1324 	 * @return whether the shortest representation can be rounded instead of
1325 	 *         the exact expansion
1326 	 */
1327 	private static boolean roundsLikeExact(double abs, BigDecimal shortest, int remainderDigits, int unitExponent) {
1328 		if (unitExponent < -307 || unitExponent > 308) {
1329 			// Math.pow(10, unitExponent) would leave the normal double range.
1330 			return false;
1331 		}
1332 		if (Math.ulp(abs) * 100.0 >= Math.pow(10.0, unitExponent)) {
1333 			return false;
1334 		}
1335 		if (remainderDigits <= 0) {
1336 			// The shortest representation is an exact multiple of the
1337 			// rounding unit; the exact value rounds to it.
1338 			return true;
1339 		}
1340 		String digits = shortest.unscaledValue().toString();
1341 		if (remainderDigits > digits.length()) {
1342 			// The dropped fraction starts with a zero digit: far from the
1343 			// halfway point.
1344 			return true;
1345 		}
1346 		// With the unit at least 200 times the maximum difference between the
1347 		// exact value and its shortest representation, only a first dropped
1348 		// digit of 4 or 5 can put the two values on opposite sides of a
1349 		// rounding boundary.
1350 		char first = digits.charAt(digits.length() - remainderDigits);
1351 		return first != '4' && first != '5';
1352 	}
1353 
1354 	/** Renders an exponent value with at least two digits, like C. */
1355 	private static String exponentDigits(int exponent) {
1356 		String digits = Integer.toString(exponent);
1357 		return digits.length() < 2 ? "0" + digits : digits;
1358 	}
1359 
1360 	private static boolean isUpperCaseConversion(char conversion) {
1361 		return conversion == 'X' || conversion == 'E' || conversion == 'F' || conversion == 'G' || conversion == 'A';
1362 	}
1363 
1364 	private static boolean isZeroMagnitude(String magnitude) {
1365 		for (int i = 0; i < magnitude.length(); i++) {
1366 			if (magnitude.charAt(i) != '0') {
1367 				return false;
1368 			}
1369 		}
1370 		return true;
1371 	}
1372 
1373 	private static boolean isAsciiDigit(char c) {
1374 		return c >= '0' && c <= '9';
1375 	}
1376 
1377 	/**
1378 	 * Parses a run of decimal digits, clamping absurd widths, precisions, and
1379 	 * argument positions to {@link Integer#MAX_VALUE} instead of failing the
1380 	 * way {@link Integer#parseInt(String)} would.
1381 	 */
1382 	private static int parseInt(String s, int from, int to) {
1383 		try {
1384 			return Integer.parseInt(s.substring(from, to));
1385 		} catch (NumberFormatException e) {
1386 			return Integer.MAX_VALUE;
1387 		}
1388 	}
1389 
1390 	private static String zeros(int count) {
1391 		return repeat('0', count);
1392 	}
1393 
1394 	private static String repeat(char c, int count) {
1395 		if (count <= 0) {
1396 			return "";
1397 		}
1398 		char[] chars = new char[count];
1399 		Arrays.fill(chars, c);
1400 		return new String(chars);
1401 	}
1402 }