Project

General

Profile

Bug #10075

Number format validation issues

Added by Vladimir Tsichevski about 1 year ago. Updated 6 months ago.

Status:
WIP
Priority:
Normal
Assignee:
Vladimir Tsichevski
Target version:
-
Start date:
Due date:
% Done:

0%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
15938
version_resolved:
reviewer:
production:
No
env_name:
topics:

NumberFormatValidationTest.cls (11.9 KB) Vladimir Tsichevski, 08/25/2025 01:56 PM


Related issues

Related to User Interface - Bug #7794: FILL-IN: editing numbers issues WIP
Related to User Interface - Bug #10442: FILL-IN for Decimal Variable: Issues with Screen Value Display Rejected
Related to User Interface - Bug #11112: Numeric FILL-IN Format Validation – GUI Rendering Failure New
Blocked by Base Language - Bug #10471: Incorrect Integer Formatting as Legacy Time Internal Test

History

#1 Updated by Vladimir Tsichevski about 1 year ago

Let’s explore this 4GL procedure example:

  SESSION:SET-NUMERIC-FORMAT(",",".");

  DEFINE VARIABLE d AS DECIMAL NO-UNDO.
  MESSAGE STRING(d, "abc->>,>>9.99<<,<<qwe abc").

This program converts a decimal value to a string using the specified format.

In OE, the program outputs abc0.00qwe abc (the value prefixed with 6 spaces). In FWD, it terminates with an error: ** Character number 16 of format abc->>,>>9.99<<,<<qwe abc is invalid. (22). This means the comma (,) at position 16 is invalid according to FWD’s logic, as it follows at least one "right balance" character (<).

The issue involves the NumberType.processDigits(..) method.

The problems:

  1. This format is valid in OE and successfully formats values.
  2. The format passes validation in other FWD components, such as when assigned to a FILL-IN widget variable, suggesting multiple validation points in FWD handle it differently.
  3. Format validation is an independent function and can be and should be performed before any number is formatted with it.

#2 Updated by Vladimir Tsichevski 11 months ago

  • Related to Bug #7794: FILL-IN: editing numbers issues added

#3 Updated by Vladimir Tsichevski 11 months ago

  • Status changed from New to WIP
  • Assignee set to Vladimir Tsichevski

Number Format Specification

This document describes how numbers are structured with optional text around them. The format includes an optional prefix, an optional sign ("+" or "–") before the number, the number itself (with an integer part and an optional fractional part), an optional sign or closing parenthesis after the number, and an optional suffix.

This specification is implementation-agnostic.

NOTE:
This specification covers validation of format strings only (structure and static rules).
Runtime formatting behavior (padding, rounding, comma emission/collapse, etc.) is
defined in a separate document.

Format Structure

A number format consists of the following components, in order:

1. Prefix (Optional)

Definition: Any text before the number, composed of characters excluding digits, dots, asterisks, greater-than signs, and plus/minus signs.

NOTE:
A right parenthesis (")") may appear in the prefix as a literal; it does not act
as a closing sign unless it appears after the number part.

