Add separator choice to the ' digit grouping option

Extend the grammar to grouping ::= "'" [" " | "," | "_" | "'"]:

- '  : U+202F (U+0020 for char if the literal encoding is not UTF-8),
       groups the integer and fraction parts, may be combined with L
- ' (apostrophe and space): U+0020, otherwise the same as '
- ', '_ '' : the given character, groups only the integer part;
       combining them with L is an error

All forms share the presentation type restrictions, the grouping of
zero padding and the four-digit exception macro. The separator kind is
stored in a 3-bit field of basic_specs (fmt::group_separator).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mateusz Pusz 2026-10-21 18:56:49 +02:00
parent 7ba4db88b1
commit f8abbeffc0
7 changed files with 276 additions and 52 deletions

View File

@ -63,10 +63,11 @@ fmt's parser.
<a id="format-spec"></a> <a id="format-spec"></a>
<pre><code class="language-json" <pre><code class="language-json"
>format_spec ::= [[fill]align][sign]["#"]["0"][width]["'"]["." precision]["L"][type] >format_spec ::= [[fill]align][sign]["#"]["0"][width][grouping]["." precision]["L"][type]
fill ::= &lt;a character other than '{' or '}'> fill ::= &lt;a character other than '{' or '}'>
align ::= "<" | ">" | "^" align ::= "<" | ">" | "^"
sign ::= "+" | "-" | " " sign ::= "+" | "-" | " "
grouping ::= "'" [" " | "," | "_" | "'"]
width ::= <a href="#replacement-field">integer</a> | "{" [<a width ::= <a href="#replacement-field">integer</a> | "{" [<a
href="#replacement-field">arg_id</a>] "}" href="#replacement-field">arg_id</a>] "}"
precision ::= <a href="#replacement-field">integer</a> | "{" [<a precision ::= <a href="#replacement-field">integer</a> | "{" [<a
@ -223,33 +224,44 @@ fmt::format(loc, "{:.2Lf}", 1234567.89); // Result: "1,234,567.89"
### Digit grouping (`'`) ### Digit grouping (`'`)
The `'` option groups the digits of integers and of floating-point values in The *grouping* option groups the digits of integers and of floating-point
decimal notation in threes, counting from the decimal marker towards the left values in decimal notation in threes, counting from the decimal marker. The
and towards the right, as specified by ISO 80000-1 and the SI Brochure. The character after `'` selects the separator:
separator is U+202F NARROW NO-BREAK SPACE: a single code unit for wide
character types and its UTF-8 encoding for `char` when the literal encoding is | Option | Separator | Groups | With `L` |
UTF-8, otherwise U+0020 SPACE. Each separator counts as one column of width. |--------|-----------|----------------------------|----------|
| `'` | U+202F NARROW NO-BREAK SPACE | integer and fraction part | allowed |
| `' ` | U+0020 SPACE | integer and fraction part | allowed |
| `',` | `,` | integer part only | error |
| `'_` | `_` | integer part only | error |
| `''` | `'` | integer part only | error |
`'` alone follows ISO 80000-1 and the SI Brochure. U+202F is written as a
single code unit for wide character types and as UTF-8 for `char` when the
literal encoding is UTF-8; otherwise U+0020 SPACE is used. Each separator
counts as one column of width. With `'` and `' `, `L` supplies only the
decimal point; grouping never comes from the locale.
In exponential notation only the fraction digits of the significand are In exponential notation only the fraction digits of the significand are
grouped, never the exponent. Infinity and NaN are not affected. With `0`, the grouped (by `'` and `' `), never the exponent. Infinity and NaN are not
padding zeros are grouped like other digits; the result never starts with a affected. With `0`, the padding zeros are grouped like other digits; the
separator, so it can be one column wider than *width*. With `L`, the locale result never starts with a separator, so it can be one column wider than
supplies only the decimal point; grouping always follows `'`. *width*.
The option is accepted for integers (other than `bool` and characters) with The option is accepted for integers (other than `bool` and characters) with
presentation type `d` or none, and for floating-point values with presentation presentation type `d` or none, and for floating-point values with presentation
types `e`, `E`, `f`, `F`, `g`, `G` or none. types `e`, `E`, `f`, `F`, `g`, `G` or none.
```c++ ```c++
fmt::format("{:'}", 1234567); // Result: "1 234 567" fmt::format("{:'}", 1234567); // Result: "1 234 567" (U+202F)
fmt::format("{:'.6f}", 1234567.891234); // Result: "1 234 567.891 234" fmt::format("{:'.6f}", 1234567.891234); // Result: "1 234 567.891 234" (U+202F)
fmt::format("{:'.6e}", 123456.789); // Result: "1.234 568e+05" fmt::format("{:' .6e}", 123456.789); // Result: "1.234 568e+05"
fmt::format("{:*>12'}", 1234567); // Result: "***1 234 567" fmt::format("{:',.6f}", 1234567.891234); // Result: "1,234,567.891234"
fmt::format("{:010'}", 1234567); // Result: "01 234 567" fmt::format("{:'_}", 1234567); // Result: "1_234_567"
fmt::format("{:*>12''}", 1234567); // Result: "***1'234'567"
fmt::format("{:010',}", 1234567); // Result: "01,234,567"
``` ```
(The spaces in the results above are U+202F.)
### Presentation type ### Presentation type
The *type* field chooses the representation for the value. Specifiers are The *type* field chooses the representation for the value. Specifiers are

View File

@ -677,6 +677,12 @@ enum class presentation_type : unsigned char {
enum class align { none, left, right, center, numeric }; enum class align { none, left, right, center, numeric };
enum class sign { none, minus, plus, space }; enum class sign { none, minus, plus, space };
// Digit group separator selected by the ' option. iso (') is U+202F, or
// U+0020 for char if the literal encoding is not UTF-8; space (' followed by
// a space) is U+0020; comma (',), underscore ('_) and apostrophe ('') are the
// corresponding characters. iso and space group both the integer and the
// fraction part, the others group only the integer part.
enum class group_separator { none, iso, space, comma, underscore, apostrophe };
enum class arg_id_kind { none, index, name }; enum class arg_id_kind { none, index, name };
// Basic format specifiers for built-in and string types. // Basic format specifiers for built-in and string types.
@ -687,8 +693,8 @@ class basic_specs {
// 0 1 2 3 // 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |type |align| w | p | s |u|#|L| f |'| unused | // |type |align| w | p | s |u|#|L| f | ' | unused |
// +-----+-----+---+---+---+-+-+-+-----+-+-------------------------+ // +-----+-----+---+---+---+-+-+-+-----+-----+---------------------+
// //
// w - dynamic width info // w - dynamic width info
// p - dynamic precision info // p - dynamic precision info
@ -697,7 +703,7 @@ class basic_specs {
// # - alternate form ('#') // # - alternate form ('#')
// L - localized // L - localized
// f - fill size // f - fill size
// ' - ISO 80000-1 digit grouping // ' - digit grouping (fmt::group_separator)
// //
// Bitfields are not used because of compiler bugs such as gcc bug 61414. // Bitfields are not used because of compiler bugs such as gcc bug 61414.
enum : unsigned { enum : unsigned {
@ -710,19 +716,20 @@ class basic_specs {
alternate_mask = 0x02000, alternate_mask = 0x02000,
localized_mask = 0x04000, localized_mask = 0x04000,
fill_size_mask = 0x38000, fill_size_mask = 0x38000,
iso_grouping_mask = 0x40000, grouping_mask = 0x1C0000,
align_shift = 3, align_shift = 3,
width_shift = 6, width_shift = 6,
precision_shift = 8, precision_shift = 8,
sign_shift = 10, sign_shift = 10,
fill_size_shift = 15, fill_size_shift = 15,
grouping_shift = 18,
max_fill_size = 4 max_fill_size = 4
}; };
unsigned data_ = 1 << fill_size_shift; unsigned data_ = 1 << fill_size_shift;
static_assert(sizeof(basic_specs::data_) * CHAR_BIT >= 19, ""); static_assert(sizeof(basic_specs::data_) * CHAR_BIT >= 21, "");
// Character (code unit) type is erased to prevent template bloat. // Character (code unit) type is erased to prevent template bloat.
char fill_data_[max_fill_size] = {' '}; char fill_data_[max_fill_size] = {' '};
@ -785,13 +792,17 @@ class basic_specs {
FMT_CONSTEXPR void set_localized() { data_ |= localized_mask; } FMT_CONSTEXPR void set_localized() { data_ |= localized_mask; }
FMT_CONSTEXPR void clear_localized() { data_ &= ~localized_mask; } FMT_CONSTEXPR void clear_localized() { data_ &= ~localized_mask; }
// Digit grouping in threes on both sides of the decimal marker as specified // Digit grouping in threes counted from the decimal marker (the ' option).
// by ISO 80000-1 and the SI Brochure (the ' option). constexpr auto grouping() const -> fmt::group_separator {
constexpr auto iso_grouping() const -> bool { return static_cast<fmt::group_separator>((data_ & grouping_mask) >>
return (data_ & iso_grouping_mask) != 0; grouping_shift);
}
FMT_CONSTEXPR void set_grouping(fmt::group_separator g) {
data_ = (data_ & ~grouping_mask) | (unsigned(g) << grouping_shift);
}
constexpr auto iso_grouping() const -> bool {
return (data_ & grouping_mask) != 0;
} }
FMT_CONSTEXPR void set_iso_grouping() { data_ |= iso_grouping_mask; }
FMT_CONSTEXPR void clear_iso_grouping() { data_ &= ~iso_grouping_mask; }
constexpr auto fill_size() const -> size_t { constexpr auto fill_size() const -> size_t {
return (data_ & fill_size_mask) >> fill_size_shift; return (data_ & fill_size_mask) >> fill_size_shift;
@ -1561,8 +1572,27 @@ FMT_CONSTEXPR auto parse_format_specs(const Char* begin, const Char* end,
case '\'': case '\'':
enter_state(state::iso_grouping, enter_state(state::iso_grouping,
in(arg_type, sint_set | uint_set | float_set)); in(arg_type, sint_set | uint_set | float_set));
specs.set_iso_grouping(); specs.set_grouping(group_separator::iso);
++begin; if (++begin != end) {
switch (to_ascii(*begin)) {
case ' ':
specs.set_grouping(group_separator::space);
++begin;
break;
case ',':
specs.set_grouping(group_separator::comma);
++begin;
break;
case '_':
specs.set_grouping(group_separator::underscore);
++begin;
break;
case '\'':
specs.set_grouping(group_separator::apostrophe);
++begin;
break;
}
}
break; break;
case '.': case '.':
enter_state(state::precision, enter_state(state::precision,
@ -1571,6 +1601,9 @@ FMT_CONSTEXPR auto parse_format_specs(const Char* begin, const Char* end,
break; break;
case 'L': case 'L':
enter_state(state::locale, is_arithmetic_type(arg_type)); enter_state(state::locale, is_arithmetic_type(arg_type));
// L may only supply the decimal point for the ISO forms (' and ' ).
if (specs.grouping() > group_separator::space)
report_error("invalid digit group separator with 'L'");
specs.set_localized(); specs.set_localized();
++begin; ++begin;
break; break;

View File

@ -2417,18 +2417,36 @@ template <typename Char> struct iso_separator {
size_t size; // In code units; the display width is always 1. size_t size; // In code units; the display width is always 1.
}; };
// Returns the digit group separator: U+202F NARROW NO-BREAK SPACE encoded as // Returns the digit group separator for g: U+202F NARROW NO-BREAK SPACE for
// a single code unit for wide character types, as UTF-8 for char if the // group_separator::iso, encoded as a single code unit for wide character types,
// literal encoding is UTF-8, and U+0020 SPACE otherwise. // as UTF-8 for char if the literal encoding is UTF-8, and U+0020 SPACE
// otherwise. The other forms use the corresponding ASCII character.
template <typename Char> template <typename Char>
FMT_CONSTEXPR auto iso_digit_separator() -> iso_separator<Char> { FMT_CONSTEXPR auto iso_digit_separator(group_separator g)
-> iso_separator<Char> {
switch (g) {
case group_separator::space: return {{Char(' '), Char(), Char()}, 1};
case group_separator::comma: return {{Char(','), Char(), Char()}, 1};
case group_separator::underscore: return {{Char('_'), Char(), Char()}, 1};
case group_separator::apostrophe: return {{Char('\''), Char(), Char()}, 1};
default: break;
}
return {{static_cast<Char>(0x202F), Char(), Char()}, 1}; return {{static_cast<Char>(0x202F), Char(), Char()}, 1};
} }
template <> template <>
FMT_CONSTEXPR inline auto iso_digit_separator<char>() -> iso_separator<char> { FMT_CONSTEXPR inline auto iso_digit_separator<char>(group_separator g)
if (FMT_UNICODE != 0 && is_utf8_enabled != 0) -> iso_separator<char> {
return {{'\xE2', '\x80', '\xAF'}, 3}; if (g == group_separator::iso) {
return {{' ', '\0', '\0'}, 1}; if (FMT_UNICODE != 0 && is_utf8_enabled != 0)
return {{'\xE2', '\x80', '\xAF'}, 3};
g = group_separator::space;
}
switch (g) {
case group_separator::comma: return {{',', '\0', '\0'}, 1};
case group_separator::underscore: return {{'_', '\0', '\0'}, 1};
case group_separator::apostrophe: return {{'\'', '\0', '\0'}, 1};
default: return {{' ', '\0', '\0'}, 1};
}
} }
FMT_CONSTEXPR inline auto is_iso_digit(char c) -> bool { FMT_CONSTEXPR inline auto is_iso_digit(char c) -> bool {
@ -2469,7 +2487,9 @@ FMT_CONSTEXPR auto write_iso_grouped(OutputIt out, const char* begin,
} }
int digits_size = static_cast<int>(int_end - int_begin); int digits_size = static_cast<int>(int_end - int_begin);
int frac_size = static_cast<int>(frac_end - frac_begin); int frac_size = static_cast<int>(frac_end - frac_begin);
int frac_seps = iso_separator_count(frac_size); // Only the ISO forms (' and ' ) group the fraction digits.
bool group_frac = specs.grouping() <= group_separator::space;
int frac_seps = group_frac ? iso_separator_count(frac_size) : 0;
auto num_chars = to_unsigned(end - begin); auto num_chars = to_unsigned(end - begin);
auto padded_specs = specs; auto padded_specs = specs;
@ -2492,7 +2512,7 @@ FMT_CONSTEXPR auto write_iso_grouped(OutputIt out, const char* begin,
} }
int num_zeros = int_size - digits_size; int num_zeros = int_size - digits_size;
int int_seps = iso_separator_count(int_size); int int_seps = iso_separator_count(int_size);
auto sep = iso_digit_separator<Char>(); auto sep = iso_digit_separator<Char>(specs.grouping());
auto num_seps = to_unsigned(int_seps + frac_seps); auto num_seps = to_unsigned(int_seps + frac_seps);
// Each separator is one code point of display width 1 but may take several // Each separator is one code point of display width 1 but may take several
// code units, so size and width are passed to write_padded separately. // code units, so size and width are passed to write_padded separately.
@ -3855,7 +3875,7 @@ FMT_CONSTEXPR20 auto write(OutputIt out, T value, format_specs specs,
// separators and pad. ' takes precedence over L for grouping; L only // separators and pad. ' takes precedence over L for grouping; L only
// supplies the decimal point. // supplies the decimal point.
auto plain_specs = specs; auto plain_specs = specs;
plain_specs.clear_iso_grouping(); plain_specs.set_grouping(group_separator::none);
plain_specs.clear_localized(); plain_specs.clear_localized();
plain_specs.set_align(align::none); plain_specs.set_align(align::none);
plain_specs.width = 0; plain_specs.width = 0;

View File

@ -256,7 +256,7 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20)
format-iso-grouping format-iso-grouping
" "
#if FMT_USE_CONSTEVAL #if FMT_USE_CONSTEVAL
fmt::format(\"{:>12'.3f} {:'} {:010'} {:'L}\", 1.5, 42, 42, 42); fmt::format(\"{:>12'.3f} {:'} {:010'} {:'L} {:' L} {:',} {:'_.2f} {:''} {:'>8''}\", 1.5, 42, 42, 42, 42, 42, 1.5, 42, 42);
#endif #endif
") ")
expect_compile( expect_compile(
@ -320,6 +320,37 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20)
" "
ERROR) ERROR)
expect_compile(
format-iso-grouping-comma-locale-error
"
#if FMT_USE_CONSTEVAL
fmt::format(\"{:',L}\", 42);
#else
#error
#endif
"
ERROR)
expect_compile(
format-iso-grouping-underscore-hex-error
"
#if FMT_USE_CONSTEVAL
fmt::format(\"{:'_x}\", 42);
#else
#error
#endif
"
ERROR)
expect_compile(
format-iso-grouping-apostrophe-locale-float-error
"
#if FMT_USE_CONSTEVAL
fmt::format(\"{:''.2Lf}\", 4.2);
#else
#error
#endif
"
ERROR)
# Compile-time argument name check # Compile-time argument name check
expect_compile( expect_compile(
format-string-name format-string-name

View File

@ -1350,12 +1350,114 @@ TEST(format_test, iso_digit_grouping) {
"invalid format specifier"); "invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'010}"), 42), format_error, EXPECT_THROW_MSG((void)fmt::format(runtime("{:'010}"), 42), format_error,
"invalid format specifier"); "invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:''}"), 42), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:.2'f}"), 4.2), format_error, EXPECT_THROW_MSG((void)fmt::format(runtime("{:.2'f}"), 4.2), format_error,
"invalid format specifier"); "invalid format specifier");
} }
TEST(format_test, digit_group_separators) {
// ' followed by space always uses U+0020 and groups both parts like '.
EXPECT_EQ(fmt::format("{:' }", 1234567), "1 234 567");
EXPECT_EQ(fmt::format("{:' .6f}", 1234567.891234), "1 234 567.891 234");
EXPECT_EQ(fmt::format("{:' .6e}", 123456.789), "1.234 568e+05");
EXPECT_EQ(fmt::format("{:' }", 0.000123456), "0.000 123 456");
EXPECT_EQ(fmt::format("{:>12' }", -1234567), " -1 234 567");
EXPECT_EQ(fmt::format("{:010' }", 1234567), "01 234 567");
// In a UTF-8 build ' and ' differ only in the separator.
if (fmt::detail::is_utf8_enabled != 0 && FMT_UNICODE != 0) {
EXPECT_EQ(fmt::format("{:'}", 1234567),
"1\xE2\x80\xAF"
"234\xE2\x80\xAF"
"567");
EXPECT_NE(fmt::format("{:'}", 1234567), fmt::format("{:' }", 1234567));
EXPECT_EQ(fmt::format("{:>12'}", 1234567).size(),
fmt::format("{:>12' }", 1234567).size() + 2 * 2);
}
// ', '_ and '' group only the integer part.
EXPECT_EQ(fmt::format("{:',}", 1234567), "1,234,567");
EXPECT_EQ(fmt::format("{:'_}", 1234567), "1_234_567");
EXPECT_EQ(fmt::format("{:''}", 1234567), "1'234'567");
EXPECT_EQ(fmt::format("{:',}", 0), "0");
EXPECT_EQ(fmt::format("{:',}", 123), "123");
EXPECT_EQ(fmt::format("{:',}", 1234), "1,234");
EXPECT_EQ(fmt::format("{:',}", -1234567), "-1,234,567");
EXPECT_EQ(fmt::format("{:+'_d}", 1234567), "+1_234_567");
EXPECT_EQ(fmt::format("{:',}", INT64_MIN), "-9,223,372,036,854,775,808");
EXPECT_EQ(fmt::format("{:''}", UINT64_MAX), "18'446'744'073'709'551'615");
EXPECT_EQ(fmt::format("{:',.6f}", 1234567.891234), "1,234,567.891234");
EXPECT_EQ(fmt::format("{:'_.6f}", 1234567.891234), "1_234_567.891234");
EXPECT_EQ(fmt::format("{:''.6f}", 1234567.891234), "1'234'567.891234");
EXPECT_EQ(fmt::format("{:',.4f}", 0.1234567), "0.1235");
EXPECT_EQ(fmt::format("{:'_.3e}", 123456.789), "1.235e+05");
EXPECT_EQ(fmt::format("{:',.6e}", 123456.789), "1.234568e+05");
EXPECT_EQ(fmt::format("{:',}", 1234567.891234), "1,234,567.891234");
EXPECT_EQ(fmt::format("{:',}", 1.234567e300), "1.234567e+300");
EXPECT_EQ(fmt::format("{:'_g}", 1234.5), "1_234.5");
EXPECT_EQ(fmt::format("{:',.10G}", 1234567.891), "1,234,567.891");
EXPECT_EQ(fmt::format("{:#',.0f}", 1234.0), "1,234.");
EXPECT_EQ(fmt::format("{:',.0f}", 1e20), "100,000,000,000,000,000,000");
EXPECT_EQ(fmt::format("{:',}", std::numeric_limits<double>::infinity()),
"inf");
// Width and fill.
EXPECT_EQ(fmt::format("{:*>12',}", 1234567), "***1,234,567");
EXPECT_EQ(fmt::format("{:<12'_}", 1234567), "1_234_567 ");
EXPECT_EQ(fmt::format("{:^13''}", 1234567), " 1'234'567 ");
// ' as fill, then '' as grouping.
EXPECT_EQ(fmt::format("{:'>8''}", 1234), "'''1'234");
EXPECT_EQ(fmt::format("{:'>12'}", 1234), iso("'''''''1_234"));
// Zero padding groups the zeros with the same rule as '.
EXPECT_EQ(fmt::format("{:010',}", 1234567), "01,234,567");
EXPECT_EQ(fmt::format("{:04',}", 1), "0,001");
EXPECT_EQ(fmt::format("{:03',}", 1), "001");
EXPECT_EQ(fmt::format("{:010',}", -1234), "-0,001,234");
EXPECT_EQ(fmt::format("{:08'_}", 1), "0_000_001");
EXPECT_EQ(fmt::format("{:012',.2f}", 1234.5), "0,001,234.50");
EXPECT_EQ(fmt::format("{:014',.6f}", 1.234567), "000,001.234567");
EXPECT_EQ(fmt::format("{:012',.6f}", 1.234567), "0,001.234567");
EXPECT_EQ(fmt::format("{:012''.3e}", 123456.0), "0'001.235e+05");
EXPECT_EQ(fmt::format(runtime("{:010',}"), 1234567), "01,234,567");
// Invalid presentation types for every form.
for (auto spec :
{"{:'x}", "{:' x}", "{:',x}", "{:'_x}", "{:''x}", "{:'X}", "{:',X}",
"{:'o}", "{:'_o}", "{:'b}", "{:''b}", "{:',B}", "{:'c}", "{:',c}"}) {
EXPECT_THROW_MSG((void)fmt::format(runtime(spec), 42), format_error,
"invalid presentation type for digit grouping")
<< spec;
}
for (auto spec : {"{:'a}", "{:' a}", "{:',a}", "{:'_A}", "{:''A}"}) {
EXPECT_THROW_MSG((void)fmt::format(runtime(spec), 4.2), format_error,
"invalid presentation type for digit grouping")
<< spec;
}
// L only with the ISO forms.
EXPECT_THROW_MSG((void)fmt::format(runtime("{:',L}"), 42), format_error,
"invalid digit group separator with 'L'");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'_L}"), 42), format_error,
"invalid digit group separator with 'L'");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:''.2Lf}"), 4.2), format_error,
"invalid digit group separator with 'L'");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:',.4Lf}"), 4.2), format_error,
"invalid digit group separator with 'L'");
// Other characters after ' are not separators.
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'.}"), 4.2), format_error,
"invalid format string");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'-}"), 42), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'''}"), 42), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:',,}"), 42), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:','}"), 42), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:',}"), "abc"), format_error,
"invalid format specifier");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'_}"), true), format_error,
"invalid format specifier");
}
TEST(format_test, precision) { TEST(format_test, precision) {
char format_str[buffer_size]; char format_str[buffer_size];
safe_sprintf(format_str, "{0:.%u", UINT_MAX); safe_sprintf(format_str, "{0:.%u", UINT_MAX);
@ -2903,6 +3005,14 @@ TEST(format_test, iso_digit_grouping_locale) {
fmt::format(loc, "{:'Lg}", 1234567.0)}) { fmt::format(loc, "{:'Lg}", 1234567.0)}) {
EXPECT_EQ(s.find('.'), std::string::npos) << s; EXPECT_EQ(s.find('.'), std::string::npos) << s;
} }
// ' followed by space: U+0020 separator, L supplies the decimal point.
EXPECT_EQ(fmt::format(loc, "{:' .4Lf}", 1234.5678), "1 234,567 8");
EXPECT_EQ(fmt::format(loc, "{:' L}", 1234567), "1 234 567");
// ', '_ and '' cannot be combined with L.
EXPECT_THROW_MSG((void)fmt::format(loc, runtime("{:',.4Lf}"), 1234.5678),
format_error, "invalid digit group separator with 'L'");
// Without L the locale is not consulted.
EXPECT_EQ(fmt::format(loc, "{:',.4f}", 1234.5678), "1,234.5678");
// The format_facet thousands separator is not used either. // The format_facet thousands separator is not used either.
auto facet_loc = auto facet_loc =
std::locale({}, new fmt::format_facet<std::locale>(",", {1, 2, 3})); std::locale({}, new fmt::format_facet<std::locale>(",", {1, 2, 3}));

