Skip to content

render: normalizeLocaleNumber accepts a sign after the decimal separator, and returns strings outside its documented canonical form #497

Description

@Yaraslaut

Summary

morph::render::normalizeLocaleNumber (include/morph/render/locale_format.hpp:52-100) is the single control-edge step that converts locale-formatted numeric entry into the canonical .-decimal text the exact Rational/Quantity digit routines consume. It accepts inputs it documents as malformed, including a sign injected immediately after the decimal separator.

Verification status

Reproduced by execution. Branch bridge-async-threading-contract, clang 22. Real output:

normalizeLocaleNumber(",-5", dec=",", grp=".") = ".-5"
normalizeLocaleNumber(".-5", dec=".", grp=",") = ".-5"
normalizeLocaleNumber("1-2", dec=".", grp=",") = nullopt      <- control: correctly rejected
normalizeLocaleNumber(".",   dec=".", grp=",") = "."
normalizeLocaleNumber(".5",  dec=".", grp=",") = ".5"
normalizeLocaleNumber("5.",  dec=".", grp=",") = "5."

The "1-2" control shows the sign guard works — but only once a digit has been emitted.

Mechanism

sawAnyOutput is set only at the bottom of the loop (:91). The decimal-separator branch appends '.' to canonical and then continues (:66-74), skipping that assignment. So after a decimal separator the function still believes it has produced no output, and the chr == '-' guard at :79-83 lets the sign through.

The comment on that guard states the intent exactly, and the code does not implement it:

"Leading position of the output: a stripped group separator before the sign would otherwise make an injected sign look leading."

Two documented contracts broken

  1. :40-41"a sign anywhere but the leading position … yields std::nullopt". False for ",-5".
  2. :33-34 — the @brief promises output matching -?[0-9]+(\.[0-9]+)?. The final check (:95-97) rejects only "" and "-", so ".", ".5", "5." and ".-5" are all returned as canonical.

The QML mirror disagrees

src/qt/forms/qml/DynamicForm.qml:817-836 is documented at :807 as mirroring this function, but tests i !== 0 against the stripped string and therefore rejects ",-5". Two implementations that are declared mirrors give different answers for the same input — which is the more serious half of this, since it means one of the two edges validates and the other does not.

Test gap

tests/test_render_locale_format.cpp covers "1-2" (sign after a digit) but no case where the sign follows a separator, which is why the guard's incompleteness is invisible.

What would change the verdict

  • Close it if downstream parsing is shown to reject ".-5"/"5." safely and the two doc claims are corrected instead — but that would leave the C++ and QML edges disagreeing, which needs resolving either way.
  • The fix is one line: set sawAnyOutput = true in the decimal-separator branch (and tighten the final check to the documented grammar if the "."/"5." shapes also matter).

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

    area: formsSubsystem: formsbugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions