43 constexpr std::array<std::string_view, 6> kNames = {
"zero",
"one",
"two",
"few",
"many",
"other"};
44 return kNames.at(
static_cast<std::size_t
>(category));
57 std::uint64_t
const magnitude = math::detail::absU64(rounded.
numerator);
58 auto const denominator =
static_cast<std::uint64_t
>(rounded.
denominator);
59 std::string out = rounded.
numerator < 0 ?
"-" :
"";
60 out += std::to_string(magnitude / denominator);
63 std::uint64_t remainder = magnitude % denominator;
66 for (std::uint32_t digit = 0; digit < places; ++digit) {
68 out +=
static_cast<char>(
'0' +
static_cast<char>(remainder / denominator));
69 remainder %= denominator;
78[[nodiscard]]
inline std::optional<std::pair<math::Rational, std::uint32_t>>
numberOf(
Value const& value) {
79 if (std::int64_t
const*
const whole = value.
asInt()) {
83 return std::pair{decimal->value, decimal->value.decimalPlaces.value};
86 return std::pair{quantity->value, quantity->value.decimalPlaces.value};
123[[nodiscard]]
constexpr bool between(std::uint64_t operand, std::uint64_t low, std::uint64_t high)
noexcept {
124 return operand >= low && operand <= high;
131 return !n.visibleFraction && n.integer != 0 && n.integer % 1000000U == 0;
145 if (n.integer == 1 && !n.visibleFraction) {
155 if (n.whole && n.integer == 1) {
165 if (n.integer <= 1) {
175 if (n.visibleFraction) {
178 if (n.integer == 1) {
181 if (
between(n.integer % 10U, 2, 4) && !
between(n.integer % 100U, 12, 14)) {
191 if (n.visibleFraction) {
194 if (n.integer % 10U == 1 && n.integer % 100U != 11) {
197 if (
between(n.integer % 10U, 2, 4) && !
between(n.integer % 100U, 12, 14)) {
207 if (n.visibleFraction) {
210 if (n.integer == 1) {
223 if (n.integer == 0) {
226 if (n.integer == 1) {
229 if (n.integer == 2) {
232 if (
between(n.integer % 100U, 3, 10)) {
242inline constexpr std::array<std::pair<std::string_view, PluralRule>, 13>
kPluralRules = {{
271 std::uint32_t places) {
272 std::string language;
273 for (
char const chr : locale.substr(0, locale.find_first_of(
"-_"))) {
274 language +=
static_cast<char>(std::tolower(
static_cast<unsigned char>(chr)));
279 .integer = math::detail::absU64(shown.
numerator) /
static_cast<std::uint64_t
>(shown.
denominator),
281 .visibleFraction = places != 0,
283 auto const index = std::ranges::distance(
285 std::ranges::find(
detail::kPluralRules, language, &std::pair<std::string_view, detail::PluralRule>::first));
320 std::vector<std::pair<std::string, MessageFunction>>
inputs;
340 [[nodiscard]] std::expected<MessagePattern, std::string>
parse() {
342 std::size_t
const start = _pos;
344 if (!atEnd() && (peek() ==
'.' || _text.substr(_pos).starts_with(
"{{"))) {
345 return parseComplex(message);
348 auto pattern = parsePattern(
false);
350 return std::unexpected(pattern.error());
352 message.
pattern = std::move(*pattern);
357 [[nodiscard]]
bool atEnd() const noexcept {
return _pos >= _text.size(); }
358 [[nodiscard]]
char peek()
const {
return _text.at(_pos); }
361 while (!atEnd() && (peek() ==
' ' || peek() ==
'\t' || peek() ==
'\n' || peek() ==
'\r')) {
366 [[nodiscard]]
static bool nameChar(
char chr,
bool first)
noexcept {
367 bool const letter = (chr >=
'a' && chr <=
'z') || (chr >=
'A' && chr <=
'Z') || chr ==
'_';
371 return letter || (chr >=
'0' && chr <=
'9') || chr ==
'-' || chr ==
'.';
374 [[nodiscard]] std::string readName() {
375 std::size_t
const start = _pos;
376 while (!atEnd() && nameChar(peek(), _pos == start)) {
379 return std::string{_text.substr(start, _pos - start)};
383 [[nodiscard]] std::expected<std::string, std::string> readVariable() {
385 std::string name = readName();
387 return std::unexpected(std::string{
"a variable needs a name after `$`"});
393 [[nodiscard]] std::expected<std::pair<std::string, MessageFunction>, std::string> readExpressionBody() {
396 return std::unexpected(std::string{
"a placeholder is not closed"});
399 return std::unexpected(std::string{
"only variable placeholders `{$name}` are in the ui/1 subset"});
401 auto variable = readVariable();
403 return std::unexpected(variable.error());
407 if (!atEnd() && peek() ==
':') {
409 std::string
const name = readName();
410 if (name ==
"number") {
411 function = MessageFunction::Number;
412 }
else if (name ==
"string") {
413 function = MessageFunction::String;
415 return std::unexpected(
"the function `:" + name +
"` is not in the ui/1 subset");
419 if (atEnd() || peek() !=
'}') {
420 return std::unexpected(std::string{
"options and attributes are not in the ui/1 subset"});
423 return std::pair{std::move(*variable), function};
427 [[nodiscard]] std::expected<std::vector<MessagePart>, std::string> parsePattern(
bool quoted) {
428 std::vector<MessagePart> parts;
432 parts.push_back(MessagePart{.text = std::move(text), .variable = {}, .function = {}});
437 char const chr = peek();
438 if (quoted && _text.substr(_pos).starts_with(
"}}")) {
445 if (atEnd() || (peek() !=
'\\' && peek() !=
'{' && peek() !=
'}' && peek() !=
'|')) {
446 return std::unexpected(std::string{
"`\\` escapes only `\\`, `{`, `}` and `|`"});
454 auto body = readExpressionBody();
456 return std::unexpected(body.error());
459 parts.push_back(MessagePart{.text = {}, .variable = std::move(body->first), .function = body->second});
463 return std::unexpected(std::string{
"an unescaped `}` in text"});
469 return std::unexpected(std::string{
"a quoted pattern is not closed with `}}`"});
475 [[nodiscard]] std::expected<std::string, std::string> readKey() {
478 return std::string{
"*"};
483 while (!atEnd() && peek() !=
'|') {
484 if (peek() ==
'\\') {
494 return std::unexpected(std::string{
"a quoted key is not closed with `|`"});
499 std::size_t
const start = _pos;
503 while (!atEnd() && nameChar(peek(),
false)) {
507 return std::unexpected(std::string{
"a variant key is expected"});
509 return std::string{_text.substr(start, _pos - start)};
512 [[nodiscard]] std::expected<MessagePattern, std::string> parseComplex(MessagePattern& message) {
515 if (_text.substr(_pos).starts_with(
".input")) {
518 if (atEnd() || peek() !=
'{') {
519 return std::unexpected(std::string{
"`.input` takes a `{$name :function}`"});
522 auto body = readExpressionBody();
524 return std::unexpected(body.error());
526 message.inputs.push_back(std::move(*body));
529 if (_text.substr(_pos).starts_with(
".match")) {
531 return parseMatch(message);
533 if (_text.substr(_pos).starts_with(
"{{")) {
535 auto pattern = parsePattern(
true);
537 return std::unexpected(pattern.error());
539 message.pattern = std::move(*pattern);
542 return std::unexpected(std::string{
"text after the quoted pattern"});
546 return std::unexpected(std::string{
"only `.input`, `.match` and a quoted pattern are in the ui/1 subset"});
550 [[nodiscard]] std::expected<MessagePattern, std::string> parseMatch(MessagePattern& message) {
551 if (
auto refused = parseSelectors(message)) {
552 return std::unexpected(std::move(*refused));
554 bool catchAll =
false;
555 for (skipSpace(); !atEnd(); skipSpace()) {
556 auto variant = parseVariant(message.selectors.size());
558 return std::unexpected(variant.error());
561 catchAll || std::ranges::all_of(variant->keys, [](std::string
const& key) { return key ==
"*"; });
562 message.variants.push_back(std::move(*variant));
565 return std::unexpected(std::string{
"`.match` needs a catch-all variant `*`"});
571 [[nodiscard]] std::optional<std::string> parseSelectors(MessagePattern& message) {
572 for (skipSpace(); !atEnd() && peek() ==
'$'; skipSpace()) {
573 auto selector = readVariable();
575 return std::move(selector.error());
577 bool const annotated = std::ranges::any_of(message.inputs, [&selector](
auto const& input) {
578 return input.first == *selector && input.second != MessageFunction::None;
581 return "the selector `$" + *selector +
"` needs an `.input` declaration with `:number` or `:string`";
583 message.selectors.push_back(std::move(*selector));
585 if (message.selectors.empty()) {
586 return std::string{
"`.match` needs at least one selector"};
592 [[nodiscard]] std::expected<MessageVariant, std::string> parseVariant(std::size_t selectors) {
593 MessageVariant variant;
594 while (!atEnd() && !_text.substr(_pos).starts_with(
"{{")) {
595 auto key = readKey();
597 return std::unexpected(key.error());
599 variant.keys.push_back(std::move(*key));
603 return std::unexpected(std::string{
"a variant needs a quoted pattern `{{...}}`"});
605 if (variant.keys.size() != selectors) {
606 return std::unexpected(std::string{
"a variant has one key per selector"});
609 auto pattern = parsePattern(
true);
611 return std::unexpected(pattern.error());
613 variant.pattern = std::move(*pattern);
617 std::string_view _text;
618 std::size_t _pos = 0;
631[[nodiscard]]
inline std::expected<MessagePattern, std::string>
parseMessage(std::string_view
text) {
656[[nodiscard]]
inline std::optional<math::Rational>
parseExactKey(std::string_view key) {
657 bool const negative = key.starts_with(
'-');
659 key.remove_prefix(1);
661 std::int64_t numerator = 0;
662 std::int64_t denominator = 1;
663 std::uint32_t digits = 0;
664 bool fraction =
false;
665 for (
char const chr : key) {
666 if (chr ==
'.' && !fraction && digits > 0) {
673 numerator = (numerator * 10) + (chr -
'0');
678 if (digits == 0 || key.ends_with(
'.')) {
698 if (
text !=
nullptr && *
text == key) {
703 auto const number =
numberOf(value);
707 std::string_view exact = key;
708 if (exact.starts_with(
'=')) {
709 exact.remove_prefix(1);
715 if (shown == *wanted) {
733 std::vector<std::pair<std::string, MessageFunction>>
const& functions,
736 if (part.variable.empty()) {
737 out.
text += part.text;
741 for (
auto const& [name, declared] : functions) {
746 auto const found = args.find(part.variable);
747 Value const missing{};
748 Value const& value = found == args.end() ? missing : found->second;
757 out.
text +=
"{$" + part.variable +
"}";
758 out.
errors.push_back(found == args.end() || value.
isNull()
759 ?
"the argument `" + part.variable +
"` is missing"
760 :
"the argument `" + part.variable +
"` is not a " +
783 std::vector<int> bestScore;
785 std::vector<int> score;
787 for (std::size_t index = 0; index < message.
selectors.size() && matches; ++index) {
788 std::string
const& selector = message.
selectors.at(index);
790 for (
auto const& [name, declared] : message.
inputs) {
791 if (name == selector) {
795 auto const found = args.find(selector);
796 Value const missing{};
797 auto const rank =
detail::matchKey(variant.
keys.at(index), found == args.end() ? missing : found->second,
799 matches = rank.has_value();
800 score.push_back(rank.value_or(3));
802 if (matches && (best ==
nullptr || score < bestScore)) {
804 bestScore = std::move(score);
808 if (best !=
nullptr) {
Node text(TextOptions const &options)
A text node.
The document's typed value (spec 5 §3): what every expression evaluates to.
Quantity const * asQuantity() const noexcept
The quantity, if held.
Decimal const * asDecimal() const noexcept
The decimal, if held.
bool isNull() const noexcept
Whether the value is null.
std::int64_t const * asInt() const noexcept
The int, if held.
std::string const * asString() const noexcept
The string, if held.
A recursive-descent reader over one message's text.
MessageReader(std::string_view text)
Reads text.
std::expected< MessagePattern, std::string > parse()
Parses the whole message.
constexpr PluralCategory eastSlavicPlural(PluralOperands const &n) noexcept
ru, uk: one for 1 outside 11, few for 2..4 outside 12..14, many for other integers.
PluralCategory
A CLDR plural category.
constexpr std::string_view categoryName(PluralCategory category)
The keyword of a plural category, as a .match key spells it.
std::optional< math::Rational > parseExactKey(std::string_view key)
Reads an exact-number variant key: -?[0-9]+(\.[0-9]+)?, at most 18 digits.
std::optional< std::string > formatNumber(Value const &value, render::NumericLocale const &numeric)
Formats a number for display in a locale: at its own places, rounded half to even.
PluralCategory(*)(PluralOperands const &) noexcept PluralRule
A language's plural rule.
constexpr PluralCategory polishPlural(PluralOperands const &n) noexcept
pl: one, few for 2..4 outside 12..14, many for other integers, other for fractions.
FormattedMessage formatMessage(MessagePattern const &message, ValueRecord const &args, MessageLocale const &locale)
Formats a parsed message with named arguments.
std::optional< std::pair< math::Rational, std::uint32_t > > numberOf(Value const &value)
The places a number is shown with: an int has none, a decimal or quantity its declared places.
constexpr bool wholeMillion(PluralOperands const &n) noexcept
CLDR's many of the Romance languages: an integer that is a non-zero multiple of a million.
constexpr PluralCategory arabicPlural(PluralOperands const &n) noexcept
ar: zero, one, two, few for 3..10 and many for 11..99 (both modulo 100).
constexpr PluralCategory spanishPlural(PluralOperands const &n) noexcept
es: one is n = 1, and many a whole million.
constexpr PluralCategory italianPlural(PluralOperands const &n) noexcept
it: as English, and many for a whole million.
constexpr std::array< std::pair< std::string_view, PluralRule >, 13 > kPluralRules
The languages whose rules are carried, by language subtag.
constexpr bool between(std::uint64_t operand, std::uint64_t low, std::uint64_t high) noexcept
Whether an operand lies in a CLDR range low..high.
PluralCategory pluralCategory(std::string_view locale, math::Rational const &value, std::uint32_t places)
The CLDR plural category of a number in a locale.
MessageFunction
The formatting function a placeholder or a declaration names.
@ String
:string: the text, and selection by exact match.
@ None
No annotation: a string as it is, a number as :number formats it.
@ Number
:number: locale number formatting, and plural selection.
std::optional< int > matchKey(std::string_view key, Value const &value, MessageFunction function, MessageLocale const &locale)
Whether a variant key matches a selector's value.
void formatPattern(std::vector< MessagePart > const &pattern, ValueRecord const &args, std::vector< std::pair< std::string, MessageFunction > > const &functions, MessageLocale const &locale, FormattedMessage &out)
Formats one pattern.
constexpr PluralCategory czechPlural(PluralOperands const &n) noexcept
cs: one, few for 2..4, many for fractions.
std::expected< MessagePattern, std::string > parseMessage(std::string_view text)
Parses a message in the ui/1 subset of MessageFormat 2.
std::string decimalText(math::Rational const &value, std::uint32_t places)
The decimal text of an exact value at a number of places: rounded half to even, then written with exa...
constexpr PluralCategory germanicPlural(PluralOperands const &n) noexcept
en, de, nl, sv: one is i = 1 and v = 0.
constexpr PluralCategory frenchPlural(PluralOperands const &n) noexcept
fr, pt: one is i = 0..1, and many a whole million.
Exact rational arithmetic for morph model/action values.
constexpr Rational roundToDecimalPlaces(const Rational &value, DecimalPlaces places, RoundingMode mode=RoundingMode::HalfAwayFromZero) noexcept
Rounds to a fixed number of decimal places exactly, without leaving the Rational domain – the operati...
@ HalfEven
Ties go to the neighbour whose last kept digit is even: 1.25 -> 1.2, 1.35 -> 1.4.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Strong type for a decimal-precision count.
Strong type for a Rational denominator.
Strong type for a Rational numerator.
Exact rational number with a runtime decimal-precision tag.
std::int64_t numerator
Signed numerator. Carries the sign of the rational value.
std::int64_t denominator
Strictly positive denominator. Never zero, never negative.
The locale facts both control-edge conversions need, in one designated-initialisable aggregate.
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
The locale a message is formatted for.
std::string locale
The BCP 47 locale, which selects the plural rules.
render::NumericLocale numeric
The locale's number formatting.
One part of a pattern: literal text, or a placeholder of a variable.
std::string text
The literal text; empty for a placeholder.
MessageFunction function
The placeholder's annotation.
std::string variable
The variable a placeholder reads, without $; empty for text.
A parsed message in the ui/1 subset of MessageFormat 2.
std::vector< MessagePart > pattern
The pattern of a message without .match.
std::vector< std::string > selectors
The .match selectors, as variable names; empty for a message without .match.
std::vector< MessageVariant > variants
The variants of a .match.
std::vector< std::pair< std::string, MessageFunction > > inputs
The .input declarations: each variable with its function.
One variant of a .match: a key per selector (* is the catch-all) and its pattern.
std::vector< MessagePart > pattern
The pattern shown when the variant is selected.
std::vector< std::string > keys
The keys, one per selector.
An exact decimal with a unit: the id of a unit of the server's unit systems (x-unitAlternatives).
CLDR's plural operands of a number as it is shown.
std::uint64_t integer
i: the integer digits of the absolute value.
bool visibleFraction
v != 0: whether the number shows fraction digits.
bool whole
Whether n has no fraction.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Node text(Text spec)
Static or bound text.