NAME
Locale::Unicode::RBNF - Unicode CLDR Rule-Based Number Format, ported from ICU4C
SYNOPSIS
use Locale::Unicode::RBNF;
my $fmt = Locale::Unicode::RBNF->new(
locale => 'fr',
type => 'spellout',
cldr => '/path/to/cldr/common',
) || die( Locale::Unicode::RBNF->error );
say $fmt->format( 1980 );
# mille neuf cent quatre-vingts
say $fmt->format( 71, '%spellout-cardinal-feminine' );
# soixante-et-onze
say $fmt->format( 3.14 );
# trois virgule un quatre
my $n = $fmt->parse( 'deux mille vingt-six' );
# 2026
# Japanese, for instance for amounts written in kanji
my $ja = Locale::Unicode::RBNF->new( locale => 'ja', cldr => '/path/to/cldr/common' );
say $ja->format( 3000000, '%spellout-cardinal-financial' );
# Custom rules, as with ICU's RuleBasedNumberFormat( description, locale )
my $custom = Locale::Unicode::RBNF->new(
locale => 'en',
rules => "%main: 0: zero; 1: one; 2: two; 3: =#,##0=;",
);
VERSION
v0.1.0
DESCRIPTION
This is a port from ICU4C RuleBasedNumberFormat (ICU 79.1), that is rbnf.cpp, nfrs.cpp, nfrule.cpp and nfsubs.cpp, using the RBNF data of the Unicode CLDR.
It formats numbers using rules, typically to spell them out ("one thousand two hundred"), to produce ordinals ("1st", "1er"), or to write them in numbering systems such as Roman numerals or Hebrew numbers, and it parses such text back into numbers, in strict or lenient mode.
The rules, decimal format symbols, numbering system digits and plural rules are read from a CLDR common directory; see Locale::Unicode::RBNF::CLDR.
CONSTRUCTOR
new
Takes a hash or an hash reference of options, and returns a new object, or, upon error, sets an error that can be retrieved with "error" and returns undef in scalar context, or an empty list in list context.
capitalizationThe capitalisation context. See "capitalization".
cldrThe path to the CLDR common directory. It defaults to the environment variable
LOCALE_UNICODE_RBNF_CLDR. It is required, unlessrulesare provided, in which case root symbols and plural rules are used without it.default_rule_setThe name of the public rule set to use by default.
fatalBoolean. If true, errors are fatal, which means that the Locale::Unicode::RBNF::Exception object is passed to
dieinstead of being returned. The default follows the value set with$Locale::Unicode::RBNF::FATAL_EXCEPTIONS, which is false by default. See "ERROR HANDLING".lenientBoolean. Enables lenient parsing. Default false.
localeA locale, can be a BCP 47 identifier such as
fr,fr-CHorzh-Hant-TW, but also a CLDR identifier such asfr_CH, or also a Locale::Unicode object.It is parsed and canonicalised with Locale::Unicode, thus
fr-chbecomesfr-CH. A-u-nu-extension selects the numbering system, as inja-JP-u-nu-hanidec. The default isroot.If Locale::Unicode cannot parse a given locale, or if that locale does not have a language subtag, it will return an error.
The ICU keyword syntax, such as
fr_CH@numbers=latn, is not supported. You must use the BCP 47 extension instead.localizationsLocalised display names of the rule sets, in the ICU syntax, such as
<<%main, %other>, <en, Main, Other>, <fr, Principal, Autre>>. When provided, the first rule set listed becomes the default one.max_fraction_digitsThe maximum number of fraction digits used when rounding, if a rounding mode other than
unnecessaryis set. Default 3.numbering_systemOverrides the numbering system whose symbols and digits are used by decimal patterns in the rules. Default: the locale's default numbering system.
rounding_modeOne of
ceiling,floor,down,up,half_even,half_down,half_up,half_odd,half_ceiling,half_floororunnecessary(the default, under which numbers are not rounded before formatting).rulesA rule description in the ICU syntax, instead of the CLDR rules.
typeThe CLDR rule set grouping:
spellout(the default),ordinal,numbering_systemorduration.
METHODS
capitalization
Sets or gets the capitalisation context: none (default), middle_of_sentence, beginning_of_sentence, ui_list_or_menu or standalone. For the last two, the first word is titlecased only if the CLDR number-spellout context transforms of the locale say so.
default_rule_set
Sets or gets the name of the default rule set. An empty string restores the original default.
error
my $fmt = Locale::Unicode::RBNF->new( locale => 'fr', type => 'nonsense' ) ||
die( Locale::Unicode::RBNF->error );
say $fmt->error->message;
In accessor mode, this returns the last error, as a Locale::Unicode::RBNF::Exception object, or possibly undef if there were none.
When called as a class method, this returns the last error of any object.
When called mutator mode the arguments provided are concatenated, and a new error is set. It then returns undef in scalar context, or an empty list in list context. In object context, such as $fmt->format( 'abc' )->length, it returns a Locale::Unicode::RBNF::NullObject instead, so that the chain of method calls does not die.
If "fatal" is true, it dies with the exception instead.
See "ERROR HANDLING".
fatal
Sets or gets the boolean value for this option. If true, the exceptions will be fatal. By default, it follows the value set with $Locale::Unicode::RBNF::FATAL_EXCEPTIONS, which itself is false by default.
See "ERROR HANDLING".
format
my $text = $fmt->format( $number );
my $text = $fmt->format( $number, '%spellout-ordinal' );
Formats a number, using the given public rule set or the default one. Integers within the int64 range use ICU's integer code path, other numbers its double code path. Larger integers are formatted with the locale's decimal pattern, as ICU does.
format_double
Formats a number with ICU's double code path, even if it is integral.
format_int
Formats an integer with ICU's integer code path.
lenient
Sets or gets the lenient parsing mode.
locale
my $locale = $fmt->locale;
say $locale;
# fr-CH
Returns the canonical locale of the formatter as a Locale::Unicode object, which stringifies as a BCP 47 identifier, such as fr-CH or ja-JP-u-nu-hanidec, extensions included.
The locale root is returned as root, rather than as its canonical form und.
max_fraction_digits
Sets or gets the maximum number of fraction digits used for rounding.
numbering_system
Returns the numbering system used.
parse
my $n = $fmt->parse( $text );
my( $n, $index, $error_index ) = $fmt->parse( $text, $start );
Parses text into a number, trying every public and parseable rule set, and keeping the result that consumed the most characters. In list context, it returns the value, the index after the parsed text, and the error index (-1 on success).
pass_error
return( $self->pass_error( $other->error ) );
return( $self->pass_error );
Propagates the error stored in another object (or the class-level error) into the current object's error slot, without constructing a new exception, and then returns undef in scalar context, or an empty list in list context, or a Locale::Unicode::RBNF::NullObject in object context, just like "error" does.
Without any argument, the last error is passed on.
If the error is not an object, "error" is called with it.
If an hash reference is passed with the key class it will bless the error into that exception class provided.
Supported options are:
classThe name of a class name to re-bless the error object provided.
codeThe error code to set in the error object being passed.
errorThe error object to be passed on.
If this is not provided, it will get it with the object
errormethod, or the class global variable$ERROR
rounding_mode
Sets or gets the rounding mode.
rule_set_display_name
my $name = $fmt->rule_set_display_name( '%main', 'fr' );
Returns the localised name of a rule set, given by name or index, when localizations were provided.
rule_set_display_name_locales
Returns an array reference of the locales of the localised names.
rule_set_names
Returns an array reference of the public rule set names.
rule_sets_from_description
my $r = Locale::Unicode::RBNF->rule_sets_from_description( $description );
# {
# lenient_parse => undef,
# rule_sets => [
# {
# name => '%spellout-numbering-year',
# type => 'spellout-numbering-year',
# private => 0,
# parseable => 1,
# rules => [ { descriptor => 'x.x', text => '=0.0=' }, ... ],
# },
# ...
# ],
# }
Divides a rule description into its rule sets and rules, without compiling them, exactly as ICU does when it reads them: same whitespace stripping, same extraction of the %%lenient-parse rules, same division into rule sets and rules, and same separation of the rule descriptor from the rule text. It uses the very code of the constructor. This is meant for storing the rules, for instance in a database. It can be called as a class method, and returns undef upon error.
The rule text is kept as it is in the description, including a leading apostrophe, but without the terminating semicolon. See also "rbnf_groupings" in Locale::Unicode::RBNF::CLDR.
rules
Returns a textual description of the rules in use, equivalent to the original one.
symbols
Sets or gets the decimal format symbols, as an hash reference with the keys decimal, group, minus, plus, percent, per_mille, infinity, nan and digits (an array reference of 10 digits).
valid_locale
my $fmt = Locale::Unicode::RBNF->new( locale => 'fr-FR', cldr => $cldr );
say $fmt->valid_locale;
# fr
Returns the locale built using the CLDR rules, and found by following the locale inheritance chain, as a Locale::Unicode object, such as fr or root.
It returns undef when the rules were provided with the rules option.
ERROR HANDLING
Exceptions are built as Locale::Unicode::RBNF::Exception objects, which stringify as their message followed by the package, line and file where they occurred.
By default, a method that fails sets an error, retrievable with "error", emits it as a warning if the warnings category Locale::Unicode::RBNF is enabled in the caller, and returns undef in scalar context, an empty list in list context, or a Locale::Unicode::RBNF::NullObject in object context.
If the fatal option, or "fatal", is true, the error is raised with die instead, and can be caught with eval or try-catch if you are using Perl version 5.34.0, or higher. The global variable $Locale::Unicode::RBNF::FATAL_EXCEPTIONS sets the default for new objects, and applies to errors of class methods, such as those of the constructor.
{
local $Locale::Unicode::RBNF::FATAL_EXCEPTIONS = 1;
my $fmt = eval{ Locale::Unicode::RBNF->new( locale => 'x-private' ) };
warn( $@->message ) if( $@ );
}
PARSING ENGINE
Parsing is costly: as in ICU, it tries every rule of every public rule set, recursively. This distribution therefore includes a port to C of the parsing code path (Locale-Unicode-RBNF.xs), built when a C compiler is available, and used automatically. Formatting remains in Perl, since it is fast.
The pure Perl implementation, in Locale::Unicode::RBNF::PP, is the fallback: it is loaded automatically when the XS shared object cannot be loaded, such as when the distribution was installed without a C compiler, or when the environment variable PERL_LOCALE_UNICODE_RBNF_PP is set to a true value before the module is loaded. The variable $Locale::Unicode::RBNF::IsPurePerl tells which one is in use. Both give the same results; the test suite checks it.
The C engine is the internal class Locale::Unicode::RBNF::XS, created by the formatter when parsing first needs it. Its constructor new takes an hash reference of the rule sets, rules, substitutions, plural patterns and decimal formats of the formatter, flattened into arrays of hashes, together with the Perl callbacks it uses for lenient matching (case folding, separators, NFC boundaries and normalisation, and digit values), whose results it caches per code point. Its method parse, given the text and the lenient flag, returns the value, the number of characters matched and the error index. You should not normally use this class directly.
DIFFERENCES WITH ICU
Lenient parsing uses ICU 79's NFC and case-folding matching. ICU additionally falls back on a collator tailored with
%%lenient-parserules when a rule description provides some; collation tailoring is not available here, so that fallback never matches. Current CLDR data has no such rules.Where ICU creates a
NumberFormatfor the default locale, during lenient parsing, the decimal format of the formatter's locale is used.Decimal patterns support the subset needed by rule-based formats:
#,0,,,., literal affixes, percent and per mille. Significant digits, exponents and rounding increments are rejected.Contextual capitalisation titlecases the first character, without a sentence break iterator.
Parse results are computed with doubles, as ICU does. Beyond 2^53, the pure Perl engine may keep integer precision where ICU and the C engine round.
Where ICU converts an out of range double to an
int32_tto select a plural form, which is undefined behaviour in C++, the saturating semantics of Java and aarch64 are used.
AUTHOR
Jacques Deguest <jack@deguest.jp>
SEE ALSO
https://unicode-org.github.io/icu/userguide/format_parse/numbers/rbnf.html, https://www.unicode.org/reports/tr35/tr35-numbers.html#Rule-Based_Number_Formatting
COPYRIGHT & LICENSE
Copyright(c) 2026 DEGUEST Pte. Ltd.
Portions are derived from ICU, Copyright (C) 2016 and later Unicode, Inc. and others, under the Unicode License (https://www.unicode.org/copyright.html). Its text is in the LICENSE-UNICODE file of this distribution.
This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.