indgroup
Format and strictly parse numbers with Indian lakh and crore grouping (12,34,567) and the
digit grouping of every Unicode CLDR locale: 1.234.567,89 in German, 1'234'567.89 in
Swiss German, a narrow no-break space in French, 1234 but 12.345 in Spanish. Exact for
int, float and Decimal, typed, zero dependencies.
Install
pip install indgroup
Usage
The Indian system groups the last three digits, then the rest in pairs, so a million reads
as 10,00,000 and a crore as 1,00,00,000.
from decimal import Decimal
from indgroup import format_indian, parse_indian
print(format_indian(1234567))
print(format_indian(-12596475))
print(format_indian(Decimal("1234567.50")))
print(format_indian(0.1))
print(format_indian(1234567, separator=" "))
print(repr(parse_indian("12,34,567")))
print(repr(parse_indian("-1,234.50")))
# 12,34,567
# -1,25,96,475
# 12,34,567.50
# 0.1
# 12 34 567
# 1234567
# Decimal('-1234.50')
Any locale, with the symbols and grouping CLDR records for it. grouping takes the
useGrouping vocabulary of Intl.NumberFormat: auto honours the locale's minimum
grouping digits, min2 asks for at least two digits before the first separator, always
groups every number longer than a group, off never groups.
from indgroup import format_number
print(format_number(1234567.89, "de"))
print(format_number(1234567.89, "de-CH"))
print(ascii(format_number(1234567.89, "fr")))
print(format_number(1234567, "hi"))
print(format_number(1234, "es"), format_number(12345, "es"))
print(format_number(1234, "es", grouping="always"))
print(format_number(1234, "en", grouping="min2"))
print(ascii(format_number(-5, "sv")))
# 1.234.567,89
# 1'234'567.89
# '1\u202f234\u202f567,89'
# 12,34,567
# 1234 12.345
# 1.234
# 1234
# '\u22125'
Nothing is rounded: a Decimal keeps every digit it has, trailing zeros included, and a
float is read through its shortest round-trip spelling, so 0.1 prints 0.1 and not the
binary value it stands for, and a whole float such as 5.0 prints 5, as in Intl. Values
with more than a million integer digits are refused with ValueError.
Parsing is strict. It takes the locale's own minus, decimal and group symbols, ASCII digits
only, and groups exactly where the locale writes them, or none at all. A value without a
fraction comes back as int, one with a fraction as a Decimal with every digit kept, so
formatting the result gives the same text again; the one exception is -0, which reads back
as the int 0. Surrounding spaces, tabs and line breaks are ignored, but a group symbol is
never taken for one, even when it is a no-break space.
from indgroup import number_symbols, parse_number, resolve_locale
print(repr(parse_number("1.234.567,5", "de")))
print(repr(parse_number("12,34,567", "en-IN")))
for text in ["1,234,567", "1 234"]:
try:
parse_number(text, "en-IN")
except ValueError:
print("rejected", text)
print(number_symbols("en-IN"))
print(resolve_locale("pt_BR.UTF-8"), resolve_locale("zh-TW"), resolve_locale("iw"))
# Decimal('1234567.5')
# 1234567
# rejected 1,234,567
# rejected 1 234
# NumberSymbols(locale='en-IN', decimal='.', group=',', minus='-', primary=3, secondary=2, min_grouping=1)
# pt zh-Hant he
Command line
$ indgroup 1234567 -1234.5
12,34,567
-1,234.5
$ indgroup -l de 1234567.89
1.234.567,89
$ indgroup --parse 12,34,567.50
1234567.50
Values come from the arguments or from stdin, one per line. -l/--locale picks the locale
(default en-IN), -g/--grouping the strategy, -p/--parse reads grouped values back,
--locales lists the locales. Put -- before a negative value other than plain digits,
such as -1e5 or -1.234,5.
Errors exit with status 2.
API
| Name | What it does |
|---|---|
format_indian(value, *, separator=",") |
Indian lakh and crore grouping, any single separator |
parse_indian(text, *, separator=",") |
strict inverse of format_indian, int or Decimal |
format_number(value, locale, *, grouping="auto") |
the locale's symbols and grouping, no rounding |
parse_number(text, locale) |
strict inverse of format_number |
number_symbols(locale) |
the NumberSymbols a locale uses |
NumberSymbols |
frozen value: locale, decimal, group, minus, primary, secondary, min_grouping |
resolve_locale(locale) |
the CLDR locale a tag reads, or LookupError |
supported_locales() |
the 766 CLDR locales, sorted |
cldr_version() |
the CLDR release of the table, 48.0.0 |
Number |
int | float | Decimal |
Grouping |
Literal["auto", "min2", "always", "off"] |
Wrong types raise TypeError; NaN, infinities, unknown grouping values and malformed text
raise ValueError; an unknown language raises LookupError instead of silently reading
English.
How it compares with Intl.NumberFormat
The 650 locales ICU ships, the four grouping strategies and a set of integers and decimals
are checked against Intl.NumberFormat of Node 24.18 (ICU 78.3, CLDR 48) with Latin digits.
They agree everywhere except Swiss French, where the ICU data writes an apostrophe while
CLDR 48 gives fr-CH no group symbol of its own, so it inherits the narrow no-break space
of fr. This library follows CLDR. Other differences are by design:
- Digits are always Latin (
0to9); the native digits some locales default to, such as Arabic-Indic, are out of scope. - Values are never rounded;
Intl.NumberFormatshows three fraction digits by default. - Only the decimal pattern is used: no percent, currency, compact or scientific notation.
Why
Python's locale module depends on what the operating system has installed, the en_IN
locale is absent on most systems, and format(n, ",") knows only groups of three. Babel
covers CLDR but is a large dependency that rounds to the pattern's fraction digits, and
the small packages that mention lakh and crore convert numbers to words or are tied to template
filters. This is a plain formatter and a strict parser with exact Decimal support.
Development
pip install -r requirements-dev.txt
python scripts/fetch_upstream.py # optional: the CLDR, ICU and Intl reference files
pwsh scripts/check.ps1
The table in src/indgroup/_cldr.json is derived by scripts/build_table.py from the CLDR
JSON packages. The reference files are not part of the repository; their tests skip while
they are absent. INDGROUP_E2E=1 adds a test that builds, installs and drives the wheel.
License
MIT, see LICENSE. The number data is derived from the Unicode CLDR and ships under the Unicode License v3, see LICENSE-UNICODE.
Links
- Website: https://lianes8studio.com
- GitHub: https://github.com/JaviLianes8
- LinkedIn: https://www.linkedin.com/in/jlianes/
Metadata
Release files for indgroup 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| indgroup-0.2.0.tar.gz | 35.5 kB | Without provenance | Details for indgroup-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| indgroup-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for indgroup-0.2.0-py3-none-any.whl |
Total release size: 58.5 kB
Release files / indgroup-0.2.0.tar.gz
| Download URL | indgroup-0.2.0.tar.gz |
|---|---|
| Size | 35.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22f62a904a9050c89ef32cf040b85c21dafeec438ee72de36b09d018cff73754
|
|
BLAKE2b-256 checksum How to use checksums |
9cf9895a3e8d5571a635263c4777453d9aae209ea9cfedbe6a759b3686204666
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / indgroup-0.2.0-py3-none-any.whl
| Download URL | indgroup-0.2.0-py3-none-any.whl |
|---|---|
| Size | 23.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
938b728a3a39773955e70d062b7d97c13f685ba9a33365a32c79c617646d4276
|
|
BLAKE2b-256 checksum How to use checksums |
d956831d0120876b7aa6ff431d7711f98199457e4b50c3aba3c96ad08e91efdb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Security for indgroup 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.