numbr 2.1.1__tar.gz → 3.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {numbr-2.1.1 → numbr-3.2.3}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
  2. {numbr-2.1.1 → numbr-3.2.3}/.github/workflows/ci.yml +2 -2
  3. {numbr-2.1.1 → numbr-3.2.3}/.github/workflows/publish.yml +31 -0
  4. numbr-3.2.3/CHANGELOG.md +5 -0
  5. numbr-3.2.3/PKG-INFO +430 -0
  6. numbr-3.2.3/README.md +402 -0
  7. {numbr-2.1.1 → numbr-3.2.3}/RELEASING.md +12 -11
  8. numbr-3.2.3/assets/png/numbr-lockup-endorsed-1118x344.png +0 -0
  9. {numbr-2.1.1 → numbr-3.2.3}/pyproject.toml +5 -11
  10. {numbr-2.1.1 → numbr-3.2.3}/src/numbr/__init__.py +40 -5
  11. numbr-3.2.3/src/numbr/engine.py +2072 -0
  12. numbr-3.2.3/tests/test_decimals.py +163 -0
  13. numbr-3.2.3/tests/test_diagnostics_and_batch.py +206 -0
  14. numbr-3.2.3/tests/test_notation_and_normalization.py +185 -0
  15. numbr-3.2.3/tests/test_structured_extraction.py +159 -0
  16. numbr-2.1.1/CHANGELOG.md +0 -44
  17. numbr-2.1.1/PKG-INFO +0 -175
  18. numbr-2.1.1/README.md +0 -141
  19. numbr-2.1.1/assets/png/numbr-primary-symbol-320x320.png +0 -0
  20. numbr-2.1.1/assets/png/numbr-primary-wordmark-1200x360.png +0 -0
  21. numbr-2.1.1/assets/png/numbr-primary-wordmark-python-no-brandmark-654x169.png +0 -0
  22. numbr-2.1.1/src/numbr/engine.py +0 -1211
  23. {numbr-2.1.1 → numbr-3.2.3}/.gitattributes +0 -0
  24. {numbr-2.1.1 → numbr-3.2.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {numbr-2.1.1 → numbr-3.2.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  26. {numbr-2.1.1 → numbr-3.2.3}/.github/pull_request_template.md +0 -0
  27. {numbr-2.1.1 → numbr-3.2.3}/.github/release.yml +0 -0
  28. {numbr-2.1.1 → numbr-3.2.3}/.github/workflows/codeql.yml +0 -0
  29. {numbr-2.1.1 → numbr-3.2.3}/.gitignore +0 -0
  30. {numbr-2.1.1 → numbr-3.2.3}/CODE_OF_CONDUCT.md +0 -0
  31. {numbr-2.1.1 → numbr-3.2.3}/CONTRIBUTING.md +0 -0
  32. {numbr-2.1.1 → numbr-3.2.3}/LICENSE +0 -0
  33. {numbr-2.1.1 → numbr-3.2.3}/SECURITY.md +0 -0
  34. {numbr-2.1.1 → numbr-3.2.3}/assets/png/organization/author-lock-up.png +0 -0
  35. {numbr-2.1.1 → numbr-3.2.3}/assets/png/organization/author-mark.png +0 -0
  36. {numbr-2.1.1 → numbr-3.2.3}/assets/png/organization/author-wordmark.png +0 -0
  37. {numbr-2.1.1 → numbr-3.2.3}/tests/test_cardinals.py +0 -0
  38. {numbr-2.1.1 → numbr-3.2.3}/tests/test_extraction.py +0 -0
  39. {numbr-2.1.1 → numbr-3.2.3}/tests/test_ordinals.py +0 -0
  40. {numbr-2.1.1 → numbr-3.2.3}/tests/test_public_api.py +0 -0
  41. {numbr-2.1.1 → numbr-3.2.3}/tests/test_roman.py +0 -0
@@ -10,7 +10,7 @@ body:
10
10
  id: version
11
11
  attributes:
12
12
  label: numbr version
13
- placeholder: 2.1.1
13
+ placeholder: 3.2.3
14
14
  validations:
15
15
  required: true
16
16
  - type: input
@@ -16,7 +16,7 @@ jobs:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
19
+ python-version: ["3.13"]
20
20
  steps:
21
21
  - name: Check out repository
22
22
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -55,7 +55,7 @@ jobs:
55
55
  run: |
56
56
  python -m pip install --force-reinstall dist/*.whl
57
57
  cd /tmp
58
- python -I -c "import numbr; assert numbr.Cast('Ⅳ', 'Ordinal Word') == 'fourth'"
58
+ python -I -c "import numbr; assert numbr.__version__ == '3.2.3'; assert numbr.Cast('Ⅳ', 'Ordinal Word') == 'fourth'"
59
59
  - name: Upload distributions
60
60
  uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
61
61
  with:
@@ -8,8 +8,39 @@ permissions:
8
8
  contents: read
9
9
 
10
10
  jobs:
11
+ check-version:
12
+ name: Check whether version is already published
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ exists: ${{ steps.pypi.outputs.exists }}
16
+ steps:
17
+ - name: Check PyPI for the tagged version
18
+ id: pypi
19
+ shell: bash
20
+ env:
21
+ TAG_NAME: ${{ github.event.release.tag_name }}
22
+ run: |
23
+ version="${TAG_NAME#v}"
24
+ status=$(curl --silent --show-error --output /dev/null --write-out "%{http_code}" \
25
+ "https://pypi.org/pypi/numbr/${version}/json")
26
+ case "$status" in
27
+ 200)
28
+ echo "exists=true" >> "$GITHUB_OUTPUT"
29
+ echo "numbr ${version} is already published; skipping package publication."
30
+ ;;
31
+ 404)
32
+ echo "exists=false" >> "$GITHUB_OUTPUT"
33
+ ;;
34
+ *)
35
+ echo "Unexpected response from PyPI: HTTP ${status}" >&2
36
+ exit 1
37
+ ;;
38
+ esac
39
+
11
40
  build:
12
41
  name: Build release distributions
42
+ needs: check-version
43
+ if: needs.check-version.outputs.exists != 'true'
13
44
  runs-on: ubuntu-latest
14
45
  steps:
15
46
  - name: Check out released tag
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ See the [numbr changelog](https://doydl.studio/pages/open-source/numbr/changelog/) for the latest release history.
4
+
5
+ All notable changes follow Keep a Changelog and Python packaging version conventions.
numbr-3.2.3/PKG-INFO ADDED
@@ -0,0 +1,430 @@
1
+ Metadata-Version: 2.5
2
+ Name: numbr
3
+ Version: 3.2.3
4
+ Summary: Parse, diagnose, batch-convert, extract, and normalize English numbers, exact decimals, scientific and accounting notation, and Roman numerals.
5
+ Project-URL: Homepage, https://github.com/doydl-technologies/numbr
6
+ Project-URL: Documentation, https://github.com/doydl-technologies/numbr#readme
7
+ Project-URL: Issues, https://github.com/doydl-technologies/numbr/issues
8
+ Project-URL: Changelog, https://github.com/doydl-technologies/numbr/blob/main/CHANGELOG.md
9
+ Author: DOYDL Technologies
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: accounting,batch-conversion,decimals,diagnostics,nlp,normalization,numbers,ordinals,parser,roman-numerals,scientific-notation
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: <3.14,>=3.13
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == 'dev'
24
+ Requires-Dist: pytest>=7.4; extra == 'dev'
25
+ Requires-Dist: ruff>=0.9; extra == 'dev'
26
+ Requires-Dist: twine>=5.1; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ <p align="center">
30
+ <img src="https://raw.githubusercontent.com/doydl-technologies/numbr/main/assets/png/numbr-lockup-endorsed-1118x344.png" alt="numbr" width="700">
31
+ </p>
32
+
33
+ <p align="center">
34
+ Parses, extracts, normalizes, and converts English number words, integers, exact decimals, scientific and accounting notation, ordinal digits, and Roman numerals.
35
+ </p>
36
+
37
+
38
+ <p align="center">
39
+ <a href="https://pypi.org/project/numbr/"><img src="https://img.shields.io/pypi/v/numbr" alt="PyPI version"></a>
40
+ <a href="https://pypi.org/project/numbr/"><img src="https://img.shields.io/pypi/pyversions/numbr" alt="Supported Python versions"></a>
41
+ <a href="https://pepy.tech/project/numbr"><img src="https://static.pepy.tech/badge/numbr" alt="Downloads"></a>
42
+ <a href="https://doydl.com"><img src="https://img.shields.io/badge/Powered%20by-DOYDL%20Technologies-blue" alt="Powered by DOYDL Technologies"></a>
43
+ </p>
44
+
45
+ ## Table of contents
46
+
47
+ - [What numbr does](#what-numbr-does)
48
+ - [Installation](#installation)
49
+ - [Quick start](#quick-start)
50
+ - [Choosing the right API](#choosing-the-right-api)
51
+ - [Exact decimal values](#exact-decimal-values)
52
+ - [Representations](#representations)
53
+ - [Public API](#public-api)
54
+ - [General conversion](#general-conversion)
55
+ - [Scientific and accounting notation](#scientific-and-accounting-notation)
56
+ - [Structured extraction](#structured-extraction)
57
+ - [Whole-text normalization](#whole-text-normalization)
58
+ - [Rich parsing diagnostics](#rich-parsing-diagnostics)
59
+ - [Batch conversion](#batch-conversion)
60
+ - [Parsing and formatting](#parsing-and-formatting)
61
+ - [Development](#development)
62
+ - [License](#license)
63
+
64
+ ## What numbr does
65
+
66
+ `numbr` gives Python applications one consistent way to understand numbers
67
+ written in different forms. It can recognize that `"forty-two"`, `"42"`, and
68
+ `"XLII"` represent the same value, then convert them to the representation an
69
+ application needs.
70
+
71
+ The package is useful when numeric data arrives as human-written text rather
72
+ than a clean integer or decimal. Common examples include form responses,
73
+ documents, transcripts, scraped text, NLP pipelines, and imported business
74
+ data.
75
+
76
+ With `numbr`, an application can:
77
+
78
+ - identify the representation of one numeric value;
79
+ - convert between digits, English number words, ordinals, and Roman numerals;
80
+ - parse exact decimals, scientific notation, and accounting notation;
81
+ - find every numeric expression in a larger passage of text;
82
+ - replace numeric expressions while leaving the surrounding text intact;
83
+ - receive structured explanations for invalid input; and
84
+ - apply the same conversion or diagnostic operation to an iterable of values.
85
+
86
+ `numbr` is deterministic and rule-based. It does not use a language model,
87
+ network service, locale database, or heuristic confidence score. The same
88
+ supported input produces the same result on every platform.
89
+
90
+ ## Installation
91
+
92
+ Install the latest release from PyPI:
93
+
94
+ ```bash
95
+ python -m pip install numbr
96
+ ```
97
+
98
+ Or install the current development version from GitHub:
99
+
100
+ ```bash
101
+ python -m pip install 'numbr @ git+ssh://git@github.com/doydl-technologies/numbr.git'
102
+ ```
103
+
104
+ Python 3.13 is required. The package has no runtime dependencies.
105
+
106
+ ## Quick start
107
+
108
+ Import `numbr`, then choose an operation based on whether the input is one
109
+ value, a passage of text, or a collection of values.
110
+
111
+ ```python
112
+ import numbr
113
+
114
+ # Identify how one value is written.
115
+ numbr.Type("forty-second")
116
+ # "Ordinal Word"
117
+
118
+ # Convert one value to a requested representation.
119
+ numbr.Cast("forty-second", "Cardinal Number")
120
+ # 42
121
+
122
+ numbr.Cast("1.050", "Cardinal Word")
123
+ # "one point zero five zero"
124
+
125
+ # Collect normalized values from a larger passage.
126
+ numbr.extractNumericValue("She placed 2nd with 1.05 points")
127
+ # [2, Decimal("1.05")]
128
+
129
+ # Collect values together with their original text and source positions.
130
+ numbr.extractNumericEntities("She placed 2nd with 1.05 points")
131
+ # [NumericMatch(text='2nd', value=2, representation='Ordinal Number', ...),
132
+ # NumericMatch(text='1.05', value=Decimal('1.05'), representation='Decimal Number', ...)]
133
+
134
+ # Rewrite recognized expressions without changing the surrounding sentence.
135
+ numbr.normalizeNumericText("She placed forty-second with 1.20e3 points")
136
+ # "She placed 42nd with 1200 points"
137
+
138
+ # Apply one conversion to several values.
139
+ numbr.batchCast(["one", "2", "III"], "Cardinal Number")
140
+ # [1, 2, 3]
141
+ ```
142
+
143
+ The `Decimal(...)` text shown above is Python's representation of the returned
144
+ value; the quick-start example does not need to import `Decimal`.
145
+
146
+ ## Choosing the right API
147
+
148
+ | Goal | API | Result |
149
+ |---|---|---|
150
+ | Identify how one value is written | `Type(value)` | A representation name such as `"Ordinal Word"`, or `None` |
151
+ | Convert one value | `Cast(value, target)` | The value in the requested representation |
152
+ | Collect only values from text | `extractNumericValue(text)` | A list of normalized `int` or `Decimal` values |
153
+ | Collect values and source details | `extractNumericEntities(text)` | A list of `NumericMatch` records with text, type, value, and position |
154
+ | Rewrite numbers inside text | `normalizeNumericText(text)` | A new string with recognized expressions normalized |
155
+ | Explain why parsing failed | `diagnoseNumeric(value)` | A `ParseResult` containing either a value or structured diagnostics |
156
+ | Convert many inputs | `batchCast(values, target)` | A list of converted values using the selected error policy |
157
+ | Diagnose many inputs | `batchDiagnose(values, target)` | One index-stable `ParseResult` per input |
158
+
159
+ Conversion operates on one numeric value. Extraction searches a larger string
160
+ and returns the numbers it contains. Normalization also searches a larger
161
+ string, but returns rewritten text instead of match records.
162
+
163
+ ## Exact decimal values
164
+
165
+ Decimal parsing returns [`decimal.Decimal`](https://docs.python.org/3/library/decimal.html#decimal.Decimal)
166
+ instead of `float`. `Decimal` is part of Python's standard library; it is not an
167
+ additional dependency. This avoids binary floating-point rounding and preserves
168
+ meaningful fractional zeros such as the final zero in `"1.050"`.
169
+
170
+ No `Decimal` import is required when the input is a string:
171
+
172
+ ```python
173
+ import numbr
174
+
175
+ value = numbr.Cast("1.050", "Decimal Number")
176
+
177
+ value
178
+ # Decimal("1.050")
179
+
180
+ numbr.Cast(value, "Cardinal Word")
181
+ # "one point zero five zero"
182
+ ```
183
+
184
+ Import `Decimal` only when application code needs to construct a decimal value
185
+ directly or perform decimal arithmetic:
186
+
187
+ ```python
188
+ from decimal import Decimal
189
+
190
+ import numbr
191
+
192
+ numbr.decimalToAccounting(Decimal("-1234.50"), currency="$")
193
+ # "($1,234.50)"
194
+ ```
195
+
196
+ ## Representations
197
+
198
+ | Representation | Meaning | Examples | `Type` result |
199
+ |---|---|---|---|
200
+ | Cardinal number | A whole-number quantity written with digits | `42`, `"-42"` | `"Cardinal Number"` |
201
+ | Decimal number | A fixed-point value with a fractional part | `"1.05"`, `Decimal("1.050")` | `"Decimal Number"` |
202
+ | Scientific number | A coefficient multiplied by a power of ten | `"1.20e3"`, `"-4E-2"` | `"Scientific Number"` |
203
+ | Accounting number | A formatted business value whose parentheses indicate a negative amount | `"(1,234.50)"`, `"($50.00)"` | `"Accounting Number"` |
204
+ | Cardinal word | A quantity written with English words | `"forty-two"` | `"Cardinal Word"` |
205
+ | Ordinal number | A position or rank written with digits and a suffix | `"42nd"` | `"Ordinal Number"` |
206
+ | Ordinal word | A position or rank written with English words | `"forty-second"` | `"Ordinal Word"` |
207
+ | Roman numeral | A value written with Roman symbols | `"XLII"`, `"Ⅳ"` | `"Roman Numeral"` |
208
+
209
+ Decimal phrases such as `"one point zero five"` are cardinal-word forms. Digits
210
+ following `point` are interpreted individually so precision and trailing zeros
211
+ are preserved.
212
+
213
+ English-word parsing is intended for numeric expressions, not unrestricted
214
+ natural-language interpretation. Surrounding prose should be passed to an
215
+ extraction or normalization API rather than directly to `Type` or `Cast`.
216
+
217
+ `Cast` accepts the display names and common aliases such as `int`, `decimal`,
218
+ `scientific`, `accounting`, `cardword`, `ordnum`, and `roman`.
219
+
220
+ ```python
221
+ numbr.Type("twenty-first")
222
+ # "Ordinal Word"
223
+
224
+ numbr.Cast("twenty-first", "Cardinal Number")
225
+ # 21
226
+
227
+ numbr.Cast(1994, "Roman Numeral")
228
+ # "MCMXCIV"
229
+
230
+ numbr.Type("-0.004")
231
+ # "Decimal Number"
232
+
233
+ numbr.Cast("-0.004", "Cardinal Word")
234
+ # "negative zero point zero zero four"
235
+ ```
236
+
237
+ Roman output is limited to canonical values from 1 through 3999. English number
238
+ conversion supports values whose integer component has an absolute value less
239
+ than `10**24`.
240
+
241
+ ## Public API
242
+
243
+ ### General conversion
244
+
245
+ - `Type(value)` detects the representation or returns `None`.
246
+ - `Cast(value, target, *, as_str=False)` converts between representations and
247
+ raises `ValueError` when the input or requested conversion is invalid.
248
+
249
+ Decimal-number conversions return `decimal.Decimal` to avoid binary floating-point
250
+ rounding and to preserve explicitly supplied fractional zeros. Non-integral
251
+ decimals cannot be cast to integer-only cardinal, ordinal, or Roman forms.
252
+
253
+ ### Scientific and accounting notation
254
+
255
+ Scientific notation accepts signed coefficients and exponents, including forms
256
+ such as `.5e1` and `5.e2`. Accounting notation uses parentheses for negative
257
+ values and accepts a supported currency token either immediately outside or
258
+ inside the parentheses.
259
+
260
+ ```python
261
+ numbr.scientificToDecimal("1.20e3")
262
+ # Decimal("1.20E+3")
263
+
264
+ numbr.accountingToDecimal("($1,234.50)")
265
+ # Decimal("-1234.50")
266
+
267
+ numbr.decimalToScientific(Decimal("1230.00"))
268
+ # "1.23e+3"
269
+
270
+ numbr.decimalToAccounting(Decimal("-1234.50"), currency="$")
271
+ # "($1,234.50)"
272
+ ```
273
+
274
+ Both representations participate in `Type`, `Cast`, value extraction, and
275
+ structured extraction.
276
+
277
+ ### Structured extraction
278
+
279
+ `extractNumericEntities(text)` returns an immutable `NumericMatch` for every
280
+ recognized expression in source order. Each match provides the original `text`,
281
+ normalized `value`, representation name, `start` and `end` offsets, `span`, and
282
+ `is_ordinal` and `is_negative` flags.
283
+
284
+ ```python
285
+ matches = numbr.extractNumericEntities("She placed forty-second with -0.50 points in round XLII.")
286
+
287
+ matches[0].text, matches[0].value, matches[0].span
288
+ # ("forty-second", 42, (11, 23))
289
+
290
+ matches[1].value
291
+ # Decimal("-0.50")
292
+
293
+ matches[2].representation
294
+ # "Roman Numeral"
295
+ ```
296
+
297
+ Canonical Roman numerals are included by default. Single-letter ASCII forms
298
+ such as `I`, `V`, and `X` are excluded because they are ambiguous in prose; pass
299
+ `include_single_roman=True` to include them. Pass `include_roman=False` to omit
300
+ Roman numerals entirely.
301
+
302
+ ### Whole-text normalization
303
+
304
+ `normalizeNumericText(text, style="digits")` replaces every recognized entity
305
+ while preserving all surrounding characters. The default style renders
306
+ cardinals as digits and retains ordinal suffixes. Use `style="words"` to spell
307
+ values in English while retaining ordinal meaning.
308
+
309
+ ```python
310
+ text = "She placed forty-second with 1.20e3 points and lost ($1,234.50)."
311
+
312
+ numbr.normalizeNumericText(text)
313
+ # "She placed 42nd with 1200 points and lost -1234.50."
314
+
315
+ numbr.normalizeNumericText("Ranked 2nd in round Ⅳ", style="words")
316
+ # "Ranked second in round four"
317
+ ```
318
+
319
+ The Roman-numeral options are the same as those on `extractNumericEntities`.
320
+ Values outside the supported English-word range remain unchanged when using
321
+ the `words` style.
322
+
323
+ ### Rich parsing diagnostics
324
+
325
+ `diagnoseNumeric(value, target=None)` returns an immutable `ParseResult`
326
+ instead of raising for invalid input. Successful results contain the normalized
327
+ or converted value and source and target representations. Failed results contain
328
+ one or more `ParseDiagnostic` objects with a stable code, message, source span,
329
+ severity, and suggested correction.
330
+
331
+ ```python
332
+ result = numbr.diagnoseNumeric("22st")
333
+
334
+ result.ok
335
+ # False
336
+
337
+ result.error.code, result.error.span
338
+ # ("invalid_ordinal_suffix", (0, 4))
339
+
340
+ result.error.suggestion
341
+ # "Use '22nd'."
342
+ ```
343
+
344
+ Diagnostic codes distinguish empty or unsupported inputs, malformed decimal,
345
+ scientific, accounting, ordinal, grouping, word, and Roman forms, unknown
346
+ targets, and unsupported conversions. Call `result.unwrap()` when exception
347
+ semantics are preferred after inspection.
348
+
349
+ ### Batch conversion
350
+
351
+ `batchCast(values, target)` applies the same conversion rules as `Cast` to any
352
+ iterable. The `errors` policy controls failed items:
353
+
354
+ - `"raise"` raises `BatchConversionError` with the failing index and diagnostic
355
+ result.
356
+ - `"coerce"` retains the input shape and places `None` at failed positions.
357
+ - `"ignore"` omits failed items.
358
+
359
+ ```python
360
+ numbr.batchCast(["one", "invalid", "III"], "Cardinal Number", errors="coerce")
361
+ # [1, None, 3]
362
+
363
+ results = numbr.batchDiagnose(["1e3", "bad", "(2.00)"], "Decimal Number")
364
+ [result.ok for result in results]
365
+ # [True, False, True]
366
+ ```
367
+
368
+ Use `batchDiagnose` when every input needs an index-stable structured result,
369
+ regardless of success or failure.
370
+
371
+ ### Parsing and formatting
372
+
373
+ - `wordsToInt`
374
+ - `wordsToDecimal`
375
+ - `ordinalWordsToInt`
376
+ - `stringToInt`
377
+ - `stringToDecimal`
378
+ - `scientificToDecimal`
379
+ - `accountingToDecimal`
380
+ - `intToWords`
381
+ - `decimalToWords`
382
+ - `decimalToScientific`
383
+ - `decimalToAccounting`
384
+ - `intToOrdinalWords`
385
+ - `romanToInt`
386
+ - `romanToWords`
387
+ - `intToRoman`
388
+ - `ordinalSuffix`
389
+ - `stripOrdinalSuffix`
390
+ - `extractNumericValue`
391
+ - `extractNumericEntities`
392
+ - `normalizeNumericText`
393
+ - `diagnoseNumeric`
394
+ - `batchDiagnose`
395
+ - `batchCast`
396
+ - `insertSep`
397
+ - `formatDecimal`
398
+
399
+ The direct cross-representation helpers from earlier releases remain available,
400
+ including `cardinalWordToOrdinalNum`, `ordinalNumToCardinalWord`, and related
401
+ functions.
402
+
403
+ Lower-level parsing functions return `None` for invalid input. Decimal conversion
404
+ spells digits after the point individually so that zeros are preserved:
405
+
406
+ ```python
407
+ numbr.intToWords("1.05")
408
+ # "one point zero five"
409
+ ```
410
+
411
+ ## Development
412
+
413
+ ```bash
414
+ git clone git@github.com:doydl-technologies/numbr.git
415
+ cd numbr
416
+ python -m venv .venv
417
+ source .venv/bin/activate # .venv/Scripts/activate on MSYS2/Windows
418
+ python -m pip install -e '.[dev]'
419
+ pytest
420
+ ruff check .
421
+ python -m build
422
+ ```
423
+
424
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md),
425
+ [CHANGELOG.md](CHANGELOG.md), and [RELEASING.md](RELEASING.md) for project
426
+ policies, release history, and the maintainer release procedure.
427
+
428
+ ## License
429
+
430
+ Licensed under the [MIT License](LICENSE).