words2num2 0.1.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.
- words2num2/__init__.py +345 -0
- words2num2/__main__.py +39 -0
- words2num2/_version.py +3 -0
- words2num2/base.py +155 -0
- words2num2/compat.py +9 -0
- words2num2/converters/__init__.py +2 -0
- words2num2/converters/sentence.py +116 -0
- words2num2/lang_AF.py +15 -0
- words2num2/lang_AM.py +15 -0
- words2num2/lang_AR.py +15 -0
- words2num2/lang_AS.py +15 -0
- words2num2/lang_AZ.py +15 -0
- words2num2/lang_BA.py +15 -0
- words2num2/lang_BE.py +15 -0
- words2num2/lang_BG.py +15 -0
- words2num2/lang_BN.py +15 -0
- words2num2/lang_BO.py +15 -0
- words2num2/lang_BR.py +15 -0
- words2num2/lang_BS.py +15 -0
- words2num2/lang_CA.py +15 -0
- words2num2/lang_CE.py +15 -0
- words2num2/lang_CS.py +15 -0
- words2num2/lang_CY.py +15 -0
- words2num2/lang_DA.py +15 -0
- words2num2/lang_DE.py +15 -0
- words2num2/lang_EL.py +15 -0
- words2num2/lang_EN.py +242 -0
- words2num2/lang_EN_IN.py +15 -0
- words2num2/lang_EN_NG.py +15 -0
- words2num2/lang_EO.py +15 -0
- words2num2/lang_ES.py +15 -0
- words2num2/lang_ES_CO.py +15 -0
- words2num2/lang_ES_CR.py +15 -0
- words2num2/lang_ES_GT.py +15 -0
- words2num2/lang_ES_NI.py +15 -0
- words2num2/lang_ES_VE.py +15 -0
- words2num2/lang_ET.py +15 -0
- words2num2/lang_EU.py +15 -0
- words2num2/lang_FA.py +15 -0
- words2num2/lang_FI.py +15 -0
- words2num2/lang_FO.py +15 -0
- words2num2/lang_FR.py +15 -0
- words2num2/lang_FR_BE.py +15 -0
- words2num2/lang_FR_CH.py +15 -0
- words2num2/lang_FR_DZ.py +15 -0
- words2num2/lang_GL.py +15 -0
- words2num2/lang_GU.py +15 -0
- words2num2/lang_HA.py +15 -0
- words2num2/lang_HAW.py +15 -0
- words2num2/lang_HE.py +15 -0
- words2num2/lang_HI.py +15 -0
- words2num2/lang_HR.py +15 -0
- words2num2/lang_HT.py +15 -0
- words2num2/lang_HU.py +15 -0
- words2num2/lang_HY.py +15 -0
- words2num2/lang_ID.py +15 -0
- words2num2/lang_IS.py +15 -0
- words2num2/lang_IT.py +15 -0
- words2num2/lang_JA.py +15 -0
- words2num2/lang_JW.py +15 -0
- words2num2/lang_KA.py +15 -0
- words2num2/lang_KK.py +15 -0
- words2num2/lang_KM.py +15 -0
- words2num2/lang_KN.py +15 -0
- words2num2/lang_KO.py +15 -0
- words2num2/lang_KZ.py +15 -0
- words2num2/lang_LA.py +15 -0
- words2num2/lang_LB.py +15 -0
- words2num2/lang_LN.py +15 -0
- words2num2/lang_LO.py +15 -0
- words2num2/lang_LT.py +15 -0
- words2num2/lang_LV.py +15 -0
- words2num2/lang_MG.py +15 -0
- words2num2/lang_MI.py +15 -0
- words2num2/lang_MK.py +15 -0
- words2num2/lang_ML.py +15 -0
- words2num2/lang_MN.py +15 -0
- words2num2/lang_MR.py +15 -0
- words2num2/lang_MS.py +15 -0
- words2num2/lang_MT.py +15 -0
- words2num2/lang_MY.py +15 -0
- words2num2/lang_NE.py +15 -0
- words2num2/lang_NL.py +15 -0
- words2num2/lang_NN.py +15 -0
- words2num2/lang_NO.py +15 -0
- words2num2/lang_OC.py +15 -0
- words2num2/lang_PA.py +15 -0
- words2num2/lang_PL.py +15 -0
- words2num2/lang_PS.py +15 -0
- words2num2/lang_PT.py +15 -0
- words2num2/lang_PT_BR.py +15 -0
- words2num2/lang_RO.py +15 -0
- words2num2/lang_RU.py +15 -0
- words2num2/lang_SA.py +15 -0
- words2num2/lang_SD.py +15 -0
- words2num2/lang_SI.py +15 -0
- words2num2/lang_SK.py +15 -0
- words2num2/lang_SL.py +15 -0
- words2num2/lang_SN.py +15 -0
- words2num2/lang_SO.py +15 -0
- words2num2/lang_SQ.py +15 -0
- words2num2/lang_SR.py +15 -0
- words2num2/lang_SU.py +15 -0
- words2num2/lang_SV.py +15 -0
- words2num2/lang_SW.py +15 -0
- words2num2/lang_TA.py +15 -0
- words2num2/lang_TE.py +15 -0
- words2num2/lang_TET.py +15 -0
- words2num2/lang_TG.py +15 -0
- words2num2/lang_TH.py +15 -0
- words2num2/lang_TK.py +15 -0
- words2num2/lang_TL.py +15 -0
- words2num2/lang_TR.py +15 -0
- words2num2/lang_TT.py +15 -0
- words2num2/lang_UK.py +15 -0
- words2num2/lang_UR.py +15 -0
- words2num2/lang_UZ.py +15 -0
- words2num2/lang_VI.py +15 -0
- words2num2/lang_WO.py +15 -0
- words2num2/lang_YI.py +15 -0
- words2num2/lang_YO.py +15 -0
- words2num2/lang_ZH.py +15 -0
- words2num2/lang_ZH_CN.py +15 -0
- words2num2/lang_ZH_HK.py +15 -0
- words2num2/lang_ZH_TW.py +15 -0
- words2num2/utils.py +23 -0
- words2num2-0.1.0.data/scripts/words2num2 +28 -0
- words2num2-0.1.0.dist-info/METADATA +168 -0
- words2num2-0.1.0.dist-info/RECORD +133 -0
- words2num2-0.1.0.dist-info/WHEEL +5 -0
- words2num2-0.1.0.dist-info/entry_points.txt +2 -0
- words2num2-0.1.0.dist-info/licenses/COPYING +145 -0
- words2num2-0.1.0.dist-info/top_level.txt +1 -0
words2num2/__init__.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2026, Jean-Louis Queguiner. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# This library is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
6
|
+
# License as published by the Free Software Foundation; either
|
|
7
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
8
|
+
"""words2num2 — inverse of num2words2.
|
|
9
|
+
|
|
10
|
+
Convert spoken-form numbers ("forty-two", "trois cent quatre", ...) back
|
|
11
|
+
into numeric values across the same 100+ locales as num2words2.
|
|
12
|
+
|
|
13
|
+
>>> from words2num2 import words2num
|
|
14
|
+
>>> words2num("forty-two")
|
|
15
|
+
42
|
|
16
|
+
>>> words2num("one thousand two hundred")
|
|
17
|
+
1200
|
|
18
|
+
>>> words2num("three point one four")
|
|
19
|
+
Decimal('3.14')
|
|
20
|
+
>>> words2num("trente-deux", lang="fr")
|
|
21
|
+
32
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import unicode_literals
|
|
24
|
+
|
|
25
|
+
from . import (
|
|
26
|
+
lang_AF,
|
|
27
|
+
lang_AM,
|
|
28
|
+
lang_AR,
|
|
29
|
+
lang_AS,
|
|
30
|
+
lang_AZ,
|
|
31
|
+
lang_BA,
|
|
32
|
+
lang_BE,
|
|
33
|
+
lang_BG,
|
|
34
|
+
lang_BN,
|
|
35
|
+
lang_BO,
|
|
36
|
+
lang_BR,
|
|
37
|
+
lang_BS,
|
|
38
|
+
lang_CA,
|
|
39
|
+
lang_CE,
|
|
40
|
+
lang_CS,
|
|
41
|
+
lang_CY,
|
|
42
|
+
lang_DA,
|
|
43
|
+
lang_DE,
|
|
44
|
+
lang_EL,
|
|
45
|
+
lang_EN,
|
|
46
|
+
lang_EN_IN,
|
|
47
|
+
lang_EN_NG,
|
|
48
|
+
lang_EO,
|
|
49
|
+
lang_ES,
|
|
50
|
+
lang_ES_CO,
|
|
51
|
+
lang_ES_CR,
|
|
52
|
+
lang_ES_GT,
|
|
53
|
+
lang_ES_NI,
|
|
54
|
+
lang_ES_VE,
|
|
55
|
+
lang_ET,
|
|
56
|
+
lang_EU,
|
|
57
|
+
lang_FA,
|
|
58
|
+
lang_FI,
|
|
59
|
+
lang_FO,
|
|
60
|
+
lang_FR,
|
|
61
|
+
lang_FR_BE,
|
|
62
|
+
lang_FR_CH,
|
|
63
|
+
lang_FR_DZ,
|
|
64
|
+
lang_GL,
|
|
65
|
+
lang_GU,
|
|
66
|
+
lang_HA,
|
|
67
|
+
lang_HAW,
|
|
68
|
+
lang_HE,
|
|
69
|
+
lang_HI,
|
|
70
|
+
lang_HR,
|
|
71
|
+
lang_HT,
|
|
72
|
+
lang_HU,
|
|
73
|
+
lang_HY,
|
|
74
|
+
lang_ID,
|
|
75
|
+
lang_IS,
|
|
76
|
+
lang_IT,
|
|
77
|
+
lang_JA,
|
|
78
|
+
lang_JW,
|
|
79
|
+
lang_KA,
|
|
80
|
+
lang_KK,
|
|
81
|
+
lang_KM,
|
|
82
|
+
lang_KN,
|
|
83
|
+
lang_KO,
|
|
84
|
+
lang_KZ,
|
|
85
|
+
lang_LA,
|
|
86
|
+
lang_LB,
|
|
87
|
+
lang_LN,
|
|
88
|
+
lang_LO,
|
|
89
|
+
lang_LT,
|
|
90
|
+
lang_LV,
|
|
91
|
+
lang_MG,
|
|
92
|
+
lang_MI,
|
|
93
|
+
lang_MK,
|
|
94
|
+
lang_ML,
|
|
95
|
+
lang_MN,
|
|
96
|
+
lang_MR,
|
|
97
|
+
lang_MS,
|
|
98
|
+
lang_MT,
|
|
99
|
+
lang_MY,
|
|
100
|
+
lang_NE,
|
|
101
|
+
lang_NL,
|
|
102
|
+
lang_NN,
|
|
103
|
+
lang_NO,
|
|
104
|
+
lang_OC,
|
|
105
|
+
lang_PA,
|
|
106
|
+
lang_PL,
|
|
107
|
+
lang_PS,
|
|
108
|
+
lang_PT,
|
|
109
|
+
lang_PT_BR,
|
|
110
|
+
lang_RO,
|
|
111
|
+
lang_RU,
|
|
112
|
+
lang_SA,
|
|
113
|
+
lang_SD,
|
|
114
|
+
lang_SI,
|
|
115
|
+
lang_SK,
|
|
116
|
+
lang_SL,
|
|
117
|
+
lang_SN,
|
|
118
|
+
lang_SO,
|
|
119
|
+
lang_SQ,
|
|
120
|
+
lang_SR,
|
|
121
|
+
lang_SU,
|
|
122
|
+
lang_SV,
|
|
123
|
+
lang_SW,
|
|
124
|
+
lang_TA,
|
|
125
|
+
lang_TE,
|
|
126
|
+
lang_TET,
|
|
127
|
+
lang_TG,
|
|
128
|
+
lang_TH,
|
|
129
|
+
lang_TK,
|
|
130
|
+
lang_TL,
|
|
131
|
+
lang_TR,
|
|
132
|
+
lang_TT,
|
|
133
|
+
lang_UK,
|
|
134
|
+
lang_UR,
|
|
135
|
+
lang_UZ,
|
|
136
|
+
lang_VI,
|
|
137
|
+
lang_WO,
|
|
138
|
+
lang_YI,
|
|
139
|
+
lang_YO,
|
|
140
|
+
lang_ZH,
|
|
141
|
+
lang_ZH_CN,
|
|
142
|
+
lang_ZH_HK,
|
|
143
|
+
lang_ZH_TW,
|
|
144
|
+
)
|
|
145
|
+
from .base import Words2NumError
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
from ._version import __version__, __version_tuple__
|
|
149
|
+
except ImportError:
|
|
150
|
+
__version__ = "unknown"
|
|
151
|
+
__version_tuple__ = (0, 0, 0, "unknown", 0)
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"words2num",
|
|
155
|
+
"words2num_sentence",
|
|
156
|
+
"convert_sentence",
|
|
157
|
+
"sentence_to_words",
|
|
158
|
+
"Words2NumError",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
CONVERTER_CLASSES = {
|
|
163
|
+
"af": lang_AF.Words2Num_AF(),
|
|
164
|
+
"am": lang_AM.Words2Num_AM(),
|
|
165
|
+
"ar": lang_AR.Words2Num_AR(),
|
|
166
|
+
"as": lang_AS.Words2Num_AS(),
|
|
167
|
+
"az": lang_AZ.Words2Num_AZ(),
|
|
168
|
+
"ba": lang_BA.Words2Num_BA(),
|
|
169
|
+
"be": lang_BE.Words2Num_BE(),
|
|
170
|
+
"bg": lang_BG.Words2Num_BG(),
|
|
171
|
+
"bn": lang_BN.Words2Num_BN(),
|
|
172
|
+
"bo": lang_BO.Words2Num_BO(),
|
|
173
|
+
"br": lang_BR.Words2Num_BR(),
|
|
174
|
+
"bs": lang_BS.Words2Num_BS(),
|
|
175
|
+
"ca": lang_CA.Words2Num_CA(),
|
|
176
|
+
"ce": lang_CE.Words2Num_CE(),
|
|
177
|
+
"cs": lang_CS.Words2Num_CS(),
|
|
178
|
+
"cy": lang_CY.Words2Num_CY(),
|
|
179
|
+
"da": lang_DA.Words2Num_DA(),
|
|
180
|
+
"de": lang_DE.Words2Num_DE(),
|
|
181
|
+
"el": lang_EL.Words2Num_EL(),
|
|
182
|
+
"en": lang_EN.Words2Num_EN(),
|
|
183
|
+
"en_IN": lang_EN_IN.Words2Num_EN_IN(),
|
|
184
|
+
"en_NG": lang_EN_NG.Words2Num_EN_NG(),
|
|
185
|
+
"eo": lang_EO.Words2Num_EO(),
|
|
186
|
+
"es": lang_ES.Words2Num_ES(),
|
|
187
|
+
"es_CO": lang_ES_CO.Words2Num_ES_CO(),
|
|
188
|
+
"es_CR": lang_ES_CR.Words2Num_ES_CR(),
|
|
189
|
+
"es_GT": lang_ES_GT.Words2Num_ES_GT(),
|
|
190
|
+
"es_NI": lang_ES_NI.Words2Num_ES_NI(),
|
|
191
|
+
"es_VE": lang_ES_VE.Words2Num_ES_VE(),
|
|
192
|
+
"et": lang_ET.Words2Num_ET(),
|
|
193
|
+
"eu": lang_EU.Words2Num_EU(),
|
|
194
|
+
"fa": lang_FA.Words2Num_FA(),
|
|
195
|
+
"fi": lang_FI.Words2Num_FI(),
|
|
196
|
+
"fo": lang_FO.Words2Num_FO(),
|
|
197
|
+
"fr": lang_FR.Words2Num_FR(),
|
|
198
|
+
"fr_BE": lang_FR_BE.Words2Num_FR_BE(),
|
|
199
|
+
"fr_CH": lang_FR_CH.Words2Num_FR_CH(),
|
|
200
|
+
"fr_DZ": lang_FR_DZ.Words2Num_FR_DZ(),
|
|
201
|
+
"gl": lang_GL.Words2Num_GL(),
|
|
202
|
+
"gu": lang_GU.Words2Num_GU(),
|
|
203
|
+
"ha": lang_HA.Words2Num_HA(),
|
|
204
|
+
"haw": lang_HAW.Words2Num_HAW(),
|
|
205
|
+
"he": lang_HE.Words2Num_HE(),
|
|
206
|
+
"hi": lang_HI.Words2Num_HI(),
|
|
207
|
+
"hr": lang_HR.Words2Num_HR(),
|
|
208
|
+
"ht": lang_HT.Words2Num_HT(),
|
|
209
|
+
"hu": lang_HU.Words2Num_HU(),
|
|
210
|
+
"hy": lang_HY.Words2Num_HY(),
|
|
211
|
+
"id": lang_ID.Words2Num_ID(),
|
|
212
|
+
"is": lang_IS.Words2Num_IS(),
|
|
213
|
+
"it": lang_IT.Words2Num_IT(),
|
|
214
|
+
"ja": lang_JA.Words2Num_JA(),
|
|
215
|
+
"jw": lang_JW.Words2Num_JW(),
|
|
216
|
+
"ka": lang_KA.Words2Num_KA(),
|
|
217
|
+
"kk": lang_KK.Words2Num_KK(),
|
|
218
|
+
"km": lang_KM.Words2Num_KM(),
|
|
219
|
+
"kn": lang_KN.Words2Num_KN(),
|
|
220
|
+
"ko": lang_KO.Words2Num_KO(),
|
|
221
|
+
"kz": lang_KZ.Words2Num_KZ(),
|
|
222
|
+
"la": lang_LA.Words2Num_LA(),
|
|
223
|
+
"lb": lang_LB.Words2Num_LB(),
|
|
224
|
+
"ln": lang_LN.Words2Num_LN(),
|
|
225
|
+
"lo": lang_LO.Words2Num_LO(),
|
|
226
|
+
"lt": lang_LT.Words2Num_LT(),
|
|
227
|
+
"lv": lang_LV.Words2Num_LV(),
|
|
228
|
+
"mg": lang_MG.Words2Num_MG(),
|
|
229
|
+
"mi": lang_MI.Words2Num_MI(),
|
|
230
|
+
"mk": lang_MK.Words2Num_MK(),
|
|
231
|
+
"ml": lang_ML.Words2Num_ML(),
|
|
232
|
+
"mn": lang_MN.Words2Num_MN(),
|
|
233
|
+
"mr": lang_MR.Words2Num_MR(),
|
|
234
|
+
"ms": lang_MS.Words2Num_MS(),
|
|
235
|
+
"mt": lang_MT.Words2Num_MT(),
|
|
236
|
+
"my": lang_MY.Words2Num_MY(),
|
|
237
|
+
"ne": lang_NE.Words2Num_NE(),
|
|
238
|
+
"nl": lang_NL.Words2Num_NL(),
|
|
239
|
+
"nn": lang_NN.Words2Num_NN(),
|
|
240
|
+
"no": lang_NO.Words2Num_NO(),
|
|
241
|
+
"oc": lang_OC.Words2Num_OC(),
|
|
242
|
+
"pa": lang_PA.Words2Num_PA(),
|
|
243
|
+
"pl": lang_PL.Words2Num_PL(),
|
|
244
|
+
"ps": lang_PS.Words2Num_PS(),
|
|
245
|
+
"pt": lang_PT.Words2Num_PT(),
|
|
246
|
+
"pt_BR": lang_PT_BR.Words2Num_PT_BR(),
|
|
247
|
+
"ro": lang_RO.Words2Num_RO(),
|
|
248
|
+
"ru": lang_RU.Words2Num_RU(),
|
|
249
|
+
"sa": lang_SA.Words2Num_SA(),
|
|
250
|
+
"sd": lang_SD.Words2Num_SD(),
|
|
251
|
+
"si": lang_SI.Words2Num_SI(),
|
|
252
|
+
"sk": lang_SK.Words2Num_SK(),
|
|
253
|
+
"sl": lang_SL.Words2Num_SL(),
|
|
254
|
+
"sn": lang_SN.Words2Num_SN(),
|
|
255
|
+
"so": lang_SO.Words2Num_SO(),
|
|
256
|
+
"sq": lang_SQ.Words2Num_SQ(),
|
|
257
|
+
"sr": lang_SR.Words2Num_SR(),
|
|
258
|
+
"su": lang_SU.Words2Num_SU(),
|
|
259
|
+
"sv": lang_SV.Words2Num_SV(),
|
|
260
|
+
"sw": lang_SW.Words2Num_SW(),
|
|
261
|
+
"ta": lang_TA.Words2Num_TA(),
|
|
262
|
+
"te": lang_TE.Words2Num_TE(),
|
|
263
|
+
"tet": lang_TET.Words2Num_TET(),
|
|
264
|
+
"tg": lang_TG.Words2Num_TG(),
|
|
265
|
+
"th": lang_TH.Words2Num_TH(),
|
|
266
|
+
"tk": lang_TK.Words2Num_TK(),
|
|
267
|
+
"tl": lang_TL.Words2Num_TL(),
|
|
268
|
+
"tr": lang_TR.Words2Num_TR(),
|
|
269
|
+
"tt": lang_TT.Words2Num_TT(),
|
|
270
|
+
"uk": lang_UK.Words2Num_UK(),
|
|
271
|
+
"ur": lang_UR.Words2Num_UR(),
|
|
272
|
+
"uz": lang_UZ.Words2Num_UZ(),
|
|
273
|
+
"vi": lang_VI.Words2Num_VI(),
|
|
274
|
+
"wo": lang_WO.Words2Num_WO(),
|
|
275
|
+
"yi": lang_YI.Words2Num_YI(),
|
|
276
|
+
"yo": lang_YO.Words2Num_YO(),
|
|
277
|
+
"zh": lang_ZH_CN.Words2Num_ZH_CN(), # default zh -> simplified Chinese
|
|
278
|
+
"zh_CN": lang_ZH_CN.Words2Num_ZH_CN(),
|
|
279
|
+
"zh_HK": lang_ZH_HK.Words2Num_ZH_HK(),
|
|
280
|
+
"zh_TW": lang_ZH_TW.Words2Num_ZH_TW(),
|
|
281
|
+
# Aliases
|
|
282
|
+
"jp": lang_JA.Words2Num_JA(),
|
|
283
|
+
"cn": lang_ZH_CN.Words2Num_ZH_CN(),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
CONVERTER_TYPES = ["cardinal", "ordinal", "ordinal_num", "year", "currency"]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _resolve_lang(lang):
|
|
290
|
+
if lang in CONVERTER_CLASSES:
|
|
291
|
+
return lang
|
|
292
|
+
normalized = lang.replace("-", "_")
|
|
293
|
+
if normalized in CONVERTER_CLASSES:
|
|
294
|
+
return normalized
|
|
295
|
+
parts = normalized.split("_")
|
|
296
|
+
if len(parts) >= 2:
|
|
297
|
+
candidate = "{}_{}".format(parts[0].lower(), parts[1].upper())
|
|
298
|
+
if candidate in CONVERTER_CLASSES:
|
|
299
|
+
return candidate
|
|
300
|
+
if parts[0] in CONVERTER_CLASSES:
|
|
301
|
+
return parts[0]
|
|
302
|
+
if normalized[:2] in CONVERTER_CLASSES:
|
|
303
|
+
return normalized[:2]
|
|
304
|
+
raise NotImplementedError("language %r is not supported" % lang)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def words2num(text, lang="en", to="cardinal", **kwargs):
|
|
308
|
+
"""Parse ``text`` (a number written in words) into a numeric value.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
text: The words to parse, e.g. ``"forty-two"``.
|
|
312
|
+
lang: Language code (default ``"en"``).
|
|
313
|
+
to: Conversion type — ``cardinal``, ``ordinal``, ``ordinal_num``,
|
|
314
|
+
``year``, or ``currency``.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
``int``, ``float``, or ``Decimal`` depending on the input.
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
NotImplementedError: if ``lang`` is not registered.
|
|
321
|
+
Words2NumError: if ``text`` cannot be parsed.
|
|
322
|
+
"""
|
|
323
|
+
resolved = _resolve_lang(lang)
|
|
324
|
+
converter = CONVERTER_CLASSES[resolved]
|
|
325
|
+
|
|
326
|
+
if to not in CONVERTER_TYPES:
|
|
327
|
+
raise NotImplementedError("conversion type %r unsupported" % to)
|
|
328
|
+
|
|
329
|
+
return getattr(converter, "to_{}".format(to))(text, **kwargs)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def words2num_sentence(sentence, lang="en", to="cardinal", **kwargs):
|
|
333
|
+
"""Convert all word-numbers in ``sentence`` to numeric form.
|
|
334
|
+
|
|
335
|
+
Walks the sentence and tries to match the longest possible run of
|
|
336
|
+
number tokens at each position. Non-number tokens pass through.
|
|
337
|
+
"""
|
|
338
|
+
from .converters.sentence import SentenceConverter
|
|
339
|
+
|
|
340
|
+
return SentenceConverter().convert(sentence, lang=lang, to=to, **kwargs)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Aliases (parity with num2words2)
|
|
344
|
+
convert_sentence = words2num_sentence
|
|
345
|
+
sentence_to_words = words2num_sentence
|
words2num2/__main__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""words2num2 CLI entrypoint."""
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, words2num
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
args = sys.argv[1:]
|
|
12
|
+
if not args or args[0] in ("-h", "--help"):
|
|
13
|
+
print(
|
|
14
|
+
"usage: words2num2 <text> [--lang=LANG] [--to=TYPE]\n"
|
|
15
|
+
" LANG defaults to 'en'\n"
|
|
16
|
+
" TYPE is cardinal | ordinal | ordinal_num | year | currency"
|
|
17
|
+
)
|
|
18
|
+
return 0
|
|
19
|
+
if args[0] == "--version":
|
|
20
|
+
print("words2num2 {}".format(__version__))
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
lang = "en"
|
|
24
|
+
to = "cardinal"
|
|
25
|
+
text_parts = []
|
|
26
|
+
for arg in args:
|
|
27
|
+
if arg.startswith("--lang="):
|
|
28
|
+
lang = arg.split("=", 1)[1]
|
|
29
|
+
elif arg.startswith("--to="):
|
|
30
|
+
to = arg.split("=", 1)[1]
|
|
31
|
+
else:
|
|
32
|
+
text_parts.append(arg)
|
|
33
|
+
text = " ".join(text_parts)
|
|
34
|
+
print(words2num(text, lang=lang, to=to))
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
sys.exit(main())
|
words2num2/_version.py
ADDED
words2num2/base.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2026, Jean-Louis Queguiner. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# This library is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU Lesser General Public
|
|
6
|
+
# License as published by the Free Software Foundation; either
|
|
7
|
+
# version 2.1 of the License, or (at your option) any later version.
|
|
8
|
+
"""Base classes for the words-to-number conversion."""
|
|
9
|
+
from __future__ import unicode_literals
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import unicodedata
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Words2NumError(ValueError):
|
|
17
|
+
"""Raised when an input string cannot be parsed as a number."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Words2Num_Base(object):
|
|
21
|
+
"""Base class for inverse-of-num2words converters.
|
|
22
|
+
|
|
23
|
+
Subclasses configure ``LANG`` (the num2words2 locale code). The default
|
|
24
|
+
implementation uses :mod:`num2words2` to derive a reverse lookup table
|
|
25
|
+
on demand, which works for *any* locale supported by num2words2 — at
|
|
26
|
+
the cost of ambiguity for languages that produce multi-token forms
|
|
27
|
+
(compound French numbers, written-Chinese, etc.).
|
|
28
|
+
|
|
29
|
+
Hand-written grammar parsers should override :meth:`to_cardinal` and
|
|
30
|
+
related ``to_*`` methods to handle conjunctions, ordinals, decimals,
|
|
31
|
+
and out-of-range values precisely.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
LANG = None
|
|
35
|
+
# Lookup tables seeded by ``setup``. Subclasses may extend.
|
|
36
|
+
DECIMAL_SEPARATORS = ("point", "dot", "comma")
|
|
37
|
+
NEGATIVE_WORDS = ("minus", "negative")
|
|
38
|
+
AND_WORDS = ()
|
|
39
|
+
LOOKUP_RANGE = (-1, 10001) # build reverse table for this range
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.errmsg_unparseable = "cannot parse %r as a number"
|
|
43
|
+
self.errmsg_lang = "no num2words2 backend for locale %r"
|
|
44
|
+
self._cardinal_table = None
|
|
45
|
+
self._ordinal_table = None
|
|
46
|
+
self.setup()
|
|
47
|
+
|
|
48
|
+
# -- override hooks ---------------------------------------------------
|
|
49
|
+
def setup(self):
|
|
50
|
+
"""Subclasses may set attributes prior to the first conversion."""
|
|
51
|
+
|
|
52
|
+
# -- public API -------------------------------------------------------
|
|
53
|
+
def str_to_number(self, text, to="cardinal"):
|
|
54
|
+
"""Convert ``text`` (words) to a number using the table-based path."""
|
|
55
|
+
return getattr(self, "to_{}".format(to))(text)
|
|
56
|
+
|
|
57
|
+
def to_cardinal(self, text):
|
|
58
|
+
return self._lookup(self._normalize(text), self._cardinal())
|
|
59
|
+
|
|
60
|
+
def to_ordinal(self, text):
|
|
61
|
+
return self._lookup(self._normalize(text), self._ordinal())
|
|
62
|
+
|
|
63
|
+
def to_ordinal_num(self, text):
|
|
64
|
+
# ordinal_num is e.g. "1st" — reuse direct numeric extraction.
|
|
65
|
+
m = re.search(r"-?\d+", text)
|
|
66
|
+
if not m:
|
|
67
|
+
raise Words2NumError(self.errmsg_unparseable % text)
|
|
68
|
+
return int(m.group())
|
|
69
|
+
|
|
70
|
+
def to_year(self, text):
|
|
71
|
+
return self.to_cardinal(text)
|
|
72
|
+
|
|
73
|
+
def to_currency(self, text):
|
|
74
|
+
# Currency parsing is locale-specific and best implemented per-language.
|
|
75
|
+
# Default: strip currency words and try cardinal.
|
|
76
|
+
return self.to_cardinal(text)
|
|
77
|
+
|
|
78
|
+
# -- helpers ----------------------------------------------------------
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _normalize(text):
|
|
81
|
+
"""Lowercase, strip diacritics, collapse whitespace, remove hyphens."""
|
|
82
|
+
if not isinstance(text, str):
|
|
83
|
+
raise Words2NumError("expected str, got %r" % type(text).__name__)
|
|
84
|
+
# Normalize diacritics so e.g. "trente-deux" matches "trente deux".
|
|
85
|
+
nfkd = unicodedata.normalize("NFKD", text)
|
|
86
|
+
text = "".join(c for c in nfkd if not unicodedata.combining(c))
|
|
87
|
+
text = text.lower().replace("_", " ")
|
|
88
|
+
# Hyphen joins word-pairs ("forty-two") but signals sign before a
|
|
89
|
+
# digit ("-17"). Replace only the word-joining hyphens.
|
|
90
|
+
text = re.sub(r"(?<=[a-z])-(?=[a-z])", " ", text)
|
|
91
|
+
text = re.sub(r"[,;:!\?\"']", " ", text)
|
|
92
|
+
# Remove sentence-final '.' but keep decimal points.
|
|
93
|
+
text = re.sub(r"\.(?!\d)", " ", text)
|
|
94
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
95
|
+
return text
|
|
96
|
+
|
|
97
|
+
def _lookup(self, normalized, table):
|
|
98
|
+
if not normalized:
|
|
99
|
+
raise Words2NumError(self.errmsg_unparseable % normalized)
|
|
100
|
+
# Handle leading negative.
|
|
101
|
+
sign = 1
|
|
102
|
+
for neg in self.NEGATIVE_WORDS:
|
|
103
|
+
if normalized.startswith(neg + " "):
|
|
104
|
+
sign = -1
|
|
105
|
+
normalized = normalized[len(neg) + 1:]
|
|
106
|
+
break
|
|
107
|
+
if normalized == neg:
|
|
108
|
+
raise Words2NumError(self.errmsg_unparseable % normalized)
|
|
109
|
+
if normalized in table:
|
|
110
|
+
return sign * table[normalized]
|
|
111
|
+
# If the input is already digits, return as int/float.
|
|
112
|
+
try:
|
|
113
|
+
if "." in normalized:
|
|
114
|
+
return sign * float(normalized)
|
|
115
|
+
return sign * int(normalized)
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
raise Words2NumError(self.errmsg_unparseable % normalized)
|
|
119
|
+
|
|
120
|
+
def _cardinal(self):
|
|
121
|
+
if self._cardinal_table is None:
|
|
122
|
+
self._cardinal_table = self._build_table("cardinal")
|
|
123
|
+
return self._cardinal_table
|
|
124
|
+
|
|
125
|
+
def _ordinal(self):
|
|
126
|
+
if self._ordinal_table is None:
|
|
127
|
+
self._ordinal_table = self._build_table("ordinal")
|
|
128
|
+
return self._ordinal_table
|
|
129
|
+
|
|
130
|
+
def _build_table(self, kind):
|
|
131
|
+
"""Use num2words2 to materialize a {normalized_words: number} table.
|
|
132
|
+
|
|
133
|
+
Built lazily on first access. Range is bounded by ``LOOKUP_RANGE``;
|
|
134
|
+
outside this range the generic backend will raise unless the
|
|
135
|
+
subclass overrides :meth:`to_cardinal`.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
from num2words2 import num2words
|
|
139
|
+
except ImportError as exc:
|
|
140
|
+
raise Words2NumError(
|
|
141
|
+
"num2words2 is required for the generic backend"
|
|
142
|
+
) from exc
|
|
143
|
+
if not self.LANG:
|
|
144
|
+
raise Words2NumError(self.errmsg_lang % self.LANG)
|
|
145
|
+
lo, hi = self.LOOKUP_RANGE
|
|
146
|
+
table = {}
|
|
147
|
+
for n in range(lo, hi):
|
|
148
|
+
try:
|
|
149
|
+
words = num2words(n, lang=self.LANG, to=kind)
|
|
150
|
+
except (NotImplementedError, Exception): # pragma: no cover
|
|
151
|
+
continue
|
|
152
|
+
key = self._normalize(words)
|
|
153
|
+
# First-write wins so canonical short form takes precedence.
|
|
154
|
+
table.setdefault(key, n)
|
|
155
|
+
return table
|
words2num2/compat.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2026, Jean-Louis Queguiner. All Rights Reserved.
|
|
3
|
+
"""Sentence-level words-to-number converter.
|
|
4
|
+
|
|
5
|
+
Walks a sentence token by token. At each position, tries to match the
|
|
6
|
+
longest run of consecutive number-bearing tokens that the per-language
|
|
7
|
+
converter accepts; replaces that run with the numeric value.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import unicode_literals
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SentenceConverter(object):
|
|
15
|
+
# Tokens we are willing to *include* inside a number run even though
|
|
16
|
+
# they are not numbers themselves. Currency words and units are not
|
|
17
|
+
# included — they break the run.
|
|
18
|
+
INCLUDABLE = {
|
|
19
|
+
"en": {"and", "point", "dot", "minus", "negative", "a", "an"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def convert(self, sentence, lang="en", to="cardinal", **kwargs):
|
|
23
|
+
from .. import _resolve_lang, CONVERTER_CLASSES
|
|
24
|
+
|
|
25
|
+
resolved = _resolve_lang(lang)
|
|
26
|
+
converter = CONVERTER_CLASSES[resolved]
|
|
27
|
+
includable = self.INCLUDABLE.get(resolved, set())
|
|
28
|
+
|
|
29
|
+
# Tokenize keeping whitespace + punctuation positions.
|
|
30
|
+
parts = re.findall(r"\S+|\s+", sentence)
|
|
31
|
+
out = []
|
|
32
|
+
i = 0
|
|
33
|
+
n = len(parts)
|
|
34
|
+
while i < n:
|
|
35
|
+
piece = parts[i]
|
|
36
|
+
if piece.isspace():
|
|
37
|
+
out.append(piece)
|
|
38
|
+
i += 1
|
|
39
|
+
continue
|
|
40
|
+
# A run must START with a real number word — connector words
|
|
41
|
+
# like "and" / "point" can only appear *inside* a run.
|
|
42
|
+
head = re.sub(r"[\.,;:!\?\"']+$", "", piece).lower()
|
|
43
|
+
if not self._starts_run(head, converter):
|
|
44
|
+
out.append(piece)
|
|
45
|
+
i += 1
|
|
46
|
+
continue
|
|
47
|
+
# Try to grow a number run starting at i.
|
|
48
|
+
best_value = None
|
|
49
|
+
best_end = i
|
|
50
|
+
j = i
|
|
51
|
+
while j < n:
|
|
52
|
+
tok = parts[j]
|
|
53
|
+
if tok.isspace():
|
|
54
|
+
j += 1
|
|
55
|
+
continue
|
|
56
|
+
clean = re.sub(r"[\.,;:!\?\"']+$", "", tok).lower()
|
|
57
|
+
if not self._is_candidate(clean, converter, includable):
|
|
58
|
+
break
|
|
59
|
+
# Try to parse [i..j].
|
|
60
|
+
run = "".join(parts[i:j + 1]).strip()
|
|
61
|
+
stripped = re.sub(r"[\.,;:!\?\"']+$", "", run)
|
|
62
|
+
try:
|
|
63
|
+
value = getattr(
|
|
64
|
+
converter, "to_{}".format(to)
|
|
65
|
+
)(stripped, **kwargs)
|
|
66
|
+
best_value = value
|
|
67
|
+
best_end = j
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
# A token ending in terminal punctuation closes the run.
|
|
71
|
+
if re.search(r"[\.,;:!\?]$", tok):
|
|
72
|
+
break
|
|
73
|
+
j += 1
|
|
74
|
+
if best_value is not None:
|
|
75
|
+
# Preserve trailing punctuation that we stripped during parse.
|
|
76
|
+
run = "".join(parts[i:best_end + 1])
|
|
77
|
+
m = re.search(r"[\.,;:!\?\"']+$", run)
|
|
78
|
+
trailing = m.group() if m else ""
|
|
79
|
+
out.append(str(best_value) + trailing)
|
|
80
|
+
i = best_end + 1
|
|
81
|
+
else:
|
|
82
|
+
out.append(piece)
|
|
83
|
+
i += 1
|
|
84
|
+
return "".join(out)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _is_candidate(token, converter, includable):
|
|
88
|
+
"""Cheap pre-filter: is this token plausibly part of a number run?"""
|
|
89
|
+
if not token:
|
|
90
|
+
return False
|
|
91
|
+
if token in includable:
|
|
92
|
+
return True
|
|
93
|
+
# Handle hyphenated forms like "twenty-one".
|
|
94
|
+
for sub in token.replace("-", " ").split():
|
|
95
|
+
if SentenceConverter._token_is_number_word(sub, converter):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _token_is_number_word(token, converter):
|
|
101
|
+
# Try a single-token cardinal parse — cheap and language-agnostic.
|
|
102
|
+
try:
|
|
103
|
+
converter.to_cardinal(token)
|
|
104
|
+
return True
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _starts_run(token, converter):
|
|
110
|
+
"""A run must start with a real number word — not "and"/"point"."""
|
|
111
|
+
if not token:
|
|
112
|
+
return False
|
|
113
|
+
for sub in token.replace("-", " ").split():
|
|
114
|
+
if SentenceConverter._token_is_number_word(sub, converter):
|
|
115
|
+
return True
|
|
116
|
+
return False
|
words2num2/lang_AF.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) 2026, Jean-Louis Queguiner. All Rights Reserved.
|
|
3
|
+
"""AF words-to-number parser.
|
|
4
|
+
|
|
5
|
+
Default implementation: generic reverse-lookup driven by num2words2.
|
|
6
|
+
For grammar-precise parsing (out-of-range values, ordinals, conjunctions),
|
|
7
|
+
override :meth:`to_cardinal` and friends.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import unicode_literals
|
|
10
|
+
|
|
11
|
+
from .base import Words2Num_Base
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Words2Num_AF(Words2Num_Base):
|
|
15
|
+
LANG = "af"
|