Skip to content

Commit

Permalink
Add support of local numbering systems for number symbols
Browse files Browse the repository at this point in the history
- Load number symbols for multiple number systems from cldr data
- Add numbering_systems and default_numbering_system properties for Locale
- Use default numbering system of the locale for formatting number symbols

Fixes partially issue python-babel#446
  • Loading branch information
kajte committed Oct 24, 2023
1 parent 65de3dc commit b0d8b60
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 16 deletions.
29 changes: 26 additions & 3 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class Locale:
`Locale` objects provide access to a collection of locale data, such as
territory and language names, number and date format patterns, and more:
>>> locale.number_symbols['decimal']
>>> locale.number_symbols['latn']['decimal']
u'.'
If a locale is requested for which no locale data is available, an
Expand Down Expand Up @@ -625,16 +625,39 @@ def currency_symbols(self) -> localedata.LocaleDataDict:

@property
def number_symbols(self) -> localedata.LocaleDataDict:
"""Symbols used in number formatting.
"""Symbols used in number formatting by number system.
.. note:: The format of the value returned may change between
Babel versions.
>>> Locale('fr', 'FR').number_symbols['decimal']
>>> Locale('fr', 'FR').number_symbols["latn"]['decimal']
u','
>>> Locale('fa', 'IR').number_symbols["arabext"]['decimal']
u'٫'
>>> Locale('fa', 'IR').number_symbols["latn"]['decimal']
u'.'
"""
return self._data['number_symbols']

@property
def other_numbering_systems(self) -> localedata.LocaleDataDict:
"""Mapping of othern numbering systems.
>>> Locale('el', 'GR').other_numbering_systems['traditional']
u'grek'
.. note:: The format of the value returned may change between
Babel versions.
"""
return self._data['numbering_systems']

@property
def default_numbering_system(self) -> str:
"""The default numbering systems used by the locale.
>>> Locale('el', 'GR').default_numbering_system
u'latn'
"""
return self._data['default_numbering_system']

@property
def decimal_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for decimal number formatting.
Expand Down
18 changes: 12 additions & 6 deletions babel/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ def get_decimal_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('decimal', '.')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('decimal', '.')


def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
Expand All @@ -336,7 +337,8 @@ def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('plusSign', '+')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('plusSign', '+')


def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
Expand All @@ -347,7 +349,8 @@ def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('minusSign', '-')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('minusSign', '-')


def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
Expand All @@ -358,7 +361,8 @@ def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('exponential', 'E')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('exponential', 'E')


def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
Expand All @@ -369,7 +373,8 @@ def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('group', ',')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('group', ',')


def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
Expand All @@ -380,7 +385,8 @@ def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
:param locale: the `Locale` object or locale identifier
"""
return Locale.parse(locale).number_symbols.get('infinity', '∞')
parsed_locale = Locale.parse(locale)
return parsed_locale.number_symbols[parsed_locale.default_numbering_system].get('infinity', '∞')


def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
Expand Down
24 changes: 19 additions & 5 deletions scripts/import_cldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False):
parse_interval_formats(data, calendar)

parse_number_symbols(data, tree)
parse_numbering_systems(data, tree)
parse_decimal_formats(data, tree)
parse_scientific_formats(data, tree)
parse_percent_formats(data, tree)
Expand Down Expand Up @@ -739,14 +740,27 @@ def parse_calendar_datetime_skeletons(data, calendar):

def parse_number_symbols(data, tree):
number_symbols = data.setdefault('number_symbols', {})
for symbol_elem in tree.findall('.//numbers/symbols'):
if _should_skip_number_elem(data, symbol_elem): # TODO: Support other number systems
for symbol_system_elem in tree.findall('.//numbers/symbols'):
number_system = symbol_system_elem.get('numberSystem')
if not number_system:
continue

