Skip to content

L specifier has three different behaviours when FMT_USE_LOCALE is 0 #4941

Description

@tiagomacarios

With FMT_USE_LOCALE=0, L does three different things depending on the argument type, and one of them still reads the global locale. I think two of the three are regressions, but the right contract is your call, so this is an issue rather than a PR.

Repro

Head (8dc5d3f), MSVC 19.51, /std:c++20, header-only, global locale set to de-DE:

#define FMT_USE_LOCALE 0
#include <fmt/chrono.h>

std::locale::global(std::locale("de-DE"));

fmt::format("{:L}", 1234567)             // "1234567"   L ignored
fmt::format("{:L}", 1234.5)              // "1,234.5"   stub numpunct applied
fmt::format("{:L%a}", tm)                // "Mon"       inert
fmt::format("{:L}", std::chrono::Monday) // "Mo"        global locale
Path Behaviour Why
int inert the locale-off write_loc returns false (format.h:2225), so it falls to write_int (format.h:2371)
float stub grouping "\03" / ',' do_write_float always runs Grouping(loc, specs.localized()) (format.h:2743, 2759, 2793)
std::tm inert get_locale(static_cast<bool>(loc_ref), ...), and operator bool is false (chrono.h:2148, core.h:919)
weekday / month / duration global locale get_locale(localized(), ...) (chrono.h:1705, 1925, 1977). With the loc.get<std::locale>() arm compiled out, get_locale default-constructs std::locale(), which is the global locale, not the classic one. That then reaches std::use_facet<std::time_put> at chrono.h:389.

On the numeric split

b98ffb7 ("Improve locale handling", 2022-09-04) moved the integer localized path onto write_loc, whose locale-off form returns false, so integers silently stopped grouping. Floats never used write_loc for grouping and kept theirs.

Two years later b90b4bc ("Remove FMT_STATIC_THOUSANDS_SEPARATOR in favor of FMT_USE_LOCALE") hard-coded ',' into the stub numpunct rather than dropping it, which reads like the static-separator behaviour was deliberately retained. If that is right, the integer path is the regression, not the float path. I did not want to assume it either way.

On the chrono path

Whatever L should mean here, consulting the global locale in a build with locale support disabled looks unintended on its own. formatter<std::tm> escapes it only because locale_ref::operator bool is false (#4627).

(#4940 fixes an adjacent case where formatter<year_month_day> constructs a locale unconditionally, but that one is independent of L.)

Why nothing caught this

nolocale-test has been compiling with locale enabled since b90b4bc, because FMT_STATIC_THOUSANDS_SEPARATOR no longer exists under include/. #4939 fixes that.

Question

Which contract do you want for L when locale support is off - reject it, ignore it, or honour it via the stub? Happy to write the patch once you pick.

One implementation note: rejecting has to happen in basic_specs::set_localized, not in parse_format_specs. Only one of the six call sites is the core parser (core.h:1540); the other five are chrono (chrono.h:1914, 1966, 2063, 2110, 2127) and bypass it. Ignoring can be a no-op setter, and honouring is a change to the integer write path - those two do not have the same constraint.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions