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.
Files changed (133) hide show
  1. words2num2/__init__.py +345 -0
  2. words2num2/__main__.py +39 -0
  3. words2num2/_version.py +3 -0
  4. words2num2/base.py +155 -0
  5. words2num2/compat.py +9 -0
  6. words2num2/converters/__init__.py +2 -0
  7. words2num2/converters/sentence.py +116 -0
  8. words2num2/lang_AF.py +15 -0
  9. words2num2/lang_AM.py +15 -0
  10. words2num2/lang_AR.py +15 -0
  11. words2num2/lang_AS.py +15 -0
  12. words2num2/lang_AZ.py +15 -0
  13. words2num2/lang_BA.py +15 -0
  14. words2num2/lang_BE.py +15 -0
  15. words2num2/lang_BG.py +15 -0
  16. words2num2/lang_BN.py +15 -0
  17. words2num2/lang_BO.py +15 -0
  18. words2num2/lang_BR.py +15 -0
  19. words2num2/lang_BS.py +15 -0
  20. words2num2/lang_CA.py +15 -0
  21. words2num2/lang_CE.py +15 -0
  22. words2num2/lang_CS.py +15 -0
  23. words2num2/lang_CY.py +15 -0
  24. words2num2/lang_DA.py +15 -0
  25. words2num2/lang_DE.py +15 -0
  26. words2num2/lang_EL.py +15 -0
  27. words2num2/lang_EN.py +242 -0
  28. words2num2/lang_EN_IN.py +15 -0
  29. words2num2/lang_EN_NG.py +15 -0
  30. words2num2/lang_EO.py +15 -0
  31. words2num2/lang_ES.py +15 -0
  32. words2num2/lang_ES_CO.py +15 -0
  33. words2num2/lang_ES_CR.py +15 -0
  34. words2num2/lang_ES_GT.py +15 -0
  35. words2num2/lang_ES_NI.py +15 -0
  36. words2num2/lang_ES_VE.py +15 -0
  37. words2num2/lang_ET.py +15 -0
  38. words2num2/lang_EU.py +15 -0
  39. words2num2/lang_FA.py +15 -0
  40. words2num2/lang_FI.py +15 -0
  41. words2num2/lang_FO.py +15 -0
  42. words2num2/lang_FR.py +15 -0
  43. words2num2/lang_FR_BE.py +15 -0
  44. words2num2/lang_FR_CH.py +15 -0
  45. words2num2/lang_FR_DZ.py +15 -0
  46. words2num2/lang_GL.py +15 -0
  47. words2num2/lang_GU.py +15 -0
  48. words2num2/lang_HA.py +15 -0
  49. words2num2/lang_HAW.py +15 -0
  50. words2num2/lang_HE.py +15 -0
  51. words2num2/lang_HI.py +15 -0
  52. words2num2/lang_HR.py +15 -0
  53. words2num2/lang_HT.py +15 -0
  54. words2num2/lang_HU.py +15 -0
  55. words2num2/lang_HY.py +15 -0
  56. words2num2/lang_ID.py +15 -0
  57. words2num2/lang_IS.py +15 -0
  58. words2num2/lang_IT.py +15 -0
  59. words2num2/lang_JA.py +15 -0
  60. words2num2/lang_JW.py +15 -0
  61. words2num2/lang_KA.py +15 -0
  62. words2num2/lang_KK.py +15 -0
  63. words2num2/lang_KM.py +15 -0
  64. words2num2/lang_KN.py +15 -0
  65. words2num2/lang_KO.py +15 -0
  66. words2num2/lang_KZ.py +15 -0
  67. words2num2/lang_LA.py +15 -0
  68. words2num2/lang_LB.py +15 -0
  69. words2num2/lang_LN.py +15 -0
  70. words2num2/lang_LO.py +15 -0
  71. words2num2/lang_LT.py +15 -0
  72. words2num2/lang_LV.py +15 -0
  73. words2num2/lang_MG.py +15 -0
  74. words2num2/lang_MI.py +15 -0
  75. words2num2/lang_MK.py +15 -0
  76. words2num2/lang_ML.py +15 -0
  77. words2num2/lang_MN.py +15 -0
  78. words2num2/lang_MR.py +15 -0
  79. words2num2/lang_MS.py +15 -0
  80. words2num2/lang_MT.py +15 -0
  81. words2num2/lang_MY.py +15 -0
  82. words2num2/lang_NE.py +15 -0
  83. words2num2/lang_NL.py +15 -0
  84. words2num2/lang_NN.py +15 -0
  85. words2num2/lang_NO.py +15 -0
  86. words2num2/lang_OC.py +15 -0
  87. words2num2/lang_PA.py +15 -0
  88. words2num2/lang_PL.py +15 -0
  89. words2num2/lang_PS.py +15 -0
  90. words2num2/lang_PT.py +15 -0
  91. words2num2/lang_PT_BR.py +15 -0
  92. words2num2/lang_RO.py +15 -0
  93. words2num2/lang_RU.py +15 -0
  94. words2num2/lang_SA.py +15 -0
  95. words2num2/lang_SD.py +15 -0
  96. words2num2/lang_SI.py +15 -0
  97. words2num2/lang_SK.py +15 -0
  98. words2num2/lang_SL.py +15 -0
  99. words2num2/lang_SN.py +15 -0
  100. words2num2/lang_SO.py +15 -0
  101. words2num2/lang_SQ.py +15 -0
  102. words2num2/lang_SR.py +15 -0
  103. words2num2/lang_SU.py +15 -0
  104. words2num2/lang_SV.py +15 -0
  105. words2num2/lang_SW.py +15 -0
  106. words2num2/lang_TA.py +15 -0
  107. words2num2/lang_TE.py +15 -0
  108. words2num2/lang_TET.py +15 -0
  109. words2num2/lang_TG.py +15 -0
  110. words2num2/lang_TH.py +15 -0
  111. words2num2/lang_TK.py +15 -0
  112. words2num2/lang_TL.py +15 -0
  113. words2num2/lang_TR.py +15 -0
  114. words2num2/lang_TT.py +15 -0
  115. words2num2/lang_UK.py +15 -0
  116. words2num2/lang_UR.py +15 -0
  117. words2num2/lang_UZ.py +15 -0
  118. words2num2/lang_VI.py +15 -0
  119. words2num2/lang_WO.py +15 -0
  120. words2num2/lang_YI.py +15 -0
  121. words2num2/lang_YO.py +15 -0
  122. words2num2/lang_ZH.py +15 -0
  123. words2num2/lang_ZH_CN.py +15 -0
  124. words2num2/lang_ZH_HK.py +15 -0
  125. words2num2/lang_ZH_TW.py +15 -0
  126. words2num2/utils.py +23 -0
  127. words2num2-0.1.0.data/scripts/words2num2 +28 -0
  128. words2num2-0.1.0.dist-info/METADATA +168 -0
  129. words2num2-0.1.0.dist-info/RECORD +133 -0
  130. words2num2-0.1.0.dist-info/WHEEL +5 -0
  131. words2num2-0.1.0.dist-info/entry_points.txt +2 -0
  132. words2num2-0.1.0.dist-info/licenses/COPYING +145 -0
  133. 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
@@ -0,0 +1,3 @@
1
+ # Auto-generated version file
2
+ __version__ = "0.1.0"
3
+ __version_tuple__ = (0, 1, 0)
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,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Compatibility shims (kept for parity with num2words2)."""
3
+ from __future__ import unicode_literals
4
+
5
+
6
+ def to_s(text):
7
+ if isinstance(text, bytes):
8
+ return text.decode("utf-8")
9
+ return str(text)
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Sentence-level converters."""
@@ -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"