|
morph 0.1.0
Asynchronous bridge between GUI thread and business models
|
Locale numeric-entry normalisation for the control edge. More...
#include <algorithm>#include <cstddef>#include <cstdint>#include <limits>#include <optional>#include <string>#include <string_view>#include "../util/rational.hpp"Go to the source code of this file.
Classes | |
| struct | morph::render::NumericLocale |
| The locale facts both control-edge conversions need, in one designated-initialisable aggregate. More... | |
| struct | morph::render::detail::CodePoint |
| One decoded UTF-8 code point: its value and how many bytes it took. More... | |
| struct | morph::render::detail::DigitMatch |
| A digit matched at the start of an entry. More... | |
| struct | morph::render::detail::SignMatch |
| A sign matched at the start of an entry: how much of the text it occupies, and what it contributes to the canonical output. More... | |
Functions | |
| CodePoint | morph::render::detail::decodeUtf8 (std::string_view text) |
Decodes the UTF-8 sequence at the start of text. | |
| void | morph::render::detail::appendUtf8 (std::string &out, char32_t value) |
Appends value to out as UTF-8. | |
| void | morph::render::detail::appendDisplayDigit (std::string &out, char32_t base, char chr) |
| Appends one character of canonical text to a display string, with a digit rewritten into the locale's set. | |
| std::size_t | morph::render::detail::utf8Length (char32_t value) |
How many bytes value occupies in UTF-8. | |
| char32_t | morph::render::detail::digitBase (std::string_view zeroDigit) |
The code point of zeroDigit, or U'0' when it is empty or not a well-formed UTF-8 code point. | |
| DigitMatch | morph::render::detail::leadingDigit (std::string_view rest, char32_t base) |
Matches a digit at the start of rest, in the locale's set or in ASCII. | |
| bool | morph::render::detail::groupingIsWellPlaced (std::string_view text, NumericLocale const &loc) |
Whether every group separator in text sits where a group separator can legally sit. | |
| std::size_t | morph::render::detail::leadingSeparatorLength (std::string_view rest, std::string_view separator) |
The length of separator at the start of rest, in bytes, or 0 when it is not there – or is empty. | |
| std::size_t | morph::render::detail::leadingSignLength (std::string_view rest, std::string_view localeSign, char asciiSign) |
The length of a sign at the start of rest, in bytes, or 0 when there is none there. | |
| SignMatch | morph::render::detail::leadingSign (std::string_view rest, NumericLocale const &loc) |
Matches either sign at the start of rest. | |
| std::optional< std::string > | morph::render::normalizeLocaleNumber (std::string_view text, NumericLocale const &loc) |
| Converts a locale-formatted numeric string to canonical (-?[0-9]+(\.[0-9]+)?) text. | |
| std::string | morph::render::formatCanonicalNumber (std::string_view canonicalText, NumericLocale const &loc) |
| Converts canonical (.-decimal) numeric text to locale-formatted display text, grouping the integer part in triples. | |
| std::optional< math::Rational > | morph::render::parseDecimal (std::string_view text, NumericLocale const &loc, std::optional< std::uint32_t > maxDecimalPlaces=std::nullopt) |
| Parses locale-formatted decimal entry text into the exact value it spells. | |
Locale numeric-entry normalisation for the control edge.
A locale may render and accept e.g. "1.050,25"; the payload a morph::units::Quantity field submits is always the canonical exact {num, den, dp} regardless. This header is the one control-edge conversion step between the two: the exact Rational/Quantity digit routines stay entirely locale-free (they only ever see plain .-decimal text), and a renderer calls normalizeLocaleNumber once, at the point text leaves the control, before handing it to those routines.
Definition in file locale_format.hpp.
|
inlinenodiscard |
Decodes the UTF-8 sequence at the start of text.
Strict: an overlong encoding, a surrogate code point, a truncated sequence and a stray continuation byte all report a CodePoint::length of 0 rather than some salvaged value. That strictness is load-bearing rather than pedantic. The digit test below is a range test on the decoded value, so a lenient decoder that let the overlong C0 B5 through would read it as U+0035 and accept it as the digit '5' – including in the default ASCII locale, where that input is not a digit at all.
| text | The remainder of the entry, starting at the scan position. |
text does not start with a well-formed sequence. Definition at line 145 of file locale_format.hpp.
|
inline |
Appends value to out as UTF-8.
| out | The string to append to. |
| value | A scalar value; callers here only ever pass a digit derived from a zeroDigit that decodeUtf8 already validated. |
Definition at line 197 of file locale_format.hpp.
|
inline |
Appends one character of canonical text to a display string, with a digit rewritten into the locale's set.
Anything that is not an ASCII digit is copied through verbatim. That arm is not dead code for a caller that honours the contract, but it is what bounds the damage for one that does not: a "12.34.56" handed to the display edge keeps its second '.' rather than becoming some code point below the digit base. Only digits are rewritten; text that has none passes through.
| out | The display string to append to. |
| base | The locale's DIGIT ZERO code point (see digitBase). |
| chr | One byte of canonical text. |
Definition at line 229 of file locale_format.hpp.
|
inlinenodiscard |
How many bytes value occupies in UTF-8.
| value | A scalar value. |
Definition at line 240 of file locale_format.hpp.
|
inlinenodiscard |
The code point of zeroDigit, or U'0' when it is empty or not a well-formed UTF-8 code point.
Empty reads as the ASCII '0' for the reason an empty negativeSign reads as '-': there is no locale without digits, so empty cannot mean absence, and a base of "nothing" would reject every entry the locale can produce. Only the first code point is read; anything after it is locale data this function has no use for.
| zeroDigit | The locale's DIGIT ZERO spelling. |
Definition at line 263 of file locale_format.hpp.
|
inlinenodiscard |
Matches a digit at the start of rest, in the locale's set or in ASCII.
A function of its own for the reason leadingSign is: the normalising scan reads as one statement per character class, and folding the two digit families plus the UTF-8 decode into it took it over clang-tidy's cognitive complexity threshold.
| rest | The remainder of the entry, starting at the scan position. |
| base | The locale's DIGIT ZERO code point (see digitBase). |
rest starts with no digit. Definition at line 290 of file locale_format.hpp.
|
inlinenodiscard |
Whether every group separator in text sits where a group separator can legally sit.
The rule, in one place so it can be read on its own: a group separator is preceded by one to three digits (the first group), or by exactly three (every later one); it is followed by exactly three more digits, at each group and at the end of the integer part; and it never appears after the decimal separator. A locale with no grouping (an empty NumericLocale::groupSeparator) has nothing to place, so it trivially passes.
This is a pass of its own rather than extra state inside the normalising scan below: "the grouping is well placed" and "the digits convert" are two separate statements about the entry, and reading them as one made neither clear.
It counts digits, not bytes, in the locale's set as well as in ASCII. A byte-by-byte scan is only correct while a digit is one byte: a two-byte U+0665 would reset the run-length counter on its own continuation byte, so "\u0661\u066c\u0660\u0665\u0660" – what formatCanonicalNumber emits for 1050 in an ar_EG locale – would be rejected as badly grouped and the pair would not round-trip.
Characters this function does not recognise are simply not digits — the normalising scan is what rejects them, and it rejects them whatever this pass concludes.
| text | The locale-formatted entry. |
| loc | The locale facts; only the two separators and the digit base are read. |
Definition at line 338 of file locale_format.hpp.
|
inlinenodiscard |
The length of separator at the start of rest, in bytes, or 0 when it is not there – or is empty.
The empty case is why this is a function and not an inline starts_with: rest.starts_with("") is true at every index, so an empty separator matched inline would swallow the whole entry one zero-length step at a time. Spelling the guard as !sep.empty() && ... at each call site instead adds two conjunctions to normalizeLocaleNumber, which is enough to put it over clang-tidy's cognitive-complexity threshold.
| rest | The remainder of the entry, starting at the scan position. |
| separator | The locale's spelling of this separator; empty means the locale has none, and matches nothing. |
Definition at line 392 of file locale_format.hpp.
|
inlinenodiscard |
The length of a sign at the start of rest, in bytes, or 0 when there is none there.
Two spellings count. localeSign is the locale's own, matched as a whole string so that U+2212 and the bidi-control-prefixed forms – two and three code points – match at all; a single char can express none of them. asciiSign counts as well, in every locale: U+2212 and the bidi marks are on no keyboard, so matching only the locale's spelling would reject the sign the user can actually type. Neither '-' nor '+' has a second reading in a numeric entry, so this is not the kind of guess the grouping rule forbids.
This is a function of its own so that the caller's scan reads as one statement per character class. It is shared by both signs and says nothing about what the caller then emits, which differs between them: a negative is emitted as the canonical '-', a positive is dropped (see normalizeLocaleNumber).
| rest | The remainder of the entry, starting at the scan position. |
| localeSign | The locale's spelling of this sign; empty matches nothing, leaving only the ASCII spelling. |
| asciiSign | The ASCII spelling accepted in every locale: '-' or '+'. |
Definition at line 418 of file locale_format.hpp.
|
inlinenodiscard |
Matches either sign at the start of rest.
Both signs in one function, and the emitted text carried back with the length, so the normalising scan below has a single sign branch with no inner "which sign was it" test. That is not only tidier: two branches with two inner tests each put normalizeLocaleNumber at a cognitive complexity of 28, over clang-tidy's threshold of 25; with the single branch it sits at
The negative sign is tried first. The order is not load-bearing for any locale Qt 6.11.2 reports – starts_with is an exact prefix match and no locale spells one sign as a prefix of the other – but it is fixed here so that it cannot vary.
| rest | The remainder of the entry, starting at the scan position. |
| loc | The locale facts; only the two signs are read. An empty negativeSign leaves '-', an empty positiveSign leaves '+'. |
rest starts with no sign. Definition at line 455 of file locale_format.hpp.
|
inlinenodiscard |
Converts a locale-formatted numeric string to canonical (-?[0-9]+(\.[0-9]+)?) text.
Drops NumericLocale::groupSeparator where it is correctly placed, replaces every occurrence of NumericLocale::decimalSeparator with ., and rewrites the locale's digits as ASCII ones. A default-constructed NumericLocale is the identity transform (the locale-free behavior). Malformed input (a second decimal separator, a sign anywhere but the leading position of the output, two digit families in one entry, or any character that is not a digit) yields std::nullopt rather than a best-effort guess. The decimal point counts as output, so a sign placed straight after the separator ("`,-5`" in a de-DE locale) is rejected.
The result is .-decimal and digit-only, but is not narrowed to -?[0-9]+(\.[0-9]+)?: a bare "`.`", a leading "`.5`" and a trailing "`5.`" are passed through, exactly as that same QML mirror passes them. Tightening one side alone would put the two control edges out of step, so the shape is documented here rather than narrowed.
Separators are matched as whole strings, so a multi-byte one (e.g. U+202F) works; matching them before the digit scan is what keeps their continuation bytes from being mistaken for stray non-digit characters.
| text | The locale-formatted entry, e.g. "1.050,25". |
| loc | The locale facts. Designated initialisers are the intended spelling: {.decimalSeparator = ",", .groupSeparator = "."}. |
Definition at line 589 of file locale_format.hpp.
|
inlinenodiscard |
Converts canonical (.-decimal) numeric text to locale-formatted display text, grouping the integer part in triples.
The display-direction inverse of normalizeLocaleNumber's decimal-separator substitution and digit rewriting, plus thousands grouping that normalizeLocaleNumber takes back: a grouped display round-trips, because the entry direction validates the grouping rather than stripping it (see "Grouping is validated, not stripped" on that function). A default-constructed NumericLocale is the identity transform.
The sign is emitted as NumericLocale::negativeSign, matching what normalizeLocaleNumber accepts back; an empty view is read as "-" rather than as "no sign", because formatting a negative to no sign at all is a silently wrong value.
| canonicalText | Canonical -?[0-9]+(\.[0-9]+)? text. |
| loc | The locale facts; positiveSign is not read. |
Definition at line 708 of file locale_format.hpp.
|
inlinenodiscard |
Parses locale-formatted decimal entry text into the exact value it spells.
The one decimal parser morph ships: the forms engine and any application read entry text through it. The text goes through normalizeLocaleNumber first, so the locale's separators, signs and digits are accepted and its grouping is validated; the canonical text must then be -?[0-9]+(\.[0-9]+)? (no exponent, no bare separator, no surrounding space). The value must fit math::Rational: at most math::kMaxDecimalPlaces fraction digits, and the digits read as one integer within int64_t. Nothing is rounded: text finer than maxDecimalPlaces is refused, because dropping a digit a user typed changes the value they meant.
| text | The entry, e.g. "1.050,25". |
| loc | The locale facts the entry was typed in. |
| maxDecimalPlaces | The declared precision, when there is one. The result carries it as its precision tag (clamped to math::kMaxDecimalPlaces); without one the tag is the number of fraction digits typed. |
Definition at line 758 of file locale_format.hpp.