View File

@ -54,6 +54,14 @@ TEST(iso_grouping_min2_test, four_digit_exception) {
EXPECT_EQ(fmt::format("{:04'}", 1), "0001"); EXPECT_EQ(fmt::format("{:04'}", 1), "0001");
EXPECT_EQ(fmt::format("{:05'}", 1), iso("00_001")); EXPECT_EQ(fmt::format("{:05'}", 1), iso("00_001"));
EXPECT_EQ(fmt::format("{:010'}", 1234567), iso("01_234_567")); EXPECT_EQ(fmt::format("{:010'}", 1234567), iso("01_234_567"));
// All forms; the comma forms only group the integer part.
EXPECT_EQ(fmt::format("{:',}", 1234), "1234");
EXPECT_EQ(fmt::format("{:',}", 12345), "12,345");
EXPECT_EQ(fmt::format("{:' .4f}", 1234.5678), "1234.5678");
EXPECT_EQ(fmt::format("{:' .5f}", 1234.56789), "1234.567 89");
EXPECT_EQ(fmt::format("{:'_.5f}", 1234.56789), "1234.56789");
EXPECT_EQ(fmt::format("{:04',}", 1), "0001");
EXPECT_EQ(fmt::format("{:05''}", 1), "00'001");
EXPECT_THROW_MSG((void)fmt::format(runtime("{:'x}"), 42), format_error, EXPECT_THROW_MSG((void)fmt::format(runtime("{:'x}"), 42), format_error,
"invalid presentation type for digit grouping"); "invalid presentation type for digit grouping");
} }

View File

@ -87,15 +87,25 @@ TEST(xchar_test, format) {
TEST(xchar_test, iso_digit_grouping) { TEST(xchar_test, iso_digit_grouping) {
// Wide character types use the single code unit U+202F. // Wide character types use the single code unit U+202F.
EXPECT_EQ(fmt::format(L"{:'}", 1234567), L"1 234 567"); EXPECT_EQ(fmt::format(L"{:'}", 1234567), L"1\u202F234\u202F567");
EXPECT_EQ(fmt::format(L"{:'.6f}", 1234567.891234), L"1 234 567.891 234"); EXPECT_EQ(fmt::format(L"{:'.6f}", 1234567.891234),
EXPECT_EQ(fmt::format(L"{:'.6e}", 123456.789), L"1.234 568e+05"); L"1\u202F234\u202F567.891\u202F234");
EXPECT_EQ(fmt::format(L"{:*>12'}", -1234567), L"**-1 234 567"); EXPECT_EQ(fmt::format(L"{:'.6e}", 123456.789), L"1.234\u202F568e+05");
EXPECT_EQ(fmt::format(fmt::runtime(L"{:>12'}"), 1234567), L" 1 234 567"); EXPECT_EQ(fmt::format(L"{:*>12'}", -1234567), L"**-1\u202F234\u202F567");
EXPECT_EQ(fmt::format(fmt::runtime(L"{:>12'}"), 1234567),
L" 1\u202F234\u202F567");
EXPECT_THROW((void)fmt::format(fmt::runtime(L"{:'x}"), 42), EXPECT_THROW((void)fmt::format(fmt::runtime(L"{:'x}"), 42),
fmt::format_error); fmt::format_error);
EXPECT_EQ(fmt::format(u"{:'}", 1234567), u"1 234 567"); EXPECT_EQ(fmt::format(u"{:'}", 1234567), u"1\u202F234\u202F567");
EXPECT_EQ(fmt::format(U"{:>10'.1f}", 1234.5), U" 1 234.5"); EXPECT_EQ(fmt::format(U"{:>10'.1f}", 1234.5), U" 1\u202F234.5");
// The other forms emit a single code unit too.
EXPECT_EQ(fmt::format(L"{:' }", 1234567), L"1 234 567");
EXPECT_EQ(fmt::format(L"{:',}", 1234567), L"1,234,567");
EXPECT_EQ(fmt::format(L"{:'_.3f}", 1234.5), L"1_234.500");
EXPECT_EQ(fmt::format(L"{:''}", -1234567), L"-1'234'567");
EXPECT_EQ(fmt::format(L"{:010',}", 1234567), L"01,234,567");
EXPECT_EQ(fmt::format(u"{:*>10',}", 1234567), u"*1,234,567");
EXPECT_EQ(fmt::format(U"{:' .4f}", 1234.5678), U"1 234.567 8");
} }
TEST(xchar_test, is_formattable) { TEST(xchar_test, is_formattable) {