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 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
@@ -0,0 +1,7 @@
1
+ """``python -m indgroup``: see :mod:`indgroup._cli`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._cli import main
6
+
7
+ raise SystemExit(main())
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ indgroup = indgroup._cli:main
@@ -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