Examples:
  • abc,
  • ((<
  • ,,(
  • (()
  • abc — spaces are allowed in the prefix.

2. Sign Before the Number (Optional)

Definition: A "+" or "–" character immediately following the prefix.

Examples:
  • +
  • -
  • None

3. Number Part

Definition: The core number, consisting of either:
  • An integer part with an optional fractional part after a dot, or
  • A dot followed directly by a fractional part (no integer part).

Integer Part

Definition: Sequences composed of Z, z, *, >, or 9. The allowed forms are:
  • One or more >, optionally followed by more > or commas, then optionally followed by one or more Z, z, * or commas, and finally optionally 9 or commas.
  • One or more Z, z, or *, optionally followed by more Z, z, * or commas, and finally optionally 9 or commas.
  • One or more 9, optionally followed by more 9 or commas.

Ordering constraint: If > appears, all > must come before any Z, z, *, or 9 placeholders.

Rules ensure:
  1. At least one placeholder from Z, z, *, >, or 9 must be present.
  2. The integer part cannot start with a comma.
Examples:
  • Z*9
  • >>,
  • 9,,
  • Z,Z
Invalid examples:
  • ,Z9 — starts with a comma.

Dot

Definition: A single dot ("."), separating the integer and fractional parts, or preceding the fractional part if there is no integer part.

Fractional Part (Optional)

Definition: If present, must start with either 9 or <. The forms are:
  • 9 followed by more 9 or commas, then optionally < or commas.
  • < followed by more < or commas.
Examples:
  • 9,,
  • 9,<
  • <,,
  • None (e.g., 9. — dot with no fractional part)
Invalid examples:
  • ,9 — starts with a comma.

4. Sign or Closing Parenthesis After the Number (Optional)

Definition: A "+", "–", or single ")" immediately after the number part.
If "(" appears in the prefix and a suffix exists, exactly one ")" must appear here.
If there is no suffix, the ")" is optional.

Examples:
  • +
  • -
  • )
  • None

5. Suffix (Optional)

Definition: Any text after the number, excluding digits, commas, asterisks, greater-than signs, plus/minus signs, and opening parentheses.
If present, the suffix must contain at least one character and cannot start with a dot ("."). Dots are allowed after the first character.

Examples:
  • xy
  • qwe
  • x.y
  • y.z — starts with "y" and includes a dot.
  • 99.99a, — invalid: commas are not allowed in the suffix.
  • 99.99, — valid if the comma is part of the fractional part; otherwise invalid.

Extra Rules Checked Later

These rules are not part of the initial structural definition but are validated after parsing.

1. Length of Less-Than Span Must Be Equal to or Less Than Greater-Than Span

The length of the Less-Than Span must be equal to or less than the length of the Greater-Than Span.

Less-Than Span: A maximal sequence starting with "<" and containing only "<" and comma (",") characters.
Greater-Than Span: A maximal sequence starting with ">" and containing only ">" and comma (",") characters.

Error: If the Less-Than Span is longer, error 22 is raised:
"Character number [position] of format [format text] is invalid."

Examples:
  • Okay: ->>,>,>,,>,,9.<,<,,<,,<<< — both spans are length 12.
  • Okay: abc>9.<xy — both spans are length 1.
  • Not okay: abc9.<,,<xy — Greater-Than Span length 0, Less-Than Span length 4.

2. Only One Sign Allowed

  1. Only one sign specification is permitted.
  2. Valid forms:
    1. A single "+" before or after the number part.
    2. A single "–" before or after the number part.
    3. One or more "(" in the prefix, with a ")" after the number part (required if there is a suffix; optional otherwise).
  3. No other combinations of "+", "–", or "(" are valid.
  4. A ")" after the number part is required only when "(" appears in the prefix and a suffix exists.

Error: If more than one sign specification is found, error 22 is raised.

Examples:
  • Okay: ((abc9) — multiple "(" in prefix, one ")" after number, no suffix.
  • Okay: ((abc9 — multiple "(" in prefix, no closing parenthesis, no suffix.
  • Okay: +abc9 — single "+" before number.
  • Okay: abc9- — single "–" after number.
  • Not okay: (abc9-xy — "(" in prefix and "–" after number.
  • Not okay: +abc9-xy — "+" before and "–" after number.

3. Must Have at Least One Digit Placeholder

Definition: The number part must include at least one placeholder character: 9, <, *, >, z, or Z.

Check: Performed after the format is assembled.

Error: If none are found, error 148 is raised:
"Numeric format [format text] provides for no digits."

Examples:
  • Okay: abc9xy — contains a "9".
  • Not okay: abc,,xy — no digit placeholders.

Examples

Formats That Fit All Rules

  • abc,9xy
    • Prefix: abc,
    • Number: 9
    • Suffix: xy
  • ((>Z*9.<)xy
    • Prefix: ((
    • Number: >Z*9.<
    • Closing parenthesis: )
    • Suffix: xy
  • (9
    • Prefix: (
    • Number: 9
    • No suffix, no closing parenthesis (allowed)
  • (9)
    • Prefix: (
    • Number: 9
    • Closing parenthesis: )
    • No suffix
  • .9
    • Prefix: None
    • Number: .9
  • 9.
    • Number: 9. — integer part "9", dot, no fractional part.
  • ,Z9
    • Prefix: ,
    • Number: Z9

Formats That Don’t Fit

  • 9., — fractional part starts with a comma.
  • 9..xy — suffix starts with a dot.
  • abc.,9 — dot in prefix (invalid; dots are reserved for the number part).
  • +9- — two sign specifications.
  • abc9.<,,<xy — Less-Than Span longer than Greater-Than Span.
  • (abc9-xy — "(" in prefix combined with "–" after number.
  • abc.Z — fractional part starts with invalid "Z".
  • .abc,,9xy — dot parsed as decimal delimiter; remainder treated as suffix, but commas not allowed in suffix.

#4 Updated by Vladimir Tsichevski 11 months ago

I've updated the specification in #10075-3.

#5 Updated by Vladimir Tsichevski 11 months ago

Issue 2

Let’s examine this 4GL procedure example:

SESSION:NUMERIC-FORMAT = "American".

DEFINE VARIABLE d AS DECIMAL NO-UNDO.

d = 123456.78901.
MESSAGE 1 STRING(d, "abc->>,>>9.99<<<<qwe abc").
d = ?.
MESSAGE 2 STRING(d, ".").
d = 0.
MESSAGE 3 STRING(d, ".").

In the first MESSAGE call, the number 123456.78901 is formatted using the abc->>,>>9.99<<<<qwe abc format, which is supported by both OE and FWD. The expected output is abc123,456.79qwe abc, but FWD produces ab123,456.79we abc instead.

The second MESSAGE call converts an unknown value (represented by ?) to a string using an invalid format (lacking digit placeholders). This executes without errors in both OE and FWD, suggesting that the conversion occurs before format validity is verified for unknown values.

The third MESSAGE call converts a zero value to a string using the same invalid format. In OE, this triggers error 148, suppressing the argument display. In FWD, no error is raised, and the dot (.) is displayed.

#6 Updated by Vladimir Tsichevski 11 months ago

  • Related to Bug #10442: FILL-IN for Decimal Variable: Issues with Screen Value Display added

#7 Updated by Vladimir Tsichevski 11 months ago

The ABLUnit test class NumberFormatValidationTest.cls has been attached.

#8 Updated by Greg Shah 11 months ago

Please commit all tests into the testcases project.

#9 Updated by Vladimir Tsichevski 11 months ago

Greg Shah wrote:

Please commit all tests into the testcases project.

1. I'm uncertain about the appropriate location within the project to place this test class.
2. I cannot test the result in testcases because I am unable to execute ant tasks in the project (see #7143-1679) :-(

#11 Updated by Vladimir Tsichevski 11 months ago

Greg Shah wrote:

See Format String Tests.

This page points to a single test class designed to evaluate the STRING function across all data types. My test class, however, focuses on numeric format validation and does not assess the formatting output. I conclude that format validation and formatting represent two distinct concerns, with format validation always occurring prior to applying the format.

#12 Updated by Greg Shah 11 months ago

It is still the place to put the test code. I expect many more tests to be added over time.

#13 Updated by Vladimir Tsichevski 11 months ago

Greg Shah wrote:

It is still the place to put the test code. I expect many more tests to be added over time.

Do you mean the TestString.cls file?

#14 Updated by Greg Shah 11 months ago

I mean in that set of tests. It probably won't be in that same class. Over time we will probably refactor the existing ABLUnit tests to be split up into separate methods, which may mean more classes too. My point is that the test set is not complete.

#15 Updated by Vladimir Tsichevski 11 months ago

Greg Shah wrote:

I mean in that set of tests. It probably won't be in that same class. Over time we will probably refactor the existing ABLUnit tests to be split up into separate methods, which may mean more classes too. My point is that the test set is not complete.

I successfully executed TestString.cls, but 68 out of 96 total tests failed. It’s unclear whether FWD is responsible or if the tests were imperfect. Regardless, debugging my specific issue in this state would be challenging. Therefore, I’ve decided to place NumberFormatValidationTest.cls in the same directory as TestString.cls until all tests in this class pass successfully.

#16 Updated by Greg Shah 11 months ago

Therefore, I’ve decided to place NumberFormatValidationTest.cls in the same directory as TestString.cls

This is what I was suggesting all along. We DO NOT need to put all tests in one class. That makes no sense.

until all tests in this class pass successfully.

No, this is permanent. I don't want all code in one test class.

#17 Updated by Vladimir Tsichevski 11 months ago

Greg Shah wrote:

Therefore, I’ve decided to place NumberFormatValidationTest.cls in the same directory as TestString.cls

This is what I was suggesting all along. We DO NOT need to put all tests in one class. That makes no sense.

until all tests in this class pass successfully.

No, this is permanent. I don't want all code in one test class.

We are fully in consensus!

#18 Updated by Vladimir Tsichevski 11 months ago

NumberFormatValidationTest and NumberFormattingTest classes added to the testcases at rev. 1802.

#19 Updated by Vladimir Tsichevski 6 months ago

  • Related to Bug #11112: Numeric FILL-IN Format Validation – GUI Rendering Failure added

#20 Updated by Vladimir Tsichevski 6 months ago

I rebased 10075a to latest trunk. The rev. is 16354 now.

#21 Updated by Vladimir Tsichevski 6 months ago

  • Blocked by Bug #10471: Incorrect Integer Formatting as Legacy Time added

#22 Updated by Vladimir Tsichevski 6 months ago

Another related issue (initially reported in #10471-12):

The following statements throw error 22 ("Character number 2 of format is invalid") in OE, at position 2:

STRING(0, "+h").
STRING(0, "+hh").
STRING(0, "+hh:").
STRING(0, "+hh:m").

However, in FWD, these statements do not throw any error.

#23 Updated by Vladimir Tsichevski 6 months ago

Updated version of #10075-3.

Number Format Specification

This document describes the structure of number formats used for formatting and validation of numeric values, including optional text around the number.

This specification is implementation-agnostic and focuses on format string validation (static structure and rules).
Runtime formatting behavior (padding, rounding, comma emission/collapse, etc.) is defined in a separate document.

Format Structure

A number format consists of the following components, in strict order:

1. Prefix (Optional)

Definition: Any text before the number part, composed of characters excluding digits (0–9), dots (.), asterisks (*), greater-than signs (>), plus/minus signs (+, -), and Z/z.

Note: A right parenthesis ) may appear in the prefix as a literal; it does not act as a closing sign unless it appears after the number part.

Examples:

  • abc,
  • ((<
  • ,,(
  • (()
  • " abc" — spaces are allowed.

2. Sign Before the Number (Optional)

Definition: A single + or - character immediately following the prefix.

Examples:

  • +
  • -
  • None

3. Number Part

Definition: The core numeric portion, consisting of either:
  • An integer part optionally followed by a dot and fractional part, or
  • A dot directly followed by a fractional part (no integer part).

Integer Part

Definition: A sequence composed of Z, z, *, >, 9, or commas ,.

Rules:

  • All > must appear before any Z, z, *, 9, or comma.
  • At least one digit placeholder (Z, z, *, >, 9) must be present.
  • Cannot start with a comma.

Examples:

  • Z*9
  • >>,
  • 9,,
  • Z,Z
Invalid Examples:
  • ,Z9 — starts with a comma.

Dot

Definition: A single dot . separating integer and fractional parts, or starting the fractional part if no integer part exists.

Fractional Part (Optional)

Must start with 9 or <.

Forms:

  • 9 followed by 9 or commas, optionally followed by < characters
  • < characters only (implies optional fractional digits)

Examples:

  • 9,,
  • 9,<
  • <,,

4. Closing Sign / Parenthesis (Optional)

Only allowed when prefix contains matching opening parenthesis or when using + / - after number.

5. Suffix (Optional)

Any text after the number part / closing sign, excluding certain control characters in some contexts.

Validation Rules

1. Greater-Than / Less-Than Symmetry

Length of Less-Than Span must be equal to or less than length of Greater-Than Span.

Error: If violated, error 22 is raised at the offending character:
"Character number [position] of format [format text] is invalid."

Examples:

  • Okay: ->>,>,>,,>,,9.<,<,,<,,<<< — both spans are length 12.
  • Okay: abc>9.<xy — both spans are length 1.
  • Not okay: abc9.<,,<xy — Greater-Than Span length 0, Less-Than Span length 4. Character at position 6 (<) is invalid.
  • Not okay: abc>9.<,,<xy — Greater-Than Span length 1, Less-Than Span length 4. Character at position 8 (,@) is invalid.

2. Only One Sign Specification Allowed

Valid forms:

  • Single + before or after number.
  • Single - before or after number.
  • One or more ( in prefix + optional ) after number (required if suffix exists).

Rule: No mixing of signs (e.g. no + and -, no ( and -, etc.).

Error: Multiple signs → error 22.

Examples:

  • OK: ((abc9)xy
  • OK: +abc9
  • OK: abc9-
  • Not OK: (abc9-xy
  • Not OK: +abc9-xy

3. Must Have at Least One Digit Placeholder

Rule: Number part must contain at least one 9, <, *, >, z, or Z.

Error: If missing → error 148:
"Numeric format [format text] provides for no digits."

Examples:

  • OK: abc9xy
  • Not OK: abc,,xy

Valid Format Examples

  • abc,9xy
    • Prefix: abc,
    • Number: 9
    • Suffix: xy
  • ((>Z*9.<)xy
    • Prefix: ((
    • Number: >Z*9.<
    • Closing parenthesis: )
    • Suffix: xy
  • (9
    • Prefix: (
    • Number: 9
  • .9
    • Number: .9
  • 9.
    • Number: 9.

Invalid Format Examples

  • 9., — fractional part starts with comma.
  • 9..xy — suffix starts with dot.
  • abc.,9 — dot in prefix.
  • +9- — two signs.
  • abc9.<,,<xy — Less-Than Span longer than Greater-Than Span.
  • (abc9-xy( + -.
  • abc.Z — fractional part starts with Z.
  • .abc,,9xy — dot as delimiter; suffix contains commas.

#24 Updated by Vladimir Tsichevski 6 months ago

Numeric Formatting Logic Specification

This document describes the OE runtime formatting logic for numeric values.
The logic is currently implemented in the NumericFormat Java class as part of a standalone proof-of-concept for numeric formatting.
It is intended to replace the current FWD implementation, which has known issues.

Limits and Compatibility

  • In OE, the DECIMAL type is effectively limited to 10 fractional digits in most operations
    (e.g. 0.00000000005 rounds to 0.0000000001).
  • The current Java implementation does not enforce this limit — it assumes the input Number already reflects 4GL rounding behavior.
  • Rounding and truncation follow Java DecimalFormat rules with RoundingMode.HALF_UP, which is consistent with 4GL behavior.

Example of 4GL fractional digit limitation:

  MESSAGE 0.00000000005.                 // displays 0.0000000001
  MESSAGE 0.00000000005 + 0.00000000005. // displays 0.0000000002

Common Steps

  1. Parse the format string
    • The input format string is validated and parsed by NumericFormatParser.
    • This step ensures strict conformance to the rules defined in Number Format Specification.
    • The parser produces an immutable NumericFormat instance.
    • The instance contains all parsed format data used during formatting.
    • It is thread-safe and reusable across multiple formatting calls.
  2. Format the number

    Formatting is performed by the method NumericFormat.format(Number, boolean, boolean).

Input Parameters

  • value: the number to format (java.lang.Number)
  • rightAligned: if true, the result is left-padded with spaces to the maximum format length
  • allowSignReuse: if true, the sign position may be reused for non-negative values

Output

  • A formatted string that conforms to the specification.
  • Throws Numbered Exception (error 74) if the value cannot be represented within the format constraints.

Sign Reuse (Space-for-Sign Optimization)

This optimization is enabled only when all of the following conditions are met:

  1. The format contains a + or - sign placeholder before the numeric part.
  2. The value being formatted is non-negative.
  3. allowSignReuse is true.

When active:

  • The sign character is omitted.
  • An extra digit from the value may occupy the sign's position (especially useful with +).

The + sign position may be reused to display an extra digit only when the value exceeds the normal digit capacity.

Note:
When editing a FILL-IN field interactively, sign reuse is disabled if the last keystroke causes the formatted value (without reuse) to fully occupy the available format length without leading spaces.
This behavior is preserved in this implementation by providing the allowSignReuse flag.

Examples:

Formatting with the format "abc->>,>>9":

  1. For the value 12345: # With sign reusing: " abc12,345". The minus (-) character is omitted, and the first digit ("1") occupies its position. # Without sign reusing: " abc1,234". The minus (-) character is omitted, but a space is added instead, and the value is truncated from the right to fit into the format length.
  2. For the value 1234:
    1. With sign reusing: " abc1,234". The minus (-) character is omitted, and a space is added instead, and the value fits into the format length.
    2. Without sign reusing: " abc1,234". The minus (-) character is omitted, and a space is added instead, and the value fits into the format length.

Formatting with the format "abc+>>,>>9":

  1. For the value 12345: # With sign reusing: "abc+12,345". The plus (+) character is omitted, and the first digit ("1") occupies its position. # Without sign reusing: "abc+ 1,234". The plus (+) character is omitted, and a space is added instead, and the value is truncated from the right to fit into the format length.
  2. For the value 1234:
    1. With sign reusing: "abc+ 1,234". The plus (+) character is omitted, and a space is added instead, and the value fits into the format length.
    2. Without sign reusing: "abc+ 1,234". The plus (+) character is omitted, and a space is added instead, and the value fits into the format length.

Credit/Debit Sign Placeholders (CR/DB/DR)

Numeric formats support three ways to display the sign of a value:

  1. Using + or - placeholders, either before or after the numeric part
    (e.g. ->>9.99, +>>9.99, >>9.99+, >>9.99-).
  2. Using opening and closing parentheses
    (e.g. ((>>9.99), (>>9.99)).
  3. Using CR (credit), DB or DR (debit) as exact suffixes
    (e.g. >>9.99DB, +>>9.99CR, >>9.99Dr, >>9.99dB).

Recognition of CR/DB/DR suffixes

The CR, DB, and DR modifiers:

  • Affect formatting only — they are not validated during format parsing.
  • Must appear exactly as the suffix (nothing after them).

    → Recognized in >>9.99dR
    → Ignored in >>9.99abcdR, >>9.99dRabc, >>9.99dRdR.

  • Are case-insensitive during recognition (DR, Dr, dR, dr are equivalent).
  • Are case-preserving in the output (the case you write is the case you get).

Formatting behavior with CR/DB/DR

  • For negative values: the suffix is inserted exactly as written in the format, preserving case

    (e.g. -1 with >>9.99dR" 1.00dR").

  • For positive values: the suffix is replaced by two spaces

    (these spaces are not trimmed from the output).

  • The three modifiers (CR, DB, DR) are interchangeable — all indicate a negative value.
  • They can coexist with other sign placeholders:

    Pre-numeric sign (+/-): both are applied
    (e.g. -1 with ->>9.99dR" -1.00dR").

    Post-numeric sign (+/-) or parentheses: the CR/DB/DR suffix is ignored
    (e.g. 1 with >>9.99-dR" 1.00dR";
    -1 with (>>9.99)CR"(123.45)").

  • CR/DB/DR cannot be used for space reuse (sign position reuse)
    (unlike ordinary - or + placeholders).

    Example (fails):
    STRING(12345.456, ">>>9.99<CR")Value 12345.46 cannot be displayed using >>>9.99<CR.

    Example (succeeds):
    STRING(12345.456, "->>>9.99<")"12345.46" (ordinary sign is reused).

Formatting Workflow

The formatting algorithm proceeds in the following steps:

  1. Pre-format raw value
    • Convert the input value to a string using a pre-configured DecimalFormat (dot separator, no scientific notation, up to 340 fractional digits).
    • Result: rawSigned (signed string).
    • Set isNegative flag if the value is negative (starts with minus sign).

    Note:
    This step allows uniform processing of all kinds of Java Number input values, e.g. Integer, Double, BigDecimal.

  2. Validate sign for negative values

    If the value is negative and the format does not allow signs (neither opening nor closing nor extra nor CR/DB/DR), throw error 74.

  3. Process prefix

    These characters come before the number and optional left sign specification.
    When formatting, the prefix is copied to output literally.

    • Copy prefix characters to output.
    • Replace ( with ( (if negative) or space (if positive).

    Examples:

    1. 1234 formatted with the "abc9999" format gives: "abc1234". The "a", "b" and "c" here are fill characters, which form the prefix and are copied verbatim.
    2. 1234 formatted with the "ab(c9999)" format gives: "ab c1234 ". The "a", "b" and "c" here are fill characters, which, together with the "(", form the prefix part. The "(" and ")" are not fill characters and form sign specification.
    3. -1234 formatted with the "(a(b(c9999)xy" format gives: "(a(b(c1234)xy".
    4. -1234 formatted with the "ab(c9999" format gives: "ab(c1234". Note: in this case we have no suffix part, so the closing parenthesis is optional, while the format "ab(c1234xy" has the suffix part "xy" and no closing parenthesis, so it is invalid.
  4. Process opening sign (if present)
    • Append - (negative) or space (otherwise).
    • Compute reusableSignIndex (position after prefix) if: * allowSignReuse is true and * value is non-negative

    Examples:

    1. 1234 formatted with the "+9999" format gives: "+1234".
    2. -1234 formatted with the "+9999" format gives: "-1234".
    3. 1234 formatted with the "9999+" format gives: "1234+".
    4. -1234 formatted with the "9999+" format gives: "1234-".
    5. 1234 formatted with the "-9999" format gives: " 1234".
    6. -1234 formatted with the "-9999" format gives: "-1234".
    7. 1234 formatted with the "9999-" format gives: "1234 ".
    8. -1234 formatted with the "9999-" format gives: "1234-".
  5. Count raw integer digits

    Count digits before the decimal point in rawSigned as rawIntegerDigitsAvailable.

  6. Handle special case: zero integer part and no integer placeholders

    If maximumIntegerDigits == 0 and integer part of raw value is zero:

    • Set allowedFractionalDigits = minimumFractionalDigits
    • Pre-format value using minimumFractionalDigits
    • If rounding causes carry-over (e.g. 0.9951.00), output '1' if sign reuse is allowed, else throw error 74.

    Examples:

    1. 0.0 formatted with ".99" gives: ".00".
    2. 0.99 formatted with ".99" gives: ".99".
    3. 0.995 formatted with "-.99" gives "1.00". Sign is reused here, so formatting is successful.
    4. 9.995 formatted with "-9.99" gives: "10.00". Sign is reused here, so formatting is successful.
    5. 0.999 formatted with ".99" throws error 74: value rounds to 1 and does not fit after rounding.
    6. 0.995 formatted with ".99" throws error 74: value rounds to 1 and does not fit after rounding.
  7. Normal case
    • Check if value fits: rawIntegerDigitsAvailable ≤ maximumIntegerDigits + (reusableSignIndex ≥ 0 ? 1 : 0)
      → throw error 74 if not
    • Compute unusedIntegerDigits = maximumIntegerDigits - rawIntegerDigitsAvailable
    • Compute allowedFractionalDigits = minimumFractionalDigits + min(optionalFractionalDigits, unusedIntegerDigits)
    • Pre-format value using minimumFractionalDigits and allowedFractionalDigits
    • If integer part exists in format: * Extract integer part from pre-formatted string * Format it using formatPart() (reversed) * Append to output * Throw error 74 if formatting fails (value does not fit)

    Examples:

    1. 1234 formatted with "999999" gives: "001234" (padded with leading zeroes).
    2. 0.1234 formatted with "9.999999" gives: "0.123400" (padded with trailing zeroes).
    3. -1234 formatted with "-999999" gives: "-001234" (padded with leading zeroes).
    4. -0.1234 formatted with "-0.999999" gives: "-0.123400" (padded with trailing zeroes).
    5. 1234 cannot be formatted with "999", since the value does not fit into the format. This causes an error.
    6. 0.1234 formatted with "9.99" gives: "0.12" (extra fraction digits are rounded).
    7. 0.1256 formatted with "9.99" gives: "0.13" (extra fraction digits are rounded).
  8. Process fractional part (if present in format)
    • Append decimal delimiter
    • If allowedFractionalDigits > 0 * Extract fractional part from pre-formatted string * Format it using formatPart() (not reversed) * Append to output
  9. Process closing sign / parenthesis (if present in format)
    • Replace ) with ) (negative) or space otherwise
    • Replace - with - (negative) or space otherwise
    • Replace + with - (negative) or + otherwise
  10. Append suffix (if present)

    If the format has content after the fractional part (afterFractionPart < format.length()):

    • If CR/DB/DR suffix is detected (hasSignCreditDebit == true): * Append the suffix exactly as written if the value is negative * Append two spaces (" ") if the value is positive * Note: These two spaces are not trimmed from the output.
    • Otherwise (normal suffix / closing sign handling):
      • If the character at afterFractionPart is a closing sign/parenthesis: * Append ), -, or + for negative values; append space for positive values * Set suffixStart to the position after the closing sign
      • Append the remaining format substring starting from suffixStart
  11. Apply sign reuse (if applicable)

    If reusableSignIndex is valid and output exceeds maxValueLength() (or sign is -), delete character at that index.

  12. Apply right-alignment (if requested)

    Left-pad with spaces to maxValueLength().

  13. Return result

    The final string is returned.

The formatPart() Helper Method

The private method formatPart() (called from NumericFormat.format()) is responsible for formatting one contiguous segment of digit placeholders — either the integer part or the fractional part.
It implements most of the placeholder-specific rendering rules (padding, zero suppression, star/zero replacement, comma handling) and is used twice in a typical formatting flow:

  • once for the integer part (with reversed digit order and left padding)
  • once for the fractional part (normal left-to-right order, right padding)

Signature:

   private String formatPart(
       String digits,               // digit string to place (without sign/decimal)
       char[] placeholders,         // format characters for this part, e.g. ['9','9',',','9']
       boolean reverse,             // true for integer part → process from right to left
       boolean padWithZeroes,       // true for integer part when using '9', false for 'Z'/'*'
       int optionalCount            // number of optional placeholders that can be collapsed
   )

Behavior and Rules

1. Input preparation

  • digits — string of digits only (no sign, no decimal point)
  • For integer part: usually the whole number as string, possibly with leading zeros removed
  • For fractional part: digits after decimal point, without leading zeros unless value is zero

2. Direction of processing

  • Integer part → processed right-to-left (reverse = true)
    → aligns digits to the right, pads/suppresses on the left
  • Fractional part → processed left-to-right (reverse = false)
    → aligns digits to the left, pads/suppresses on the right

3. Placeholder handling

Char Meaning Integer part Fractional part
9 Mandatory digit pad 0 pad 0
Z / z Optional digit — suppress leading zero space (not allowed)
* Optional digit — suppress leading zero * (not allowed)
> Extra digit slot (collapsible) space → digit (not allowed)
< Suppress trailing zero (not allowed) suppress 0 & comma
, Grouping separator collapse → space if no digit collapse → one comma + 0-padding

4. Comma (grouping separator) behavior

  • Integer part — multiple consecutive commas collapse to spaces when there are not enough digits
  • Fractional part — multiple consecutive commas collapse to one comma + trailing zero padding when digits are missing

5. Padding vs Suppression

  • When there are more placeholders than digits:
    • 9 → always pad with 0
    • Z / * / > → pad with space / * / space (left side for integer, right side for fraction)
    • < (fraction only) → suppress trailing 0 and associated commas
  • When there are more digits than placeholders: * If enough > placeholders exist → use them to display extra digits (leftmost first) * Otherwise → value does not fit → error 74

6. Examples

Integer part examples (reverse = true):
  • digits = "1234", placeholders = "9,9,9,9" → "1,2,3,4"
  • digits = "123", placeholders = "9,9,9,9" → " 1,2,3"
  • digits = "1", placeholders = "9,9,9,9" → " 0,0,1"
  • digits = "12345", placeholders = ">,>,9,9" → "12,345"
Fractional part examples (reverse = false):
  • digits = "2345", placeholders = "9,9,,,,9" → "2,3,,,,5"
  • digits = "23", placeholders = "9,9,,,,9" → "2,3,0,0,0"
  • digits = "2", placeholders = "9,9,,,,9" → "2,0,0,0,0"
  • digits = "23456", placeholders = "9.<<<<" → "2.2346" (extra digit shown)
  • digits = "23000", placeholders = "9.<<<<" → "2.23" (trailing zeros suppressed)

This helper is the core of placeholder logic and is reused symmetrically for both sides of the decimal point, with only the direction and padding character changing.

Legacy Time Formatting (Integer Values)

Special handling exists for formatting integer values (seconds since midnight) as time strings
when the format starts with one of the exact prefixes: hh:mm, hh:mm:ss, or +hh:mm.

Detailed rules, valid patterns, and formatting behavior (including negative values, large values,
modulo 86400, hours % 60 quirk in + prefix, trailing spaces preservation, case-insensitivity of placeholders,
AM/PM placement anywhere in suffix, etc.) are documented separately in #10471.

Also available in: Atom PDF