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.
- indgroup-0.2.0/.gitattributes +1 -0
- indgroup-0.2.0/.gitignore +16 -0
- indgroup-0.2.0/CHANGELOG.md +45 -0
- indgroup-0.2.0/CONTRIBUTING.md +19 -0
- indgroup-0.2.0/LICENSE +21 -0
- indgroup-0.2.0/LICENSE-UNICODE +41 -0
- indgroup-0.2.0/PKG-INFO +215 -0
- indgroup-0.2.0/README.md +177 -0
- indgroup-0.2.0/pyproject.toml +75 -0
- indgroup-0.2.0/requirements-dev.txt +3 -0
- indgroup-0.2.0/scripts/build_table.py +153 -0
- indgroup-0.2.0/scripts/check.ps1 +16 -0
- indgroup-0.2.0/scripts/fetch_upstream.py +54 -0
- indgroup-0.2.0/scripts/intl_reference.mjs +59 -0
- indgroup-0.2.0/scripts/release.ps1 +28 -0
- indgroup-0.2.0/src/indgroup/__init__.py +25 -0
- indgroup-0.2.0/src/indgroup/__main__.py +7 -0
- indgroup-0.2.0/src/indgroup/_cldr.json +1 -0
- indgroup-0.2.0/src/indgroup/_cli.py +108 -0
- indgroup-0.2.0/src/indgroup/_digits.py +70 -0
- indgroup-0.2.0/src/indgroup/_grouping.py +42 -0
- indgroup-0.2.0/src/indgroup/_locale.py +120 -0
- indgroup-0.2.0/src/indgroup/_number.py +54 -0
- indgroup-0.2.0/src/indgroup/_symbols.py +35 -0
- indgroup-0.2.0/src/indgroup/_value.py +47 -0
- indgroup-0.2.0/src/indgroup/py.typed +0 -0
- indgroup-0.2.0/tests/conftest.py +29 -0
- indgroup-0.2.0/tests/test_cli.py +85 -0
- indgroup-0.2.0/tests/test_contract.py +42 -0
- indgroup-0.2.0/tests/test_docs.py +94 -0
- indgroup-0.2.0/tests/test_e2e_wheel.py +118 -0
- indgroup-0.2.0/tests/test_grouping.py +144 -0
- indgroup-0.2.0/tests/test_intl.py +65 -0
- indgroup-0.2.0/tests/test_locale.py +61 -0
- indgroup-0.2.0/tests/test_number.py +133 -0
- indgroup-0.2.0/tests/test_review_fixes.py +68 -0
- indgroup-0.2.0/tests/test_sources.py +104 -0
- 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
|
indgroup-0.2.0/PKG-INFO
ADDED
|
@@ -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/
|
indgroup-0.2.0/README.md
ADDED
|
@@ -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"
|