indgroup 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. indgroup-0.2.0/.gitattributes +1 -0
  2. indgroup-0.2.0/.gitignore +16 -0
  3. indgroup-0.2.0/CHANGELOG.md +45 -0
  4. indgroup-0.2.0/CONTRIBUTING.md +19 -0
  5. indgroup-0.2.0/LICENSE +21 -0
  6. indgroup-0.2.0/LICENSE-UNICODE +41 -0
  7. indgroup-0.2.0/PKG-INFO +215 -0
  8. indgroup-0.2.0/README.md +177 -0
  9. indgroup-0.2.0/pyproject.toml +75 -0
  10. indgroup-0.2.0/requirements-dev.txt +3 -0
  11. indgroup-0.2.0/scripts/build_table.py +153 -0
  12. indgroup-0.2.0/scripts/check.ps1 +16 -0
  13. indgroup-0.2.0/scripts/fetch_upstream.py +54 -0
  14. indgroup-0.2.0/scripts/intl_reference.mjs +59 -0
  15. indgroup-0.2.0/scripts/release.ps1 +28 -0
  16. indgroup-0.2.0/src/indgroup/__init__.py +25 -0
  17. indgroup-0.2.0/src/indgroup/__main__.py +7 -0
  18. indgroup-0.2.0/src/indgroup/_cldr.json +1 -0
  19. indgroup-0.2.0/src/indgroup/_cli.py +108 -0
  20. indgroup-0.2.0/src/indgroup/_digits.py +70 -0
  21. indgroup-0.2.0/src/indgroup/_grouping.py +42 -0
  22. indgroup-0.2.0/src/indgroup/_locale.py +120 -0
  23. indgroup-0.2.0/src/indgroup/_number.py +54 -0
  24. indgroup-0.2.0/src/indgroup/_symbols.py +35 -0
  25. indgroup-0.2.0/src/indgroup/_value.py +47 -0
  26. indgroup-0.2.0/src/indgroup/py.typed +0 -0
  27. indgroup-0.2.0/tests/conftest.py +29 -0
  28. indgroup-0.2.0/tests/test_cli.py +85 -0
  29. indgroup-0.2.0/tests/test_contract.py +42 -0
  30. indgroup-0.2.0/tests/test_docs.py +94 -0
  31. indgroup-0.2.0/tests/test_e2e_wheel.py +118 -0
  32. indgroup-0.2.0/tests/test_grouping.py +144 -0
  33. indgroup-0.2.0/tests/test_intl.py +65 -0
  34. indgroup-0.2.0/tests/test_locale.py +61 -0
  35. indgroup-0.2.0/tests/test_number.py +133 -0
  36. indgroup-0.2.0/tests/test_review_fixes.py +68 -0
  37. indgroup-0.2.0/tests/test_sources.py +104 -0
  38. indgroup-0.2.0/tests/upstream/MANIFEST.json +50 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Third-party reference files, fetched locally by scripts/fetch_upstream.py; only the manifest is tracked.