for elem in symbol_elem.findall('./*'):
if _should_skip_elem(elem):
for symbol_element in symbol_system_elem.findall('./*'):
if _should_skip_elem(symbol_element):
continue
number_symbols[elem.tag] = str(elem.text)

number_symbols.setdefault(number_system, {})[symbol_element.tag] = str(symbol_element.text)


def parse_numbering_systems(data, tree):
default_number_system_node = tree.find('.//numbers/defaultNumberingSystem')
if default_number_system_node is not None:
data['default_numbering_system'] = default_number_system_node.text

numbering_systems = data.setdefault('numbering_systems', {})
other_numbering_systems_node = tree.find('.//numbers/otherNumberingSystems') or []
for system in other_numbering_systems_node:
numbering_systems[system.tag] = system.text


def parse_decimal_formats(data, tree):
Expand Down
22 changes: 20 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
def test_locale_provides_access_to_cldr_locale_data():
locale = Locale('en', 'US')
assert locale.display_name == 'English (United States)'
assert locale.number_symbols['decimal'] == '.'
assert locale.number_symbols["latn"]['decimal'] == '.'


def test_locale_repr():
Expand Down Expand Up @@ -162,7 +162,25 @@ def test_currency_symbols_property(self):
assert Locale('es', 'CO').currency_symbols['USD'] == 'US$'

def test_number_symbols_property(self):
assert Locale('fr', 'FR').number_symbols['decimal'] == ','
assert Locale('fr', 'FR').number_symbols["latn"]['decimal'] == ','
assert Locale('ar', 'IL').number_symbols["arab"]['percentSign'] == \u061c'
assert Locale('ar', 'IL').number_symbols["latn"]['percentSign'] == '\u200e%\u200e'

def test_other_numbering_systems_property(self):
assert Locale('fr', 'FR').other_numbering_systems['native'] == 'latn'
assert 'traditional' not in Locale('fr', 'FR').other_numbering_systems

assert Locale('el', 'GR').other_numbering_systems['native'] == 'latn'
assert Locale('el', 'GR').other_numbering_systems['traditional'] == 'grek'

def test_default_numbering_systems_property(self):
assert Locale('en', 'GB').default_numbering_system == 'latn'
assert Locale('ar', 'EG').default_numbering_system == 'arab'

@pytest.mark.all_locales
def test_all_locales_have_default_numbering_system(self, locale):
locale = Locale.parse(locale)
assert locale.default_numbering_system

def test_decimal_formats(self):
assert Locale('en', 'US').decimal_formats[None].pattern == '#,##0.###'
Expand Down
12 changes: 12 additions & 0 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,23 +317,35 @@ def test_get_territory_currencies():

def test_get_decimal_symbol():
assert numbers.get_decimal_symbol('en_US') == '.'
assert numbers.get_decimal_symbol('sv_SE') == ','
assert numbers.get_decimal_symbol('ar_EG') == '٫'


def test_get_plus_sign_symbol():
assert numbers.get_plus_sign_symbol('en_US') == '+'
assert numbers.get_plus_sign_symbol('ar_EG') == '\u061c+'


def test_get_minus_sign_symbol():
assert numbers.get_minus_sign_symbol('en_US') == '-'
assert numbers.get_minus_sign_symbol('nl_NL') == '-'
assert numbers.get_minus_sign_symbol('ar_EG') == '\u061c-'


def test_get_exponential_symbol():
assert numbers.get_exponential_symbol('en_US') == 'E'
assert numbers.get_exponential_symbol('ja_JP') == 'E'
assert numbers.get_exponential_symbol('ar_EG') == 'اس'


def test_get_group_symbol():
assert numbers.get_group_symbol('en_US') == ','
assert numbers.get_group_symbol('sv') == "\xa0"
assert numbers.get_group_symbol('ar_EG') == '٬'


def test_get_infinity_symbol():
assert numbers.get_infinity_symbol('en_US') == '∞'


def test_decimal_precision():
Expand Down

0 comments on commit b0d8b60

Please sign in to comment.