indgroup 0.2.0__py3-none-any.whl
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/__init__.py +25 -0
- indgroup/__main__.py +7 -0
- indgroup/_cldr.json +1 -0
- indgroup/_cli.py +108 -0
- indgroup/_digits.py +70 -0
- indgroup/_grouping.py +42 -0
- indgroup/_locale.py +120 -0
- indgroup/_number.py +54 -0
- indgroup/_symbols.py +35 -0
- indgroup/_value.py +47 -0
- indgroup/py.typed +0 -0
- indgroup-0.2.0.dist-info/METADATA +215 -0
- indgroup-0.2.0.dist-info/RECORD +17 -0
- indgroup-0.2.0.dist-info/WHEEL +4 -0
- indgroup-0.2.0.dist-info/entry_points.txt +2 -0
- indgroup-0.2.0.dist-info/licenses/LICENSE +21 -0
- indgroup-0.2.0.dist-info/licenses/LICENSE-UNICODE +41 -0
indgroup/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Format and parse numbers with Indian lakh and crore grouping and every Unicode CLDR locale."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._grouping import format_indian, parse_indian
|
|
6
|
+
from ._locale import cldr_version, resolve_locale, supported_locales
|
|
7
|
+
from ._number import Grouping, format_number, parse_number
|
|
8
|
+
from ._symbols import NumberSymbols, number_symbols
|
|
9
|
+
from ._value import Number
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Grouping",
|
|
15
|
+
"Number",
|
|
16
|
+
"NumberSymbols",
|
|
17
|
+
"cldr_version",
|
|
18
|
+
"format_indian",
|
|
19
|
+
"format_number",
|
|
20
|
+
"number_symbols",
|
|
21
|
+
"parse_indian",
|
|
22
|
+
"parse_number",
|
|
23
|
+
"resolve_locale",
|
|
24
|
+
"supported_locales",
|
|
25
|
+
]
|
indgroup/__main__.py
ADDED
indgroup/_cldr.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"aliases":{"aar":"aa","abk":"ab","adp":"dz","afr":"af","ajp":"apc","aka":"ak","alb":"sq","als":"sq","amh":"am","ara":"ar","arb":"ar","arg":"an","arm":"hy","asm":"as","aze":"az","azj":"az","bak":"ba","bam":"bm","baq":"eu","bcc":"bal","bel":"be","ben":"bn","bh":"bho","bih":"bho","bod":"bo","bos":"bs","bre":"br","bul":"bg","bur":"my","bxk":"luy","bxr":"bua","cat":"ca","ces":"cs","che":"ce","chi":"zh","chu":"cu","chv":"cv","cld":"syr","cls":"sa","cmn":"zh","cnr":"sr-ME","cor":"kw","cos":"co","cym":"cy","cze":"cs","dan":"da","deu":"de","dgo":"doi","div":"dv","drh":"mn","drw":"fa-AF","dut":"nl","dzo":"dz","ekk":"et","ell":"el","eng":"en","epo":"eo","est":"et","eus":"eu","ewe":"ee","fao":"fo","fas":"fa","fat":"ak","fin":"fi","fra":"fr","fre":"fr","fry":"fy","fuc":"ff","ful":"ff","gaz":"om","geo":"ka","ger":"de","gla":"gd","gle":"ga","glg":"gl","glv":"gv","gom":"kok","gre":"el","grn":"gn","gug":"gn","guj":"gu","hat":"ht","hau":"ha","hbs":"sr-Latn","heb":"he","hin":"hi","hrv":"hr","hun":"hu","hye":"hy","ibo":"ig","ice":"is","ido":"io","iii":"ii","ike":"iu","iku":"iu","ile":"ie","in":"id","ina":"ia","ind":"id","isl":"is","ita":"it","iw":"he","jav":"jv","ji":"yi","jpn":"ja","jw":"jv","kal":"kl","kan":"kn","kas":"ks","kat":"ka","kaz":"kk","khk":"mn","khm":"km","kik":"ki","kin":"rw","kir":"ky","kmr":"ku","kor":"ko","kur":"ku","lao":"lo","lat":"la","lav":"lv","lin":"ln","lit":"lt","ltz":"lb","lub":"lu","lug":"lg","lvs":"lv","mac":"mk","mal":"ml","mao":"mi","mar":"mr","may":"ms","mkd":"mk","mlg":"mg","mlt":"mt","mo":"ro","mol":"ro","mon":"mn","mri":"mi","msa":"ms","mup":"raj","mya":"my","nav":"nv","nbl":"nr","nde":"nd","nep":"ne","nld":"nl","nno":"nn","nob":"nb","nor":"no","npi":"ne","nya":"ny","oci":"oc","ori":"or","orm":"om","ory":"or","oss":"os","pan":"pa","pbu":"ps","per":"fa","pes":"fa","pli":"pi","plt":"mg","pol":"pl","por":"pt","prp":"gu","prs":"fa-AF","pus":"ps","que":"qu","quz":"qu","roh":"rm","ron":"ro","rum":"ro","run":"rn","rus":"ru","sag":"sg","san":"sa","scc":"sr","scr":"hr","sh":"sr-Latn","sin":"si","slk":"sk","slo":"sk","slv":"sl","sme":"se","sna":"sn","snd":"sd","som":"so","sot":"st","spa":"es","spy":"kln","sqi":"sq","src":"sc","srd":"sc","srp":"sr","ssw":"ss","sun":"su","swa":"sw","swc":"sw-CD","swe":"sv","swh":"sw","tam":"ta","tat":"tt","tel":"te","tgk":"tg","tgl":"fil","tha":"th","tib":"bo","tir":"ti","tl":"fil","tnf":"fa-AF","ton":"to","tsn":"tn","tso":"ts","tuk":"tk","tur":"tr","tw":"ak","twi":"ak","uig":"ug","ukr":"uk","urd":"ur","uzb":"uz","uzn":"uz","ven":"ve","vie":"vi","vol":"vo","wel":"cy","wln":"wa","wol":"wo","xho":"xh","xpe":"kpe","ydd":"yi","yid":"yi","yor":"yo","zha":"za","zho":"zh","zsm":"ms","zul":"zu","zyb":"za"},"cldr":"48.0.0","locales":{"aa":0,"aa-DJ":0,"aa-ER":0,"ab":0,"af":1,"af-NA":1,"agq":1,"ak":0,"am":0,"an":0,"ann":0,"apc":0,"ar":2,"ar-AE":2,"ar-BH":2,"ar-DJ":2,"ar-DZ":3,"ar-EG":2,"ar-EH":2,"ar-ER":2,"ar-IL":2,"ar-IQ":2,"ar-JO":2,"ar-KM":2,"ar-KW":2,"ar-LB":3,"ar-LY":3,"ar-MA":3,"ar-MR":3,"ar-OM":2,"ar-PS":2,"ar-QA":2,"ar-SA":2,"ar-SD":2,"ar-SO":2,"ar-SS":2,"ar-SY":2,"ar-TD":2,"ar-TN":3,"ar-YE":2,"arn":0,"as":4,"asa":0,"ast":5,"az":5,"az-Arab":0,"az-Arab-IQ":0,"az-Arab-TR":0,"az-Cyrl":5,"az-Latn":5,"ba":6,"bal":0,"bal-Arab":0,"bal-Latn":0,"bas":1,"be":7,"be-tarask":7,"bem":0,"bew":0,"bez":0,"bg":7,"bgc":0,"bgn":8,"bgn-AE":8,"bgn-AF":8,"bgn-IR":8,"bgn-OM":8,"bho":0,"blo":1,"blt":0,"bm":0,"bm-Nkoo":0,"bn":4,"bn-IN":4,"bo":0,"bo-IN":0,"bqi":0,"br":1,"brx":4,"bs":5,"bs-Cyrl":5,"bs-Latn":5,"bss":0,"bua":1,"byn":0,"ca":5,"ca-AD":5,"ca-ES-valencia":5,"ca-FR":5,"ca-IT":5,"cad":0,"cch":0,"ccp":4,"ccp-IN":4,"ce":0,"ceb":0,"cgg":0,"cho":0,"chr":0,"cic":0,"ckb":0,"ckb-IR":0,"co":0,"cop":0,"cs":1,"csw":0,"cu":0,"cv":1,"cy":0,"da":5,"da-GL":5,"dav":0,"de":5,"de-AT":1,"de-BE":5,"de-CH":9,"de-IT":5,"de-LI":9,"de-LU":5,"dje":6,"doi":0,"dsb":5,"dua":1,"dv":0,"dyo":1,"dz":4,"ebu":0,"ee":10,"ee-TG":10,"el":5,"el-CY":5,"el-polyton":5,"en":0,"en-001":0,"en-150":0,"en-AE":0,"en-AG":0,"en-AI":0,"en-AS":0,"en-AT":5,"en-AU":0,"en-BB":0,"en-BE":5,"en-BI":0,"en-BM":0,"en-BS":0,"en-BW":0,"en-BZ":0,"en-CA":0,"en-CC":0,"en-CH":9,"en-CK":0,"en-CM":0,"en-CX":0,"en-CY":0,"en-CZ":1,"en-DE":5,"en-DG":0,"en-DK":5,"en-DM":0,"en-Dsrt":0,"en-EE":1,"en-ER":0,"en-ES":5,"en-FI":1,"en-FJ":0,"en-FK":0,"en-FM":0,"en-FR":11,"en-GB":0,"en-GD":0,"en-GE":11,"en-GG":0,"en-GH":0,"en-GI":0,"en-GM":0,"en-GS":0,"en-GU":0,"en-GY":0,"en-HK":0,"en-HU":1,"en-ID":5,"en-IE":0,"en-IL":0,"en-IM":0,"en-IN":4,"en-IO":0,"en-IT":5,"en-JE":0,"en-JM":0,"en-JP":0,"en-KE":0,"en-KI":0,"en-KN":0,"en-KY":0,"en-LC":0,"en-LR":0,"en-LS":0,"en-LT":1,"en-LV":1,"en-MG":0,"en-MH":0,"en-MO":0,"en-MP":0,"en-MS":0,"en-MT":0,"en-MU":0,"en-MV":0,"en-MW":0,"en-MY":0,"en-NA":0,"en-NF":0,"en-NG":0,"en-NL":5,"en-NO":1,"en-NR":0,"en-NU":0,"en-NZ":0,"en-PG":0,"en-PH":0,"en-PK":0,"en-PL":5,"en-PN":0,"en-PR":0,"en-PT":1,"en-PW":0,"en-RO":5,"en-RW":0,"en-SB":0,"en-SC":0,"en-SD":0,"en-SE":1,"en-SG":0,"en-SH":0,"en-SI":5,"en-SK":1,"en-SL":0,"en-SS":0,"en-SX":0,"en-SZ":0,"en-Shaw":0,"en-TC":0,"en-TK":0,"en-TO":0,"en-TT":0,"en-TV":0,"en-TZ":0,"en-UA":1,"en-UG":0,"en-UM":0,"en-VC":0,"en-VG":0,"en-VI":0,"en-VU":0,"en-WS":0,"en-ZA":1,"en-ZM":0,"en-ZW":0,"eo":11,"es":12,"es-419":0,"es-AR":5,"es-BO":5,"es-BR":0,"es-BZ":0,"es-CL":5,"es-CO":5,"es-CR":1,"es-CU":0,"es-DO":0,"es-EA":12,"es-EC":5,"es-GQ":12,"es-GT":0,"es-HN":0,"es-IC":12,"es-MX":0,"es-NI":0,"es-PA":0,"es-PE":0,"es-PH":12,"es-PR":0,"es-PY":5,"es-SV":0,"es-US":0,"es-UY":5,"es-VE":5,"et":13,"eu":14,"ewo":1,"fa":15,"fa-AF":15,"ff":1,"ff-Adlm":16,"ff-Adlm-BF":16,"ff-Adlm-CM":16,"ff-Adlm-GH":16,"ff-Adlm-GM":16,"ff-Adlm-GW":16,"ff-Adlm-LR":16,"ff-Adlm-MR":16,"ff-Adlm-NE":16,"ff-Adlm-NG":16,"ff-Adlm-SL":16,"ff-Adlm-SN":16,"ff-Latn":1,"ff-Latn-BF":1,"ff-Latn-CM":1,"ff-Latn-GH":1,"ff-Latn-GM":1,"ff-Latn-GN":1,"ff-Latn-GW":1,"ff-Latn-LR":1,"ff-Latn-MR":1,"ff-Latn-NE":1,"ff-Latn-NG":1,"ff-Latn-SL":1,"fi":17,"fil":0,"fo":14,"fo-DK":14,"fr":11,"fr-BE":11,"fr-BF":11,"fr-BI":11,"fr-BJ":11,"fr-BL":11,"fr-CA":1,"fr-CD":11,"fr-CF":11,"fr-CG":11,"fr-CH":11,"fr-CI":11,"fr-CM":11,"fr-DJ":11,"fr-DZ":11,"fr-GA":11,"fr-GF":11,"fr-GN":11,"fr-GP":11,"fr-GQ":11,"fr-HT":11,"fr-KM":11,"fr-LU":5,"fr-MA":5,"fr-MC":11,"fr-MF":11,"fr-MG":11,"fr-ML":11,"fr-MQ":11,"fr-MR":11,"fr-MU":11,"fr-NC":11,"fr-NE":11,"fr-PF":11,"fr-PM":11,"fr-RE":11,"fr-RW":11,"fr-SC":11,"fr-SN":11,"fr-SY":11,"fr-TD":11,"fr-TG":11,"fr-TN":11,"fr-VU":11,"fr-WF":11,"fr-YT":11,"frr":0,"fur":5,"fy":5,"ga":0,"ga-GB":0,"gaa":0,"gd":0,"gez":0,"gez-ER":0,"gl":5,"gn":0,"gsw":18,"gsw-FR":18,"gsw-LI":18,"gu":4,"guz":0,"gv":0,"ha":0,"ha-Arab":0,"ha-Arab-SD":0,"ha-GH":0,"ha-NE":0,"haw":0,"he":2,"hi":4,"hi-Latn":4,"hnj":0,"hnj-Hmnp":0,"hr":14,"hr-BA":14,"hsb":5,"ht":1,"hu":7,"hy":7,"ia":12,"id":5,"ie":7,"ig":0,"ii":0,"io":0,"is":5,"it":12,"it-CH":19,"it-SM":12,"it-VA":12,"iu":0,"iu-Latn":0,"ja":0,"jbo":0,"jgo":5,"jmc":0,"jv":5,"ka":7,"kaa":0,"kaa-Cyrl":0,"kaa-Latn":0,"kab":1,"kaj":0,"kam":0,"kcg":0,"kde":0,"kea":1,"kek":0,"ken":0,"kgp":5,"khq":6,"ki":0,"kk":1,"kk-Arab":0,"kk-Cyrl":1,"kk-KZ":1,"kkj":5,"kl":5,"kln":0,"km":0,"kn":0,"ko":0,"ko-CN":0,"ko-KP":0,"kok":4,"kok-Deva":4,"kok-Latn":4,"kpe":0,"kpe-GN":0,"ks":20,"ks-Arab":20,"ks-Deva":0,"ksb":0,"ksf":1,"ksh":17,"ku":5,"ku-Arab":0,"ku-Arab-IR":0,"ku-Latn":5,"ku-Latn-IQ":5,"ku-Latn-SY":5,"ku-TR":5,"kw":0,"kxv":4,"kxv-Deva":4,"kxv-Latn":4,"kxv-Orya":4,"kxv-Telu":4,"ky":1,"la":0,"lag":0,"lb":5,"lg":0,"lij":5,"lkt":0,"lld":5,"lmo":21,"ln":5,"ln-AO":5,"ln-CF":5,"ln-CG":5,"lo":5,"lrc":0,"lrc-IQ":0,"lt":17,"ltg":0,"lu":5,"luo":0,"luy":0,"lv":7,"lzz":0,"mai":0,"mas":0,"mas-TZ":0,"mdf":0,"mer":0,"mfe":6,"mg":0,"mgh":5,"mgo":0,"mhn":0,"mi":0,"mic":0,"mk":5,"ml":4,"mn":0,"mn-Mong":0,"mn-Mong-MN":0,"mni":0,"mni-Beng":0,"mni-Mtei":0,"moh":0,"mr":4,"ms":0,"ms-Arab":0,"ms-Arab-BN":5,"ms-BN":5,"ms-ID":5,"ms-SG":0,"mt":0,"mua":5,"mus":0,"mww":0,"mww-Hmnp":0,"my":0,"myv":0,"mzn":0,"naq":0,"nb":17,"nb-SJ":17,"nd":0,"nds":5,"nds-NL":5,"ne":4,"ne-IN":4,"nl":5,"nl-AW":5,"nl-BE":5,"nl-BQ":5,"nl-CW":5,"nl-SR":5,"nl-SX":5,"nmg":1,"nn":17,"nnh":5,"no":17,"nqo":20,"nr":1,"nso":0,"nus":0,"nv":0,"ny":0,"nyn":0,"oc":1,"oc-ES":1,"oka":0,"oka-US":0,"om":0,"om-KE":0,"or":4,"os":1,"os-RU":1,"osa":0,"pa":4,"pa-Arab":2,"pa-Guru":4,"pap":0,"pap-AW":0,"pcm":0,"pi":0,"pi-Latn":0,"pis":0,"pl":7,"pms":5,"prg":1,"ps":22,"ps-PK":22,"pt":5,"pt-AO":1,"pt-CH":7,"pt-CV":7,"pt-GQ":7,"pt-GW":7,"pt-LU":7,"pt-MO":7,"pt-MZ":7,"pt-PT":7,"pt-ST":7,"pt-TL":7,"qu":0,"qu-BO":5,"qu-EC":0,"quc":0,"raj":0,"rhg":0,"rhg-Rohg":0,"rhg-Rohg-BD":0,"rif":0,"rm":11,"rn":5,"ro":5,"ro-MD":5,"rof":0,"ru":1,"ru-BY":1,"ru-KG":1,"ru-KZ":1,"ru-MD":1,"ru-UA":7,"rw":5,"rwk":0,"sa":4,"sah":1,"saq":0,"sat":0,"sat-Deva":0,"sat-Olck":0,"sbp":0,"sc":5,"scn":5,"sd":0,"sd-Arab":0,"sd-Deva":0,"sdh":0,"sdh-IQ":0,"se":17,"se-FI":17,"se-SE":17,"seh":5,"ses":6,"sg":5,"sgs":0,"shi":1,"shi-Latn":1,"shi-Tfng":1,"shn":0,"shn-TH":0,"si":0,"sid":0,"sk":1,"skr":0,"sl":23,"sma":0,"sma-NO":0,"smj":0,"smj-NO":0,"smn":1,"sms":0,"sn":0,"so":0,"so-DJ":0,"so-ET":0,"so-KE":0,"sq":7,"sq-MK":7,"sq-XK":7,"sr":5,"sr-Cyrl":5,"sr-Cyrl-BA":5,"sr-Cyrl-ME":5,"sr-Cyrl-XK":5,"sr-Latn":5,"sr-Latn-BA":5,"sr-Latn-ME":5,"sr-Latn-XK":5,"ss":1,"ss-SZ":1,"ssy":0,"st":0,"st-LS":0,"su":5,"su-Latn":5,"suz":0,"suz-Deva":0,"suz-Sunu":0,"sv":17,"sv-AX":17,"sv-FI":17,"sw":0,"sw-CD":5,"sw-KE":0,"sw-UG":0,"syr":0,"syr-SY":0,"szl":1,"ta":4,"ta-LK":4,"ta-MY":0,"ta-SG":0,"te":4,"teo":0,"teo-KE":0,"tg":1,"th":0,"ti":0,"ti-ER":0,"tig":0,"tk":1,"tn":9,"tn-BW":9,"to":0,"tok":24,"tpi":0,"tr":5,"tr-CY":5,"trv":0,"trw":0,"ts":1,"tt":1,"twq":6,"tyv":6,"tzm":1,"ug":0,"uk":1,"und":0,"ur":2,"ur-IN":2,"uz":1,"uz-Arab":22,"uz-Cyrl":1,"uz-Latn":1,"vai":0,"vai-Latn":0,"vai-Vaii":0,"ve":1,"vec":11,"vi":5,"vmw":5,"vo":0,"vun":0,"wa":0,"wae":21,"wal":0,"wbp":0,"wo":5,"xh":6,"xnr":4,"xog":0,"yav":1,"yi":0,"yo":0,"yo-BJ":0,"yrl":5,"yrl-CO":5,"yrl-VE":5,"yue":0,"yue-Hans":0,"yue-Hant":0,"yue-Hant-CN":0,"yue-Hant-MO":0,"za":0,"zgh":1,"zh":0,"zh-Hans":0,"zh-Hans-HK":0,"zh-Hans-MO":0,"zh-Hans-MY":0,"zh-Hans-SG":0,"zh-Hant":0,"zh-Hant-HK":0,"zh-Hant-MO":0,"zh-Hant-MY":0,"zh-Latn":0,"zu":0},"parents":{"az-Arab":"und","az-Cyrl":"und","bal-Latn":"und","blt-Latn":"und","bm-Nkoo":"und","bs-Cyrl":"und","byn-Latn":"und","cu-Glag":"und","dje-Arab":"und","dyo-Arab":"und","en-150":"en-001","en-AG":"en-001","en-AI":"en-001","en-AT":"en-150","en-AU":"en-001","en-BB":"en-001","en-BE":"en-150","en-BM":"en-001","en-BS":"en-001","en-BW":"en-001","en-BZ":"en-001","en-CC":"en-001","en-CH":"en-150","en-CK":"en-001","en-CM":"en-001","en-CX":"en-001","en-CY":"en-001","en-CZ":"en-150","en-DE":"en-150","en-DG":"en-001","en-DK":"en-150","en-DM":"en-001","en-Dsrt":"und","en-EE":"en-150","en-ER":"en-001","en-ES":"en-150","en-FI":"en-150","en-FJ":"en-001","en-FK":"en-001","en-FM":"en-001","en-FR":"en-150","en-GB":"en-001","en-GD":"en-001","en-GE":"en-150","en-GG":"en-001","en-GH":"en-001","en-GI":"en-001","en-GM":"en-001","en-GS":"en-001","en-GY":"en-001","en-HK":"en-001","en-HU":"en-150","en-ID":"en-001","en-IE":"en-001","en-IL":"en-001","en-IM":"en-001","en-IN":"en-001","en-IO":"en-001","en-IT":"en-150","en-JE":"en-001","en-JM":"en-001","en-KE":"en-001","en-KI":"en-001","en-KN":"en-001","en-KY":"en-001","en-LC":"en-001","en-LR":"en-001","en-LS":"en-001","en-LT":"en-150","en-LV":"en-150","en-MG":"en-001","en-MO":"en-001","en-MS":"en-001","en-MT":"en-001","en-MU":"en-001","en-MV":"en-001","en-MW":"en-001","en-MY":"en-001","en-NA":"en-001","en-NF":"en-001","en-NG":"en-001","en-NL":"en-150","en-NO":"en-150","en-NR":"en-001","en-NU":"en-001","en-NZ":"en-001","en-PG":"en-001","en-PK":"en-001","en-PL":"en-150","en-PN":"en-001","en-PT":"en-150","en-PW":"en-001","en-RO":"en-150","en-RW":"en-001","en-SB":"en-001","en-SC":"en-001","en-SD":"en-001","en-SE":"en-150","en-SG":"en-001","en-SH":"en-001","en-SI":"en-150","en-SK":"en-150","en-SL":"en-001","en-SS":"en-001","en-SX":"en-001","en-SZ":"en-001","en-Shaw":"und","en-TC":"en-001","en-TK":"en-001","en-TO":"en-001","en-TT":"en-001","en-TV":"en-001","en-TZ":"en-001","en-UA":"en-150","en-UG":"en-001","en-VC":"en-001","en-VG":"en-001","en-VU":"en-001","en-WS":"en-001","en-ZA":"en-001","en-ZM":"en-001","en-ZW":"en-001","es-AR":"es-419","es-BO":"es-419","es-BR":"es-419","es-BZ":"es-419","es-CL":"es-419","es-CO":"es-419","es-CR":"es-419","es-CU":"es-419","es-DO":"es-419","es-EC":"es-419","es-GT":"es-419","es-HN":"es-419","es-JP":"es-419","es-MX":"es-419","es-NI":"es-419","es-PA":"es-419","es-PE":"es-419","es-PR":"es-419","es-PY":"es-419","es-SV":"es-419","es-US":"es-419","es-UY":"es-419","es-VE":"es-419","ff-Adlm":"und","ff-Arab":"und","ha-Arab":"und","hi-Latn":"en-IN","ht":"fr-HT","iu-Latn":"und","kaa-Latn":"und","kk-Arab":"und","kok-Latn":"und","ks-Deva":"und","ku-Arab":"und","kxv-Deva":"und","kxv-Orya":"und","kxv-Telu":"und","ky-Arab":"und","ky-Latn":"und","ml-Arab":"und","mn-Mong":"und","mni-Mtei":"und","ms-Arab":"und","nb":"no","nn":"no","no-NO":"no","pa-Arab":"und","pt-AO":"pt-PT","pt-CH":"pt-PT","pt-CV":"pt-PT","pt-FR":"pt-PT","pt-GQ":"pt-PT","pt-GW":"pt-PT","pt-LU":"pt-PT","pt-MO":"pt-PT","pt-MZ":"pt-PT","pt-ST":"pt-PT","pt-TL":"pt-PT","sat-Deva":"und","sd-Deva":"und","sd-Khoj":"und","sd-Sind":"und","shi-Latn":"und","so-Arab":"und","sr-Latn":"und","suz-Sunu":"und","sw-Arab":"und","tg-Arab":"und","ug-Cyrl":"und","uz-Arab":"und","uz-Cyrl":"und","vai-Latn":"und","wo-Arab":"und","yo-Arab":"und","yue-Hans":"und","zh-Hant":"und","zh-Hant-MO":"zh-Hant-HK"},"profiles":[[".",",","-",3,3,1],[",","\u00a0","-",3,3,1],[".",",","\u200e-",3,3,1],[",",".","\u200e-",3,3,1],[".",",","-",3,2,1],[",",".","-",3,3,1],[".","\u00a0","-",3,3,1],[",","\u00a0","-",3,3,2],["\u066b","\u060c","-",3,3,1],[".","'","-",3,3,1],[".",",","-",3,3,3],[",","\u202f","-",3,3,1],[",",".","-",3,3,2],[",","\u00a0","\u2212",3,3,2],[",",".","\u2212",3,3,1],[".",",","\u200e\u2212",3,3,1],[".","\u2e41","-",3,3,1],[",","\u00a0","\u2212",3,3,1],[".","'","\u2212",3,3,1],[".","'","-",3,3,2],[".","\u060c","-",3,3,1],[",","'","-",3,3,1],[",",".","\u200e\u2212",3,3,1],[",",".","\u2212",3,3,2],[",","\u00a0","-",2,2,1]],"regions":{"004":"AF","008":"AL","010":"AQ","012":"DZ","016":"AS","020":"AD","024":"AO","028":"AG","031":"AZ","032":"AR","036":"AU","040":"AT","044":"BS","048":"BH","050":"BD","051":"AM","052":"BB","056":"BE","060":"BM","064":"BT","068":"BO","070":"BA","072":"BW","074":"BV","076":"BR","084":"BZ","086":"IO","090":"SB","092":"VG","096":"BN","100":"BG","104":"MM","108":"BI","112":"BY","116":"KH","120":"CM","124":"CA","132":"CV","136":"KY","140":"CF","144":"LK","148":"TD","152":"CL","156":"CN","158":"TW","162":"CX","166":"CC","170":"CO","174":"KM","175":"YT","178":"CG","180":"CD","184":"CK","188":"CR","191":"HR","192":"CU","196":"CY","203":"CZ","204":"BJ","208":"DK","212":"DM","214":"DO","218":"EC","222":"SV","226":"GQ","230":"ET","231":"ET","232":"ER","233":"EE","234":"FO","238":"FK","239":"GS","242":"FJ","246":"FI","248":"AX","249":"FR","250":"FR","254":"GF","258":"PF","260":"TF","262":"DJ","266":"GA","268":"GE","270":"GM","275":"PS","276":"DE","278":"DE","280":"DE","288":"GH","292":"GI","296":"KI","300":"GR","304":"GL","308":"GD","312":"GP","316":"GU","320":"GT","324":"GN","328":"GY","332":"HT","334":"HM","336":"VA","340":"HN","344":"HK","348":"HU","352":"IS","356":"IN","360":"ID","364":"IR","368":"IQ","372":"IE","376":"IL","380":"IT","384":"CI","388":"JM","392":"JP","398":"KZ","400":"JO","404":"KE","408":"KP","410":"KR","414":"KW","417":"KG","418":"LA","422":"LB","426":"LS","428":"LV","430":"LR","434":"LY","438":"LI","440":"LT","442":"LU","446":"MO","450":"MG","454":"MW","458":"MY","462":"MV","466":"ML","470":"MT","474":"MQ","478":"MR","480":"MU","484":"MX","492":"MC","496":"MN","498":"MD","499":"ME","500":"MS","504":"MA","508":"MZ","512":"OM","516":"NA","520":"NR","524":"NP","528":"NL","531":"CW","533":"AW","534":"SX","535":"BQ","540":"NC","548":"VU","554":"NZ","558":"NI","562":"NE","566":"NG","570":"NU","574":"NF","578":"NO","580":"MP","581":"UM","583":"FM","584":"MH","585":"PW","586":"PK","591":"PA","598":"PG","600":"PY","604":"PE","608":"PH","612":"PN","616":"PL","620":"PT","624":"GW","626":"TL","630":"PR","634":"QA","638":"RE","642":"RO","643":"RU","646":"RW","652":"BL","654":"SH","659":"KN","660":"AI","662":"LC","663":"MF","666":"PM","670":"VC","674":"SM","678":"ST","682":"SA","686":"SN","688":"RS","690":"SC","694":"SL","702":"SG","703":"SK","704":"VN","705":"SI","706":"SO","710":"ZA","716":"ZW","720":"YE","724":"ES","728":"SS","729":"SD","732":"EH","736":"SD","740":"SR","744":"SJ","748":"SZ","752":"SE","756":"CH","760":"SY","762":"TJ","764":"TH","768":"TG","772":"TK","776":"TO","780":"TT","784":"AE","788":"TN","792":"TR","795":"TM","796":"TC","798":"TV","800":"UG","804":"UA","807":"MK","818":"EG","826":"GB","831":"GG","832":"JE","833":"IM","834":"TZ","840":"US","850":"VI","854":"BF","858":"UY","860":"UZ","862":"VE","876":"WF","882":"WS","886":"YE","887":"YE","894":"ZM","958":"AA","959":"QM","960":"QN","962":"QP","963":"QQ","964":"QR","965":"QS","966":"QT","967":"EU","968":"QV","969":"QW","970":"QX","971":"QY","972":"QZ","973":"XA","974":"XB","975":"XC","976":"XD","977":"XE","978":"XF","979":"XG","980":"XH","981":"XI","982":"XJ","983":"XK","984":"XL","985":"XM","986":"XN","987":"XO","988":"XP","989":"XQ","990":"XR","991":"XS","992":"XT","993":"XU","994":"XV","995":"XW","996":"XX","997":"XY","998":"XZ","999":"ZZ","BU":"MM","CT":"KI","DD":"DE","DY":"BJ","FX":"FR","HV":"BF","JT":"UM","MI":"UM","NH":"VU","NQ":"AQ","PU":"UM","PZ":"PA","QU":"EU","RH":"ZW","TP":"TL","UK":"GB","VD":"VN","WK":"UM","YD":"YE","ZR":"CD"},"scripts":{"az":"Latn","az-IQ":"Arab","az-IR":"Arab","az-RU":"Cyrl","bal":"Arab","bm":"Latn","bs":"Latn","en":"Latn","ff":"Latn","ha":"Latn","ha-CM":"Arab","ha-SD":"Arab","hi":"Deva","hnj":"Hmnp","iu":"Cans","kaa":"Cyrl","kk":"Cyrl","kk-AF":"Arab","kk-CN":"Arab","kk-IR":"Arab","kk-MN":"Arab","kok":"Deva","ks":"Arab","ku":"Latn","ku-AM":"Cyrl","ku-AZ":"Cyrl","ku-GE":"Cyrl","ku-IQ":"Arab","ku-IR":"Arab","ku-LB":"Arab","ku-TM":"Cyrl","kxv":"Latn","mn":"Cyrl","mn-CN":"Mong","mni":"Beng","ms":"Latn","ms-CC":"Arab","mww":"Hmnp","pa":"Guru","pa-PK":"Arab","pi":"Latn","pi-IN":"Deva","pi-LK":"Sinh","pi-MM":"Mymr","pi-TH":"Thai","rhg":"Rohg","sat":"Olck","sd":"Arab","sd-IN":"Deva","shi":"Tfng","sr":"Cyrl","sr-ME":"Latn","sr-RO":"Latn","sr-TR":"Latn","su":"Latn","suz":"Deva","uz":"Latn","uz-AF":"Arab","uz-CN":"Cyrl","vai":"Vaii","yue":"Hant","yue-CN":"Hans","zh":"Hans","zh-AU":"Hant","zh-BN":"Hant","zh-GB":"Hant","zh-GF":"Hant","zh-HK":"Hant","zh-ID":"Hant","zh-MO":"Hant","zh-PA":"Hant","zh-PF":"Hant","zh-PH":"Hant","zh-SR":"Hant","zh-TH":"Hant","zh-TW":"Hant","zh-US":"Hant","zh-VN":"Hant"}}
|
indgroup/_cli.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Command line: ``indgroup 1234567`` prints ``12,34,567``; ``-l de`` prints ``1.234.567``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from typing import TextIO, cast
|
|
11
|
+
|
|
12
|
+
from ._locale import cldr_version, supported_locales
|
|
13
|
+
from ._number import Grouping, format_number, parse_number
|
|
14
|
+
|
|
15
|
+
__all__ = ["main"]
|
|
16
|
+
|
|
17
|
+
_BOM = chr(0xFEFF)
|
|
18
|
+
_PLAIN = re.compile(r"[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]{1,4})?")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parser() -> argparse.ArgumentParser:
|
|
22
|
+
from . import __version__
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="indgroup",
|
|
26
|
+
description="Group the digits of numbers the way a locale writes them (Unicode CLDR).",
|
|
27
|
+
epilog="Without VALUE arguments the values are read from stdin, one per non-blank line."
|
|
28
|
+
" Put -- before negative values other than plain digits, such as -1e5 or -1.234,5.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("values", nargs="*", metavar="VALUE", help="the numbers, in order")
|
|
31
|
+
parser.add_argument("-l", "--locale", default="en-IN", help="BCP 47 locale (default: en-IN)")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-g",
|
|
34
|
+
"--grouping",
|
|
35
|
+
default="auto",
|
|
36
|
+
choices=["auto", "min2", "always", "off"],
|
|
37
|
+
help="when to group, as Intl.NumberFormat useGrouping (default: auto)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-p",
|
|
41
|
+
"--parse",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="read grouped values written in the locale and print them plain",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--locales", action="store_true", help="list the supported locales and exit"
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--version", action="version", version=f"indgroup {__version__} (CLDR {cldr_version()})"
|
|
50
|
+
)
|
|
51
|
+
return parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _utf8(stream: TextIO, encoding: str = "utf-8") -> None:
|
|
55
|
+
# Pipes default to the legacy code page on Windows, which cannot carry U+202F and friends.
|
|
56
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
57
|
+
if reconfigure is not None and not stream.isatty():
|
|
58
|
+
reconfigure(encoding=encoding)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_values(stdin: TextIO) -> list[str]:
|
|
62
|
+
values = [line.strip() for line in stdin]
|
|
63
|
+
if values:
|
|
64
|
+
values[0] = values[0].removeprefix(_BOM).strip()
|
|
65
|
+
return [value for value in values if value]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _convert(value: str, args: argparse.Namespace) -> str:
|
|
69
|
+
if args.parse:
|
|
70
|
+
number = parse_number(value, args.locale)
|
|
71
|
+
return str(number) if isinstance(number, int) else format(number, "f")
|
|
72
|
+
if not _PLAIN.fullmatch(value):
|
|
73
|
+
raise ValueError(f"not a plain number: {value!r}")
|
|
74
|
+
return format_number(Decimal(value), args.locale, grouping=cast(Grouping, args.grouping))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main(
|
|
78
|
+
argv: Sequence[str] | None = None, stdin: TextIO | None = None, stdout: TextIO | None = None
|
|
79
|
+
) -> int:
|
|
80
|
+
"""Run the command line and return 0.
|
|
81
|
+
|
|
82
|
+
Bad arguments, an unknown locale, a value that cannot be read or stdin that is not
|
|
83
|
+
UTF-8 exit with status 2 through ``SystemExit``. Without streams it reads ``sys.stdin``
|
|
84
|
+
and writes ``sys.stdout``, switching them to UTF-8 when they are pipes.
|
|
85
|
+
"""
|
|
86
|
+
if stdin is None:
|
|
87
|
+
stdin = sys.stdin
|
|
88
|
+
_utf8(stdin, "utf-8-sig")
|
|
89
|
+
if stdout is None:
|
|
90
|
+
stdout = sys.stdout
|
|
91
|
+
_utf8(stdout)
|
|
92
|
+
parser = _parser()
|
|
93
|
+
args = parser.parse_args(argv)
|
|
94
|
+
if args.locales:
|
|
95
|
+
stdout.write("\n".join(supported_locales()) + "\n")
|
|
96
|
+
return 0
|
|
97
|
+
if not args.values and stdin.isatty():
|
|
98
|
+
parser.error("no values: pass VALUE arguments or pipe them on stdin")
|
|
99
|
+
try:
|
|
100
|
+
values = args.values or _read_values(stdin)
|
|
101
|
+
except UnicodeDecodeError:
|
|
102
|
+
parser.exit(2, "indgroup: error: stdin is not valid UTF-8\n")
|
|
103
|
+
try:
|
|
104
|
+
lines = [_convert(value, args) for value in values]
|
|
105
|
+
except (LookupError, ValueError) as error:
|
|
106
|
+
parser.exit(2, f"indgroup: error: {error}\n")
|
|
107
|
+
stdout.write("".join(f"{line}\n" for line in lines))
|
|
108
|
+
return 0
|
indgroup/_digits.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Digit grouping and strict reading of grouped digits, shared by every formatter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
|
|
9
|
+
from ._value import DIGITS
|
|
10
|
+
|
|
11
|
+
_SPACE = " \t\r\n"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def group_digits(digits: str, separator: str, primary: int, secondary: int, minimum: int) -> str:
|
|
15
|
+
"""Insert ``separator`` into a run of digits the way ICU's grouper does.
|
|
16
|
+
|
|
17
|
+
The first separator sits ``primary`` digits from the right, the next ones every
|
|
18
|
+
``secondary`` digits, and nothing is grouped unless at least ``minimum`` digits stand to
|
|
19
|
+
the left of the first separator. A primary size of zero means no grouping.
|
|
20
|
+
"""
|
|
21
|
+
if primary <= 0 or len(digits) - primary < minimum:
|
|
22
|
+
return digits
|
|
23
|
+
head = digits[:-primary]
|
|
24
|
+
first = len(head) % secondary or secondary
|
|
25
|
+
groups = [head[:first]]
|
|
26
|
+
groups += [head[start : start + secondary] for start in range(first, len(head), secondary)]
|
|
27
|
+
return separator.join([*groups, digits[-primary:]])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@lru_cache(maxsize=64)
|
|
31
|
+
def _grouped(separator: str, primary: int, secondary: int) -> re.Pattern[str]:
|
|
32
|
+
sep = re.escape(separator)
|
|
33
|
+
return re.compile(
|
|
34
|
+
rf"[0-9]{{1,{secondary}}}(?:{sep}[0-9]{{{secondary}}})*{sep}[0-9]{{{primary}}}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_digits(part: str, separator: str, primary: int, secondary: int) -> str:
|
|
39
|
+
"""The plain digits of an integer part written ungrouped or grouped exactly as specified."""
|
|
40
|
+
if DIGITS.fullmatch(part):
|
|
41
|
+
return part
|
|
42
|
+
if primary > 0 and _grouped(separator, primary, secondary).fullmatch(part):
|
|
43
|
+
return part.replace(separator, "")
|
|
44
|
+
raise ValueError(f"not a validly grouped integer: {part!r}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_number(
|
|
48
|
+
text: str, decimal: str, separator: str, sizes: tuple[int, int], minus: str, plus: str = ""
|
|
49
|
+
) -> int | Decimal:
|
|
50
|
+
"""Parse ``text`` strictly: an optional sign, grouped digits, an optional fraction.
|
|
51
|
+
|
|
52
|
+
A value without a fraction is an ``int``, one with a fraction a ``Decimal`` that keeps
|
|
53
|
+
every digit written, so formatting it again gives the same text.
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(text, str):
|
|
56
|
+
raise TypeError(f"text must be a str, not {type(text).__name__}")
|
|
57
|
+
raw = text.strip(_SPACE) # not str.strip(): U+00A0 and U+202F are group symbols
|
|
58
|
+
negative = raw.startswith(minus)
|
|
59
|
+
if negative:
|
|
60
|
+
raw = raw[len(minus) :]
|
|
61
|
+
elif plus and raw.startswith(plus):
|
|
62
|
+
raw = raw[len(plus) :]
|
|
63
|
+
integer, point, fraction = raw.partition(decimal)
|
|
64
|
+
digits = read_digits(integer, separator, *sizes)
|
|
65
|
+
if point and not DIGITS.fullmatch(fraction):
|
|
66
|
+
raise ValueError(f"not a valid fractional part: {fraction!r}")
|
|
67
|
+
if not point:
|
|
68
|
+
number = int(Decimal(digits)) # int(str) stops at 4300 digits
|
|
69
|
+
return -number if negative else number
|
|
70
|
+
return Decimal(f"{'-' if negative else ''}{digits}.{fraction}")
|
indgroup/_grouping.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Indian lakh and crore digit grouping for integers and decimals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
|
|
7
|
+
from ._digits import group_digits, read_number
|
|
8
|
+
from ._value import Number, split_value
|
|
9
|
+
|
|
10
|
+
_SIZES = (3, 2)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _check_separator(separator: object) -> str:
|
|
14
|
+
if not isinstance(separator, str):
|
|
15
|
+
raise TypeError(f"separator must be a str, not {type(separator).__name__}")
|
|
16
|
+
if len(separator) != 1 or separator.isdigit() or separator in ".+-":
|
|
17
|
+
raise ValueError("separator must be one character other than a digit, a sign or '.'")
|
|
18
|
+
return separator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_indian(value: Number, *, separator: str = ",") -> str:
|
|
22
|
+
"""Render ``value`` with Indian grouping, e.g. ``1234567`` -> ``"12,34,567"``.
|
|
23
|
+
|
|
24
|
+
Only the integer part is grouped; the fraction is kept digit for digit. A float is
|
|
25
|
+
read through its shortest spelling, so ``0.1`` stays ``0.1``.
|
|
26
|
+
"""
|
|
27
|
+
sep = _check_separator(separator)
|
|
28
|
+
negative, integer, fraction = split_value(value)
|
|
29
|
+
text = group_digits(integer, sep, *_SIZES, 1)
|
|
30
|
+
if fraction:
|
|
31
|
+
text = f"{text}.{fraction}"
|
|
32
|
+
return f"-{text}" if negative else text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_indian(text: str, *, separator: str = ",") -> int | Decimal:
|
|
36
|
+
"""Parse an Indian-grouped number back to an ``int`` or a ``Decimal``.
|
|
37
|
+
|
|
38
|
+
A value with no fractional part returns an ``int``; otherwise a ``Decimal``.
|
|
39
|
+
Western grouping and any other malformed input raise ``ValueError``.
|
|
40
|
+
"""
|
|
41
|
+
sep = _check_separator(separator)
|
|
42
|
+
return read_number(text, ".", sep, _SIZES, "-", "+")
|
indgroup/_locale.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Locale lookup in the number table derived from Unicode CLDR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
__all__ = ["cldr_version", "resolve_locale", "supported_locales", "table"]
|
|
12
|
+
|
|
13
|
+
_SUBTAG = re.compile(r"[A-Za-z0-9]{1,8}")
|
|
14
|
+
_LANGUAGE = re.compile(r"[A-Za-z]{2,3}|[A-Za-z]{5,8}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=1)
|
|
18
|
+
def table() -> dict[str, Any]:
|
|
19
|
+
"""The number table shipped with the package (see scripts/build_table.py)."""
|
|
20
|
+
text = files("indgroup").joinpath("_cldr.json").read_text(encoding="ascii")
|
|
21
|
+
loaded: dict[str, Any] = json.loads(text)
|
|
22
|
+
return loaded
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cldr_version() -> str:
|
|
26
|
+
"""The CLDR release the number table was derived from."""
|
|
27
|
+
return str(table()["cldr"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def supported_locales() -> tuple[str, ...]:
|
|
31
|
+
"""Every CLDR locale with number data, sorted; ``und`` is the root locale."""
|
|
32
|
+
return tuple(sorted(table()["locales"]))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# glibc locale modifiers that name a script, as in "sr_RS@latin"; others such as "@euro"
|
|
36
|
+
# do not affect number symbols.
|
|
37
|
+
_MODIFIERS = {"latin": "Latn", "cyrillic": "Cyrl", "devanagari": "Deva"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse(locale: str) -> tuple[str, str, str, list[str]]:
|
|
41
|
+
"""Language, script, region (empty when absent) and variants of a locale tag."""
|
|
42
|
+
if not isinstance(locale, str):
|
|
43
|
+
raise TypeError(f"locale must be a str, not {type(locale).__name__}")
|
|
44
|
+
tag, _, modifier = locale.partition("@")
|
|
45
|
+
subtags = tag.split(".", 1)[0].replace("_", "-").split("-") # POSIX "es_ES.UTF-8"
|
|
46
|
+
if subtags[0].lower() == "root":
|
|
47
|
+
subtags[0] = "und"
|
|
48
|
+
if not all(_SUBTAG.fullmatch(part) for part in subtags) or not _LANGUAGE.fullmatch(subtags[0]):
|
|
49
|
+
raise ValueError(f"not a locale tag: {locale!r}")
|
|
50
|
+
rest = subtags[1:]
|
|
51
|
+
if rest and len(rest[0]) == 3 and rest[0].isalpha():
|
|
52
|
+
raise ValueError(f"extended language subtags are not supported: {locale!r}")
|
|
53
|
+
script = rest.pop(0).title() if rest and len(rest[0]) == 4 and rest[0].isalpha() else ""
|
|
54
|
+
script = script or _MODIFIERS.get(modifier.lower(), "")
|
|
55
|
+
region = ""
|
|
56
|
+
if rest and (
|
|
57
|
+
(len(rest[0]) == 2 and rest[0].isalpha()) or (len(rest[0]) == 3 and rest[0].isdigit())
|
|
58
|
+
):
|
|
59
|
+
region = rest.pop(0).upper()
|
|
60
|
+
variants = []
|
|
61
|
+
for part in rest:
|
|
62
|
+
if len(part) == 1: # extensions and private use do not select number data
|
|
63
|
+
break
|
|
64
|
+
variants.append(part.lower())
|
|
65
|
+
data = table()
|
|
66
|
+
region = data["regions"].get(region, region)
|
|
67
|
+
alias = data["aliases"].get(subtags[0].lower(), subtags[0].lower()).split("-")
|
|
68
|
+
for extra in alias[1:]:
|
|
69
|
+
if len(extra) == 4:
|
|
70
|
+
script = script or extra
|
|
71
|
+
else:
|
|
72
|
+
region = region or extra
|
|
73
|
+
return alias[0], script, region, variants
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _walk(tag: str) -> str | None:
|
|
77
|
+
"""The first locale with data on the CLDR fallback chain of ``tag``."""
|
|
78
|
+
data = table()
|
|
79
|
+
while tag:
|
|
80
|
+
if tag in data["locales"]:
|
|
81
|
+
return tag
|
|
82
|
+
parent = data["parents"].get(tag) # CLDR JSON names the root locale "und"
|
|
83
|
+
tag = parent if parent is not None else tag.rpartition("-")[0]
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve_locale(locale: str) -> str:
|
|
88
|
+
"""The CLDR locale whose number symbols ``locale`` uses.
|
|
89
|
+
|
|
90
|
+
Accepts BCP 47 (``pt-BR``) and POSIX (``pt_BR.UTF-8``, ``sr_RS@latin``) spellings in any
|
|
91
|
+
case. Deprecated codes are replaced (``iw`` is ``he``, ``sh`` is ``sr-Latn``, ``UK`` is
|
|
92
|
+
``GB``), and where a language is written in several scripts the likely one is added
|
|
93
|
+
(``zh-TW`` is ``zh-Hant-TW``, ``zh-SG`` is ``zh-Hans-SG``). A locale without data of its
|
|
94
|
+
own falls back through the CLDR parent locales, then by dropping subtags (``pt-FR`` reads
|
|
95
|
+
``pt-PT``, ``de-AT-1996`` reads ``de-AT``). ``und`` and ``root`` name the root locale.
|
|
96
|
+
Raises ``LookupError`` when no language with data is reached, so an unknown language
|
|
97
|
+
never reads another language's symbols silently.
|
|
98
|
+
"""
|
|
99
|
+
language, script, region, variants = _parse(locale)
|
|
100
|
+
data = table()
|
|
101
|
+
base = "-".join(part for part in (language, script, region) if part)
|
|
102
|
+
if variants and f"{base}-{'-'.join(variants)}" in data["locales"]:
|
|
103
|
+
return f"{base}-{'-'.join(variants)}"
|
|
104
|
+
if base in data["locales"]:
|
|
105
|
+
return base
|
|
106
|
+
found = None
|
|
107
|
+
default = data["scripts"].get(language)
|
|
108
|
+
if script and script == default: # "en-Latn-GB" is "en-GB": CLDR omits default scripts
|
|
109
|
+
base = "-".join(part for part in (language, region) if part)
|
|
110
|
+
elif not script and default:
|
|
111
|
+
likely = data["scripts"].get(f"{language}-{region}", default)
|
|
112
|
+
tagged = "-".join(part for part in (language, likely, region) if part)
|
|
113
|
+
if likely != default:
|
|
114
|
+
found = _walk(tagged)
|
|
115
|
+
elif tagged in data["locales"]:
|
|
116
|
+
found = tagged
|
|
117
|
+
found = found or _walk(base)
|
|
118
|
+
if found is None:
|
|
119
|
+
raise LookupError(f"no number data for locale {locale!r}")
|
|
120
|
+
return found
|
indgroup/_number.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Locale-aware formatting and strict parsing of numbers with CLDR grouping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Literal, get_args
|
|
7
|
+
|
|
8
|
+
from ._digits import group_digits, read_number
|
|
9
|
+
from ._symbols import number_symbols
|
|
10
|
+
from ._value import Number, split_value
|
|
11
|
+
|
|
12
|
+
Grouping = Literal["auto", "min2", "always", "off"]
|
|
13
|
+
|
|
14
|
+
_GROUPINGS: tuple[str, ...] = get_args(Grouping)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_number(value: Number, locale: str, *, grouping: Grouping = "auto") -> str:
|
|
18
|
+
"""Render ``value`` the way ``locale`` writes it, with Latin digits and no rounding.
|
|
19
|
+
|
|
20
|
+
``grouping`` follows the ``useGrouping`` option of ``Intl.NumberFormat``: ``auto`` uses
|
|
21
|
+
the locale's minimum grouping digits (Spanish writes ``1234`` but ``12.345``), ``min2``
|
|
22
|
+
requires at least two digits before the first separator, ``always`` groups every number
|
|
23
|
+
longer than a group, ``off`` never groups.
|
|
24
|
+
"""
|
|
25
|
+
if not isinstance(grouping, str) or grouping not in _GROUPINGS:
|
|
26
|
+
raise ValueError(f"grouping must be one of {', '.join(_GROUPINGS)}, not {grouping!r}")
|
|
27
|
+
symbols = number_symbols(locale)
|
|
28
|
+
negative, integer, fraction = split_value(value)
|
|
29
|
+
primary, secondary, minimum = symbols.primary, symbols.secondary, symbols.min_grouping
|
|
30
|
+
if grouping == "off":
|
|
31
|
+
primary = 0
|
|
32
|
+
elif grouping == "always":
|
|
33
|
+
minimum = 1
|
|
34
|
+
if primary == 0: # ICU groups by thousands when the pattern has no grouping
|
|
35
|
+
primary = secondary = 3
|
|
36
|
+
elif grouping == "min2":
|
|
37
|
+
minimum = max(2, minimum)
|
|
38
|
+
text = group_digits(integer, symbols.group, primary, secondary, minimum)
|
|
39
|
+
if fraction:
|
|
40
|
+
text = f"{text}{symbols.decimal}{fraction}"
|
|
41
|
+
return f"{symbols.minus}{text}" if negative else text
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_number(text: str, locale: str) -> int | Decimal:
|
|
45
|
+
"""Read back a number written the way ``locale`` writes it.
|
|
46
|
+
|
|
47
|
+
Strict: the locale's own minus sign, decimal and group symbols, ASCII digits, and groups
|
|
48
|
+
exactly where the locale puts them, or no groups at all. A value without a fraction is
|
|
49
|
+
an ``int``, one with a fraction a ``Decimal`` keeping every digit. Anything else raises
|
|
50
|
+
``ValueError``.
|
|
51
|
+
"""
|
|
52
|
+
symbols = number_symbols(locale)
|
|
53
|
+
sizes = (symbols.primary, symbols.secondary)
|
|
54
|
+
return read_number(text, symbols.decimal, symbols.group, sizes, symbols.minus)
|
indgroup/_symbols.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""The number symbols and grouping sizes of a CLDR locale."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ._locale import resolve_locale, table
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class NumberSymbols:
|
|
12
|
+
"""How a locale writes a number with Latin digits, as recorded in Unicode CLDR.
|
|
13
|
+
|
|
14
|
+
``primary`` is the size of the rightmost group and ``secondary`` the size of the groups
|
|
15
|
+
to its left (3 and 3 for ``1,234,567``, 3 and 2 for the Indian ``12,34,567``); zero
|
|
16
|
+
means the locale does not group. ``min_grouping`` is CLDR's minimumGroupingDigits: the
|
|
17
|
+
number of digits that must stand left of the first separator before any is written.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
locale: str
|
|
21
|
+
decimal: str
|
|
22
|
+
group: str
|
|
23
|
+
minus: str
|
|
24
|
+
primary: int
|
|
25
|
+
secondary: int
|
|
26
|
+
min_grouping: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def number_symbols(locale: str) -> NumberSymbols:
|
|
30
|
+
"""The symbols and grouping sizes ``locale`` uses, after :func:`resolve_locale`."""
|
|
31
|
+
resolved = resolve_locale(locale)
|
|
32
|
+
data = table()
|
|
33
|
+
profile = data["profiles"][data["locales"][resolved]]
|
|
34
|
+
decimal, group, minus, primary, secondary, minimum = profile
|
|
35
|
+
return NumberSymbols(resolved, decimal, group, minus, primary, secondary, minimum)
|
indgroup/_value.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Exact decimal text of an int, float or Decimal, with no rounding and no float drift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
Number: TypeAlias = int | float | Decimal
|
|
10
|
+
|
|
11
|
+
DIGITS = re.compile(r"[0-9]+")
|
|
12
|
+
|
|
13
|
+
# A bound on the integer digits written, so a value such as Decimal("1E+999999999") is refused
|
|
14
|
+
# instead of building a gigabyte of text.
|
|
15
|
+
MAX_DIGITS = 1_000_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def split_value(value: object) -> tuple[bool, str, str]:
|
|
19
|
+
"""Sign, integer digits and fraction digits of ``value``, as written.
|
|
20
|
+
|
|
21
|
+
A float is read through its shortest round-trip spelling (its ``repr``), so ``0.1`` is
|
|
22
|
+
``0.1`` and not the binary value it stands for, and a whole float has no fraction. A
|
|
23
|
+
Decimal keeps its exponent: trailing zeros and negative zero survive. Methods of
|
|
24
|
+
subclasses are never called, so they cannot change the digits.
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(value, bool):
|
|
27
|
+
raise TypeError("value must be an int, float or Decimal, not bool")
|
|
28
|
+
from_float = False
|
|
29
|
+
if isinstance(value, int):
|
|
30
|
+
number = Decimal(value) # no int-to-str conversion, so no 4300-digit limit
|
|
31
|
+
elif isinstance(value, float):
|
|
32
|
+
number = Decimal(float.__repr__(value))
|
|
33
|
+
from_float = True
|
|
34
|
+
elif isinstance(value, Decimal):
|
|
35
|
+
number = value
|
|
36
|
+
else:
|
|
37
|
+
raise TypeError(f"value must be an int, float or Decimal, not {type(value).__name__}")
|
|
38
|
+
if not Decimal.is_finite(number):
|
|
39
|
+
raise ValueError(f"value must be a finite number, not {Decimal.__str__(number)}")
|
|
40
|
+
if Decimal.adjusted(number) >= MAX_DIGITS:
|
|
41
|
+
raise ValueError(f"value has more than {MAX_DIGITS} integer digits")
|
|
42
|
+
text = Decimal.__format__(number, "f")
|
|
43
|
+
negative = text.startswith("-")
|
|
44
|
+
integer, _, fraction = text.removeprefix("-").partition(".")
|
|
45
|
+
if from_float and fraction == "0":
|
|
46
|
+
fraction = ""
|
|
47
|
+
return negative, integer, fraction
|
indgroup/py.typed
ADDED
|
File without changes
|
|
@@ -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,17 @@
|
|
|
1
|
+
indgroup/__init__.py,sha256=r_er1PQDulBJ5ByyicwIRQL9x8lKAn9rzNxrSnPTZF0,653
|
|
2
|
+
indgroup/__main__.py,sha256=Pz_hbPKQp-1CnVa6kEn9a_XOKaiiIiy_el4_14Z3yM8,142
|
|
3
|
+
indgroup/_cldr.json,sha256=2Uz_k3nMOpAIftLUTwkvsYUICjRRtD-uG2M9cQ6SoTE,18637
|
|
4
|
+
indgroup/_cli.py,sha256=mJqvnne8yl9W7lYE2OADK5YzFFCCuonn5jWVmHwUlks,3946
|
|
5
|
+
indgroup/_digits.py,sha256=louICEIsPhX4m9GaZcIU6gxzpxo7HR9HxYcuoBzjuTc,2861
|
|
6
|
+
indgroup/_grouping.py,sha256=WmJgRz-PbGrwyGDbThMF6u1ts7PTUKAH4nFuQtFklxU,1558
|
|
7
|
+
indgroup/_locale.py,sha256=XV2l181P2FFaxgQoKrkUYSGaGcWCNVSDbES2JPYOO64,4963
|
|
8
|
+
indgroup/_number.py,sha256=zxzbUgWp8dnptkspnytd5wkp7st0vqzfjCDE_7C47AI,2347
|
|
9
|
+
indgroup/_symbols.py,sha256=riLL31ey8gmqFdo7IcAWLcOAnI7VyQb1gqe9n2mXTKc,1205
|
|
10
|
+
indgroup/_value.py,sha256=2Z3prbbztJhh1IE_VRd66fBLrmdqQSX051lekPpZw6M,1921
|
|
11
|
+
indgroup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
indgroup-0.2.0.dist-info/METADATA,sha256=MKViL1VANvHk5znxdJD4evA4tohbJHSAYqQVhrKwMAY,8653
|
|
13
|
+
indgroup-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
14
|
+
indgroup-0.2.0.dist-info/entry_points.txt,sha256=5jONNS1Hl-_N_phC_Y7WZUV-ZGTzsSR1TqIvEnFeP3Y,48
|
|
15
|
+
indgroup-0.2.0.dist-info/licenses/LICENSE,sha256=_WmABlQG__oqlx4Rzp-qDJ10xND5KCLScftbhXbLC_c,1070
|
|
16
|
+
indgroup-0.2.0.dist-info/licenses/LICENSE-UNICODE,sha256=tMCujvBPcFn5bOW74EZ_n-b22Bu-E1F3Ad_rlh-00LY,2033
|
|
17
|
+
indgroup-0.2.0.dist-info/RECORD,,
|
|
@@ -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
|