15
+ tests/upstream/*
16
+ !tests/upstream/MANIFEST.json
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0]
10
+
11
+ ### Added
12
+
13
+ - `format_number` writes a number the way a locale does, from the number data of all 766
14
+ locales of Unicode CLDR 48: decimal, group and minus symbols, primary and secondary
15
+ grouping sizes, and minimum grouping digits (Spanish `1234` but `12.345`). `grouping`
16
+ takes the `useGrouping` vocabulary of `Intl.NumberFormat`: `auto`, `min2`, `always`, `off`.
17
+ - `parse_number`, its strict inverse.
18
+ - `number_symbols` and the frozen `NumberSymbols` value it returns.
19
+ - `resolve_locale`, `supported_locales` and `cldr_version`. Tags may be BCP 47 or POSIX;
20
+ deprecated codes, likely scripts and CLDR parent locales are honoured, and an unknown
21
+ language raises `LookupError`.
22
+ - `Number` and `Grouping` type aliases.
23
+ - An `indgroup` command and `python -m indgroup`.
24
+ - The Unicode License v3 ships with the package for the derived CLDR data.
25
+
26
+ ### Changed
27
+
28
+ - `format_indian` accepts `float`, read through its shortest round-trip spelling so no
29
+ binary drift shows (`0.1` prints `0.1`, `5.0` prints `5`). Before, a float raised
30
+ `TypeError`.
31
+ - Values with more than a million integer digits raise `ValueError`; below that, any length
32
+ works, past the 4300-digit limit of `int` string conversion.
33
+ - Parsing strips only ASCII spaces, tabs and line breaks around the text, never a group
34
+ symbol such as a no-break space.
35
+ - `parse_indian` accepts ASCII digits only. Before, any Unicode decimal digit passed, so
36
+ `parse_indian` read Arabic-Indic digits the formatter never writes.
37
+ - The separator may not be `+` or `-`, which made signed output ambiguous, and must be a
38
+ `str` (`TypeError` otherwise).
39
+
40
+ ## [0.1.0]
41
+
42
+ - Add `format_indian` to render integers and decimals with Indian lakh and crore
43
+ grouping, with a configurable separator and support for negatives.
44
+ - Add `parse_indian` to read a grouped value back to an `int` or `Decimal`,
45
+ rejecting Western grouping and malformed input.
@@ -0,0 +1,19 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project keeps a small, predictable shape.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ pip install -r requirements-dev.txt
9
+ ```
10
+
11
+ ## Before you open a pull request
12
+
13
+ - Keep public API in `src/indgroup/__init__.py`; internals live in `_`-prefixed modules.
14
+ - Add a test for the behaviour you change. Tests inject time, disk and network; they
15
+ never touch the real thing.
16
+ - Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
17
+ - One logical change per commit, imperative subject line.
18
+
19
+ Open an issue first if the change is large or shifts the public API.
indgroup-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ UNICODE LICENSE V3
2
+
3
+ COPYRIGHT AND PERMISSION NOTICE
4
+
5
+ Copyright © 2004-2025 Unicode, Inc.
6
+
7
+ NOTICE TO USER: Carefully read the following legal agreement. BY
8
+ DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
9
+ SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
10
+ TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
11
+ DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a
14
+ copy of data files and any associated documentation (the "Data Files") or
15
+ software and any associated documentation (the "Software") to deal in the
16
+ Data Files or Software without restriction, including without limitation
17
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell
18
+ copies of the Data Files or Software, and to permit persons to whom the
19
+ Data Files or Software are furnished to do so, provided that either (a)
20
+ this copyright and permission notice appear with all copies of the Data
21
+ Files or Software, or (b) this copyright and permission notice appear in
22
+ associated Documentation.
23
+
24
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
25
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
27
+ THIRD PARTY RIGHTS.
28
+
29
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
30
+ BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
31
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
32
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
33
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
34
+ FILES OR SOFTWARE.
35
+
36
+ Except as contained in this notice, the name of a copyright holder shall
37
+ not be used in advertising or otherwise to promote the sale, use or other
38
+ dealings in these Data Files or Software without prior written
39
+ authorization of the copyright holder.
40
+
41
+ SPDX-License-Identifier: Unicode-3.0
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.5
2
+ Name: indgroup
3
+ Version: 0.2.0
4
+ Summary: Format and strictly parse numbers with Indian lakh and crore grouping and the digit grouping of every Unicode CLDR locale
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/indgroup
7
+ Project-URL: Issues, https://github.com/JaviLianes8/indgroup/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/indgroup/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT AND Unicode-3.0
13
+ License-File: LICENSE
14
+ License-File: LICENSE-UNICODE
15
+ Keywords: cldr,crore,decimal,digit grouping,i18n,indian numbering,intl,l10n,lakh,number format,numberformat,thousands separator
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Software Development :: Internationalization
27
+ Classifier: Topic :: Software Development :: Localization
28
+ Classifier: Topic :: Text Processing
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Provides-Extra: dev
32
+ Requires-Dist: build; extra == 'dev'
33
+ Requires-Dist: mypy; extra == 'dev'
34
+ Requires-Dist: pytest; extra == 'dev'
35
+ Requires-Dist: ruff; extra == 'dev'
36
+ Requires-Dist: twine; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # indgroup
40
+
41
+ Format and strictly parse numbers with Indian lakh and crore grouping (`12,34,567`) and the
42
+ digit grouping of every Unicode CLDR locale: `1.234.567,89` in German, `1'234'567.89` in
43
+ Swiss German, a narrow no-break space in French, `1234` but `12.345` in Spanish. Exact for
44
+ `int`, `float` and `Decimal`, typed, zero dependencies.
45
+
46
+ ## Install
47
+
48
+ ```
49
+ pip install indgroup
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ The Indian system groups the last three digits, then the rest in pairs, so a million reads
55
+ as `10,00,000` and a crore as `1,00,00,000`.
56
+
57
+ ```python
58
+ from decimal import Decimal
59
+ from indgroup import format_indian, parse_indian
60
+
61
+ print(format_indian(1234567))
62
+ print(format_indian(-12596475))
63
+ print(format_indian(Decimal("1234567.50")))
64
+ print(format_indian(0.1))
65
+ print(format_indian(1234567, separator=" "))
66
+ print(repr(parse_indian("12,34,567")))
67
+ print(repr(parse_indian("-1,234.50")))
68
+ # 12,34,567
69
+ # -1,25,96,475
70
+ # 12,34,567.50
71
+ # 0.1
72
+ # 12 34 567
73
+ # 1234567
74
+ # Decimal('-1234.50')
75
+ ```
76
+
77
+ Any locale, with the symbols and grouping CLDR records for it. `grouping` takes the
78
+ `useGrouping` vocabulary of `Intl.NumberFormat`: `auto` honours the locale's minimum
79
+ grouping digits, `min2` asks for at least two digits before the first separator, `always`
80
+ groups every number longer than a group, `off` never groups.
81
+
82
+ ```python
83
+ from indgroup import format_number
84
+
85
+ print(format_number(1234567.89, "de"))
86
+ print(format_number(1234567.89, "de-CH"))
87
+ print(ascii(format_number(1234567.89, "fr")))
88
+ print(format_number(1234567, "hi"))
89
+ print(format_number(1234, "es"), format_number(12345, "es"))
90
+ print(format_number(1234, "es", grouping="always"))
91
+ print(format_number(1234, "en", grouping="min2"))
92
+ print(ascii(format_number(-5, "sv")))
93
+ # 1.234.567,89
94
+ # 1'234'567.89
95
+ # '1\u202f234\u202f567,89'
96
+ # 12,34,567
97
+ # 1234 12.345
98
+ # 1.234
99
+ # 1234
100
+ # '\u22125'
101
+ ```
102
+
103
+ Nothing is rounded: a `Decimal` keeps every digit it has, trailing zeros included, and a
104
+ `float` is read through its shortest round-trip spelling, so `0.1` prints `0.1` and not the
105
+ binary value it stands for, and a whole float such as `5.0` prints `5`, as in Intl. Values
106
+ with more than a million integer digits are refused with `ValueError`.
107
+
108
+ Parsing is strict. It takes the locale's own minus, decimal and group symbols, ASCII digits
109
+ only, and groups exactly where the locale writes them, or none at all. A value without a
110
+ fraction comes back as `int`, one with a fraction as a `Decimal` with every digit kept, so
111
+ formatting the result gives the same text again; the one exception is `-0`, which reads back
112
+ as the `int` `0`. Surrounding spaces, tabs and line breaks are ignored, but a group symbol is
113
+ never taken for one, even when it is a no-break space.
114
+
115
+ ```python
116
+ from indgroup import number_symbols, parse_number, resolve_locale
117
+
118
+ print(repr(parse_number("1.234.567,5", "de")))
119
+ print(repr(parse_number("12,34,567", "en-IN")))
120
+ for text in ["1,234,567", "1 234"]:
121
+ try:
122
+ parse_number(text, "en-IN")
123
+ except ValueError:
124
+ print("rejected", text)
125
+ print(number_symbols("en-IN"))
126
+ print(resolve_locale("pt_BR.UTF-8"), resolve_locale("zh-TW"), resolve_locale("iw"))
127
+ # Decimal('1234567.5')
128
+ # 1234567
129
+ # rejected 1,234,567
130
+ # rejected 1 234
131
+ # NumberSymbols(locale='en-IN', decimal='.', group=',', minus='-', primary=3, secondary=2, min_grouping=1)
132
+ # pt zh-Hant he
133
+ ```
134
+
135
+ ## Command line
136
+
137
+ ```
138
+ $ indgroup 1234567 -1234.5
139
+ 12,34,567
140
+ -1,234.5
141
+ $ indgroup -l de 1234567.89
142
+ 1.234.567,89
143
+ $ indgroup --parse 12,34,567.50
144
+ 1234567.50
145
+ ```
146
+
147
+ Values come from the arguments or from stdin, one per line. `-l/--locale` picks the locale
148
+ (default `en-IN`), `-g/--grouping` the strategy, `-p/--parse` reads grouped values back,
149
+ `--locales` lists the locales. Put `--` before a negative value other than plain digits,
150
+ such as `-1e5` or `-1.234,5`.
151
+ Errors exit with status 2.
152
+
153
+ ## API
154
+
155
+ | Name | What it does |
156
+ | --- | --- |
157
+ | `format_indian(value, *, separator=",")` | Indian lakh and crore grouping, any single separator |
158
+ | `parse_indian(text, *, separator=",")` | strict inverse of `format_indian`, `int` or `Decimal` |
159
+ | `format_number(value, locale, *, grouping="auto")` | the locale's symbols and grouping, no rounding |
160
+ | `parse_number(text, locale)` | strict inverse of `format_number` |
161
+ | `number_symbols(locale)` | the `NumberSymbols` a locale uses |
162
+ | `NumberSymbols` | frozen value: `locale`, `decimal`, `group`, `minus`, `primary`, `secondary`, `min_grouping` |
163
+ | `resolve_locale(locale)` | the CLDR locale a tag reads, or `LookupError` |
164
+ | `supported_locales()` | the 766 CLDR locales, sorted |
165
+ | `cldr_version()` | the CLDR release of the table, `48.0.0` |
166
+ | `Number` | `int \| float \| Decimal` |
167
+ | `Grouping` | `Literal["auto", "min2", "always", "off"]` |
168
+
169
+ Wrong types raise `TypeError`; NaN, infinities, unknown grouping values and malformed text
170
+ raise `ValueError`; an unknown language raises `LookupError` instead of silently reading
171
+ English.
172
+
173
+ ## How it compares with `Intl.NumberFormat`
174
+
175
+ The 650 locales ICU ships, the four grouping strategies and a set of integers and decimals
176
+ are checked against `Intl.NumberFormat` of Node 24.18 (ICU 78.3, CLDR 48) with Latin digits.
177
+ They agree everywhere except Swiss French, where the ICU data writes an apostrophe while
178
+ CLDR 48 gives `fr-CH` no group symbol of its own, so it inherits the narrow no-break space
179
+ of `fr`. This library follows CLDR. Other differences are by design:
180
+
181
+ - Digits are always Latin (`0` to `9`); the native digits some locales default to, such as
182
+ Arabic-Indic, are out of scope.
183
+ - Values are never rounded; `Intl.NumberFormat` shows three fraction digits by default.
184
+ - Only the decimal pattern is used: no percent, currency, compact or scientific notation.
185
+
186
+ ## Why
187
+
188
+ Python's `locale` module depends on what the operating system has installed, the `en_IN`
189
+ locale is absent on most systems, and `format(n, ",")` knows only groups of three. Babel
190
+ covers CLDR but is a large dependency that rounds to the pattern's fraction digits, and
191
+ the small packages that mention lakh and crore convert numbers to words or are tied to template
192
+ filters. This is a plain formatter and a strict parser with exact `Decimal` support.
193
+
194
+ ## Development
195
+
196
+ ```
197
+ pip install -r requirements-dev.txt
198
+ python scripts/fetch_upstream.py # optional: the CLDR, ICU and Intl reference files
199
+ pwsh scripts/check.ps1
200
+ ```
201
+
202
+ The table in `src/indgroup/_cldr.json` is derived by `scripts/build_table.py` from the CLDR
203
+ JSON packages. The reference files are not part of the repository; their tests skip while
204
+ they are absent. `INDGROUP_E2E=1` adds a test that builds, installs and drives the wheel.
205
+
206
+ ## License
207
+
208
+ MIT, see [LICENSE](LICENSE). The number data is derived from the Unicode CLDR and ships
209
+ under the Unicode License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
210
+
211
+ ## Links
212
+
213
+ - Website: https://lianes8studio.com
214
+ - GitHub: https://github.com/JaviLianes8
215
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,177 @@
1
+ # indgroup
2
+
3
+ Format and strictly parse numbers with Indian lakh and crore grouping (`12,34,567`) and the
4
+ digit grouping of every Unicode CLDR locale: `1.234.567,89` in German, `1'234'567.89` in
5
+ Swiss German, a narrow no-break space in French, `1234` but `12.345` in Spanish. Exact for
6
+ `int`, `float` and `Decimal`, typed, zero dependencies.
7
+
8
+ ## Install
9
+
10
+ ```
11
+ pip install indgroup
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ The Indian system groups the last three digits, then the rest in pairs, so a million reads
17
+ as `10,00,000` and a crore as `1,00,00,000`.
18
+
19
+ ```python
20
+ from decimal import Decimal
21
+ from indgroup import format_indian, parse_indian
22
+
23
+ print(format_indian(1234567))
24
+ print(format_indian(-12596475))
25
+ print(format_indian(Decimal("1234567.50")))
26
+ print(format_indian(0.1))
27
+ print(format_indian(1234567, separator=" "))
28
+ print(repr(parse_indian("12,34,567")))
29
+ print(repr(parse_indian("-1,234.50")))
30
+ # 12,34,567
31
+ # -1,25,96,475
32
+ # 12,34,567.50
33
+ # 0.1
34
+ # 12 34 567
35
+ # 1234567
36
+ # Decimal('-1234.50')
37
+ ```
38
+
39
+ Any locale, with the symbols and grouping CLDR records for it. `grouping` takes the
40
+ `useGrouping` vocabulary of `Intl.NumberFormat`: `auto` honours the locale's minimum
41
+ grouping digits, `min2` asks for at least two digits before the first separator, `always`
42
+ groups every number longer than a group, `off` never groups.
43
+
44
+ ```python
45
+ from indgroup import format_number
46
+
47
+ print(format_number(1234567.89, "de"))
48
+ print(format_number(1234567.89, "de-CH"))
49
+ print(ascii(format_number(1234567.89, "fr")))
50
+ print(format_number(1234567, "hi"))
51
+ print(format_number(1234, "es"), format_number(12345, "es"))
52
+ print(format_number(1234, "es", grouping="always"))
53
+ print(format_number(1234, "en", grouping="min2"))
54
+ print(ascii(format_number(-5, "sv")))
55
+ # 1.234.567,89
56
+ # 1'234'567.89
57
+ # '1\u202f234\u202f567,89'
58
+ # 12,34,567
59
+ # 1234 12.345
60
+ # 1.234
61
+ # 1234
62
+ # '\u22125'
63
+ ```
64
+
65
+ Nothing is rounded: a `Decimal` keeps every digit it has, trailing zeros included, and a
66
+ `float` is read through its shortest round-trip spelling, so `0.1` prints `0.1` and not the
67
+ binary value it stands for, and a whole float such as `5.0` prints `5`, as in Intl. Values
68
+ with more than a million integer digits are refused with `ValueError`.
69
+
70
+ Parsing is strict. It takes the locale's own minus, decimal and group symbols, ASCII digits
71
+ only, and groups exactly where the locale writes them, or none at all. A value without a
72
+ fraction comes back as `int`, one with a fraction as a `Decimal` with every digit kept, so
73
+ formatting the result gives the same text again; the one exception is `-0`, which reads back
74
+ as the `int` `0`. Surrounding spaces, tabs and line breaks are ignored, but a group symbol is
75
+ never taken for one, even when it is a no-break space.
76
+
77
+ ```python
78
+ from indgroup import number_symbols, parse_number, resolve_locale
79
+
80
+ print(repr(parse_number("1.234.567,5", "de")))
81
+ print(repr(parse_number("12,34,567", "en-IN")))
82
+ for text in ["1,234,567", "1 234"]:
83
+ try:
84
+ parse_number(text, "en-IN")
85
+ except ValueError:
86
+ print("rejected", text)
87
+ print(number_symbols("en-IN"))
88
+ print(resolve_locale("pt_BR.UTF-8"), resolve_locale("zh-TW"), resolve_locale("iw"))
89
+ # Decimal('1234567.5')
90
+ # 1234567
91
+ # rejected 1,234,567
92
+ # rejected 1 234
93
+ # NumberSymbols(locale='en-IN', decimal='.', group=',', minus='-', primary=3, secondary=2, min_grouping=1)
94
+ # pt zh-Hant he
95
+ ```
96
+
97
+ ## Command line
98
+
99
+ ```
100
+ $ indgroup 1234567 -1234.5
101
+ 12,34,567
102
+ -1,234.5
103
+ $ indgroup -l de 1234567.89
104
+ 1.234.567,89
105
+ $ indgroup --parse 12,34,567.50
106
+ 1234567.50
107
+ ```
108
+
109
+ Values come from the arguments or from stdin, one per line. `-l/--locale` picks the locale
110
+ (default `en-IN`), `-g/--grouping` the strategy, `-p/--parse` reads grouped values back,
111
+ `--locales` lists the locales. Put `--` before a negative value other than plain digits,
112
+ such as `-1e5` or `-1.234,5`.
113
+ Errors exit with status 2.
114
+
115
+ ## API
116
+
117
+ | Name | What it does |
118
+ | --- | --- |
119
+ | `format_indian(value, *, separator=",")` | Indian lakh and crore grouping, any single separator |
120
+ | `parse_indian(text, *, separator=",")` | strict inverse of `format_indian`, `int` or `Decimal` |
121
+ | `format_number(value, locale, *, grouping="auto")` | the locale's symbols and grouping, no rounding |
122
+ | `parse_number(text, locale)` | strict inverse of `format_number` |
123
+ | `number_symbols(locale)` | the `NumberSymbols` a locale uses |
124
+ | `NumberSymbols` | frozen value: `locale`, `decimal`, `group`, `minus`, `primary`, `secondary`, `min_grouping` |
125
+ | `resolve_locale(locale)` | the CLDR locale a tag reads, or `LookupError` |
126
+ | `supported_locales()` | the 766 CLDR locales, sorted |
127
+ | `cldr_version()` | the CLDR release of the table, `48.0.0` |
128
+ | `Number` | `int \| float \| Decimal` |
129
+ | `Grouping` | `Literal["auto", "min2", "always", "off"]` |
130
+
131
+ Wrong types raise `TypeError`; NaN, infinities, unknown grouping values and malformed text
132
+ raise `ValueError`; an unknown language raises `LookupError` instead of silently reading
133
+ English.
134
+
135
+ ## How it compares with `Intl.NumberFormat`
136
+
137
+ The 650 locales ICU ships, the four grouping strategies and a set of integers and decimals
138
+ are checked against `Intl.NumberFormat` of Node 24.18 (ICU 78.3, CLDR 48) with Latin digits.
139
+ They agree everywhere except Swiss French, where the ICU data writes an apostrophe while
140
+ CLDR 48 gives `fr-CH` no group symbol of its own, so it inherits the narrow no-break space
141
+ of `fr`. This library follows CLDR. Other differences are by design:
142
+
143
+ - Digits are always Latin (`0` to `9`); the native digits some locales default to, such as
144
+ Arabic-Indic, are out of scope.
145
+ - Values are never rounded; `Intl.NumberFormat` shows three fraction digits by default.
146
+ - Only the decimal pattern is used: no percent, currency, compact or scientific notation.
147
+
148
+ ## Why
149
+
150
+ Python's `locale` module depends on what the operating system has installed, the `en_IN`
151
+ locale is absent on most systems, and `format(n, ",")` knows only groups of three. Babel
152
+ covers CLDR but is a large dependency that rounds to the pattern's fraction digits, and
153
+ the small packages that mention lakh and crore convert numbers to words or are tied to template
154
+ filters. This is a plain formatter and a strict parser with exact `Decimal` support.
155
+
156
+ ## Development
157
+
158
+ ```
159
+ pip install -r requirements-dev.txt
160
+ python scripts/fetch_upstream.py # optional: the CLDR, ICU and Intl reference files
161
+ pwsh scripts/check.ps1
162
+ ```
163
+
164
+ The table in `src/indgroup/_cldr.json` is derived by `scripts/build_table.py` from the CLDR
165
+ JSON packages. The reference files are not part of the repository; their tests skip while
166
+ they are absent. `INDGROUP_E2E=1` adds a test that builds, installs and drives the wheel.
167
+
168
+ ## License
169
+
170
+ MIT, see [LICENSE](LICENSE). The number data is derived from the Unicode CLDR and ships
171
+ under the Unicode License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
172
+
173
+ ## Links
174
+
175
+ - Website: https://lianes8studio.com
176
+ - GitHub: https://github.com/JaviLianes8
177
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "indgroup"
7
+ version = "0.2.0"
8
+ description = "Format and strictly parse numbers with Indian lakh and crore grouping and the digit grouping of every Unicode CLDR locale"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT AND Unicode-3.0"
12
+ license-files = ["LICENSE", "LICENSE-UNICODE"]
13
+ authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
14
+ keywords = ["lakh", "crore", "indian numbering", "digit grouping", "thousands separator", "number format", "cldr", "i18n", "l10n", "intl", "numberformat", "decimal"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Software Development :: Internationalization",
20
+ "Topic :: Software Development :: Localization",
21
+ "Topic :: Text Processing",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["ruff", "mypy", "pytest", "build", "twine"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://lianes8studio.com"
38
+ Repository = "https://github.com/JaviLianes8/indgroup"
39
+ Issues = "https://github.com/JaviLianes8/indgroup/issues"
40
+ Changelog = "https://github.com/JaviLianes8/indgroup/blob/main/CHANGELOG.md"
41
+ LinkedIn = "https://www.linkedin.com/in/jlianes/"
42
+ Funding = "https://buymeacoffee.com/jlianesglrs"
43
+
44
+ [project.scripts]
45
+ indgroup = "indgroup._cli:main"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/indgroup"]
49
+
50
+ [tool.hatch.build.targets.sdist]
51
+ # Third-party reference files are fetched locally and never shipped; only their manifest is.
52
+ exclude = [
53
+ ".github",
54
+ "IMPROVEMENTS-round*.md",
55
+ "tests/upstream/*.tgz",
56
+ "tests/upstream/*.cpp",
57
+ "tests/upstream/*.html",
58
+ "tests/upstream/intl-node.json",
59
+ ]
60
+
61
+ [tool.ruff]
62
+ line-length = 100
63
+ target-version = "py310"
64
+
65
+ [tool.ruff.lint]
66
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
67
+
68
+ [tool.mypy]
69
+ python_version = "3.10"
70
+ strict = true
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
74
+ pythonpath = ["src"]
75
+ addopts = "-q"
@@ -0,0 +1,3 @@
1
+ # Development dependencies: the package in editable mode plus its dev extras
2
+ # (ruff, mypy, pytest, build, twine). Install with: pip install -r requirements-dev.txt
3
+ -e .[dev]