wordsnum 0.2.0__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.
- wordsnum-0.2.0/.gitattributes +1 -0
- wordsnum-0.2.0/.github/FUNDING.yml +1 -0
- wordsnum-0.2.0/.gitignore +12 -0
- wordsnum-0.2.0/CHANGELOG.md +35 -0
- wordsnum-0.2.0/CONTRIBUTING.md +19 -0
- wordsnum-0.2.0/GUIDE.md +141 -0
- wordsnum-0.2.0/IMPROVEMENTS-round.md +42 -0
- wordsnum-0.2.0/LICENSE +21 -0
- wordsnum-0.2.0/PKG-INFO +153 -0
- wordsnum-0.2.0/README.md +127 -0
- wordsnum-0.2.0/pyproject.toml +54 -0
- wordsnum-0.2.0/requirements-dev.txt +3 -0
- wordsnum-0.2.0/scripts/check.ps1 +11 -0
- wordsnum-0.2.0/scripts/release.ps1 +28 -0
- wordsnum-0.2.0/src/wordsnum/__init__.py +31 -0
- wordsnum-0.2.0/src/wordsnum/_cardinal.py +162 -0
- wordsnum-0.2.0/src/wordsnum/_cli.py +140 -0
- wordsnum-0.2.0/src/wordsnum/_errors.py +42 -0
- wordsnum-0.2.0/src/wordsnum/_find.py +108 -0
- wordsnum-0.2.0/src/wordsnum/_forms.py +181 -0
- wordsnum-0.2.0/src/wordsnum/_parser.py +91 -0
- wordsnum-0.2.0/src/wordsnum/_result.py +76 -0
- wordsnum-0.2.0/src/wordsnum/_spell.py +89 -0
- wordsnum-0.2.0/src/wordsnum/_tokens.py +155 -0
- wordsnum-0.2.0/src/wordsnum/_vocab.py +126 -0
- wordsnum-0.2.0/src/wordsnum/py.typed +0 -0
- wordsnum-0.2.0/tests/_oracle.py +178 -0
- wordsnum-0.2.0/tests/test_cli.py +103 -0
- wordsnum-0.2.0/tests/test_contract.py +156 -0
- wordsnum-0.2.0/tests/test_e2e.py +90 -0
- wordsnum-0.2.0/tests/test_find.py +80 -0
- wordsnum-0.2.0/tests/test_forms.py +157 -0
- wordsnum-0.2.0/tests/test_grammar.py +118 -0
- wordsnum-0.2.0/tests/test_roundtrip.py +127 -0
- wordsnum-0.2.0/tests/test_spell.py +55 -0
- wordsnum-0.2.0/tests/test_strict.py +86 -0
- wordsnum-0.2.0/tests/test_words_to_num.py +188 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
buy_me_a_coffee: jlianesglrs
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0]
|
|
10
|
+
|
|
11
|
+
- Replace the word-summing parser with a real grammar. Behaviour change: malformed
|
|
12
|
+
input that 0.1.0 summed into a number is now rejected with `MisplacedWordError`, for
|
|
13
|
+
example "one two" (was 3), "one, two" (was 3), "and one" (was 1), "one and" (was 1)
|
|
14
|
+
and "thousand million".
|
|
15
|
+
- Scales up to vigintillion (10**63), with ordinal forms.
|
|
16
|
+
- Fractions ("three quarters", "a half", "seven hundredths", "three thirty-seconds")
|
|
17
|
+
and mixed numbers ("two and three quarters").
|
|
18
|
+
- Decimals with a final scale word ("two point five million") and "oh" after "point".
|
|
19
|
+
- Digit groups mixed with words ("3 million", "2,500", "1.5 billion", "21st", "-5").
|
|
20
|
+
- `parse` returns a `ParsedNumber` with the exact value, kind and source span.
|
|
21
|
+
- `words_to_fraction` and `words_to_decimal` return exact results.
|
|
22
|
+
- `find_numbers` finds every number inside free text.
|
|
23
|
+
- `num_to_words` spells integers in the canonical form, round-tripping with the parser.
|
|
24
|
+
- `strict=True` accepts only the canonical spelling.
|
|
25
|
+
- Typed errors `UnknownWordError` and `MisplacedWordError` carry `word`, `start` and `end`.
|
|
26
|
+
- The `wordsnum` command line tool: `parse`, `find` and `say`.
|
|
27
|
+
- GUIDE.md documents the full grammar and the unsupported forms.
|
|
28
|
+
|
|
29
|
+
## [0.1.0]
|
|
30
|
+
|
|
31
|
+
- First release.
|
|
32
|
+
- `words_to_num` parses spelled-out English numbers into `int` or `float`.
|
|
33
|
+
- Handles units, teens, tens, hundreds, and scales up to trillion.
|
|
34
|
+
- Accepts "and", hyphenated words, articles ("a hundred"), ordinals, negatives, and decimals.
|
|
35
|
+
- Raises `WordsNumError` with a clear message on unparseable input.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time. This project keeps a small, predictable shape.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install -r requirements-dev.txt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Before you open a pull request
|
|
12
|
+
|
|
13
|
+
- Keep public API in `src/wordsnum/__init__.py`; internals live in `_`-prefixed modules.
|
|
14
|
+
- Add a test for the behaviour you change. Tests inject time, disk and network; they
|
|
15
|
+
never touch the real thing.
|
|
16
|
+
- Run `scripts/check.ps1` (format, lint, types, tests) and make sure it passes.
|
|
17
|
+
- One logical change per commit, imperative subject line.
|
|
18
|
+
|
|
19
|
+
Open an issue first if the change is large or shifts the public API.
|
wordsnum-0.2.0/GUIDE.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# wordsnum guide
|
|
2
|
+
|
|
3
|
+
This guide describes exactly what `wordsnum` accepts, how each form is read, and what
|
|
4
|
+
it does not support.
|
|
5
|
+
|
|
6
|
+
## Public API
|
|
7
|
+
|
|
8
|
+
| Name | Returns | Purpose |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `words_to_num(text, *, strict=False)` | `int` or `float` | Quick conversion |
|
|
11
|
+
| `parse(text, *, strict=False)` | `ParsedNumber` | Exact value, kind and span |
|
|
12
|
+
| `words_to_fraction(text, *, strict=False)` | `Fraction` | Exact rational value |
|
|
13
|
+
| `words_to_decimal(text, *, strict=False)` | `Decimal` | Exact decimal value |
|
|
14
|
+
| `find_numbers(text)` | `list[ParsedNumber]` | Every number inside free text |
|
|
15
|
+
| `num_to_words(n, *, ordinal=False, british=False)` | `str` | Spell an integer |
|
|
16
|
+
| `ParsedNumber` | dataclass | `value`, `kind`, `text`, `start`, `end`, `number`, `to_decimal()` |
|
|
17
|
+
| `Kind` | `Literal` | `"cardinal"`, `"ordinal"`, `"decimal"`, `"fraction"` |
|
|
18
|
+
| `WordsNumError` | exception | Base error, a `ValueError` |
|
|
19
|
+
| `UnknownWordError` | exception | A word that is not a number word |
|
|
20
|
+
| `MisplacedWordError` | exception | A number word in a position the grammar rejects |
|
|
21
|
+
|
|
22
|
+
Every error carries `word`, `start` and `end`, with `text[start:end] == word`, or
|
|
23
|
+
`None` for all three when no single word is at fault (empty input).
|
|
24
|
+
|
|
25
|
+
## Cardinals
|
|
26
|
+
|
|
27
|
+
A cardinal is a sequence of groups, each below one thousand, joined by scale words in
|
|
28
|
+
strictly decreasing order:
|
|
29
|
+
|
|
30
|
+
- Units and teens: `zero` to `nineteen`. `zero` must stand alone.
|
|
31
|
+
- Tens: `twenty` to `ninety`, optionally followed by a unit: `twenty-one`, `forty two`.
|
|
32
|
+
- Hundreds: `one hundred`, `nine hundred ninety-nine`, `a hundred`.
|
|
33
|
+
- Scales, short scale: `thousand` (10**3), `million`, `billion`, `trillion`,
|
|
34
|
+
`quadrillion`, `quintillion`, `sextillion`, `septillion`, `octillion`, `nonillion`,
|
|
35
|
+
`decillion` (10**33), `undecillion`, `duodecillion`, `tredecillion`,
|
|
36
|
+
`quattuordecillion`, `quindecillion`, `sexdecillion`, `septendecillion`,
|
|
37
|
+
`octodecillion`, `novemdecillion` and `vigintillion` (10**63).
|
|
38
|
+
- A leading scale may drop its `one`: `hundred`, `thousand two hundred`.
|
|
39
|
+
- `a`/`an` stands for one before `hundred` or a scale: `a thousand and one`.
|
|
40
|
+
- Year style: a teen or tens word before `hundred` (`nineteen hundred eighty four`,
|
|
41
|
+
`twenty-five hundred`) is accepted when no other scale word appears.
|
|
42
|
+
- British `and` is accepted after `hundred` (`one hundred and one`) and after a scale
|
|
43
|
+
before a final group below one hundred (`one thousand and five`). Anywhere else it is
|
|
44
|
+
an error.
|
|
45
|
+
|
|
46
|
+
Rejected examples: `one two`, `thousand million`, `one thousand one million`,
|
|
47
|
+
`zero hundred`, `one hundred hundred`, `one and two`.
|
|
48
|
+
|
|
49
|
+
## Ordinals
|
|
50
|
+
|
|
51
|
+
The last word may be an ordinal: `first`, `twelfth`, `twentieth`, `twenty-first`,
|
|
52
|
+
`one hundred and third`, `hundredth`, `one millionth`, `decillionth`. Digit ordinals
|
|
53
|
+
such as `21st`, `2nd`, `103rd` and `111th` work when the suffix is correct. Ordinals
|
|
54
|
+
are never negative.
|
|
55
|
+
|
|
56
|
+
## Decimals
|
|
57
|
+
|
|
58
|
+
`point` is followed by one-digit words (`zero` to `nine`, or `oh`):
|
|
59
|
+
`three point one four`, `point five`, `one point oh five`. One final scale word may
|
|
60
|
+
follow the digits when the whole part is below a thousand: `two point five million`.
|
|
61
|
+
Digit decimals also work: `3.14`, `1,234.5`, `1.5 billion`.
|
|
62
|
+
|
|
63
|
+
## Fractions
|
|
64
|
+
|
|
65
|
+
A numerator followed by a denominator word:
|
|
66
|
+
|
|
67
|
+
- `half`/`halves`, `quarter`/`quarters`, and ordinal denominators such as `thirds`,
|
|
68
|
+
`fourths`, `hundredths`, `millionths`. A hyphenated compound also works:
|
|
69
|
+
`three thirty-seconds`, `a twenty-first`.
|
|
70
|
+
- A numerator of one takes the singular (`a third`, `one half`); any other numerator
|
|
71
|
+
takes the plural (`two thirds`, `zero halves`).
|
|
72
|
+
- `one` followed by a scale ordinal reads as an ordinal (`one hundredth` is the 100th);
|
|
73
|
+
use `a hundredth` for the fraction 1/100.
|
|
74
|
+
- Mixed numbers: `one and a half`, `two and three quarters`. The reading after the last
|
|
75
|
+
`and` is used as a mixed number only when that fraction is proper, so
|
|
76
|
+
`one hundred and one third` is 100 1/3 while `two hundred and fifty thirds` is 250/3.
|
|
77
|
+
- `first` and `second` are not denominators on their own (`two seconds` is not 2/2).
|
|
78
|
+
- Scale denominators read greedily as one numerator: `three hundred thousandths` is
|
|
79
|
+
300/1000 and `three hundred and fifty thousandths` is 300 and 50/1000. Compound scale
|
|
80
|
+
denominators are not supported: `three hundred-thousandths` is also read as
|
|
81
|
+
300/1000, so write 3/100000 as a decimal (`zero point zero zero zero zero three`).
|
|
82
|
+
|
|
83
|
+
## Signs and digits
|
|
84
|
+
|
|
85
|
+
`minus` or `negative` may lead any cardinal, decimal or fraction, and a `-` written
|
|
86
|
+
directly before leading digits is a sign too (`-5`, `-1.5 million`). Digit groups mix with
|
|
87
|
+
words: `3 million`, `5 hundred`, `2,500`, `1 million two hundred`. A digit group after
|
|
88
|
+
a scale must be below a thousand, and a digit decimal (`1.5`) may only start the
|
|
89
|
+
number. Digits cannot follow a word inside a group (`twenty 5` and `one hundred 5` are
|
|
90
|
+
rejected). Only ASCII digits are read, and runs longer than 1000 characters are not
|
|
91
|
+
treated as numbers.
|
|
92
|
+
|
|
93
|
+
## Strict and lenient
|
|
94
|
+
|
|
95
|
+
Both modes accept the same grammar; they differ only in how the words may be written.
|
|
96
|
+
Lenient mode, the default, ignores case, collapses whitespace and accepts hyphens and
|
|
97
|
+
commas between words. Strict mode checks the written form: lowercase words, exactly
|
|
98
|
+
one space between words, a hyphen exactly between a tens word and a unit word
|
|
99
|
+
(`twenty-one`, `thirty-second`), no commas between words, no surrounding whitespace,
|
|
100
|
+
and the article that matches the sound of the next word (`a hundred`, `an eighth`).
|
|
101
|
+
Everything `num_to_words` writes passes strict mode. Grammatical variants such as
|
|
102
|
+
`negative five`, `hundred`, `point five`, `2,500` or `twenty-five hundred` are valid in
|
|
103
|
+
both modes. Neither mode corrects spelling.
|
|
104
|
+
|
|
105
|
+
Spacing matters for compound denominators: `twenty-ninths` is a plural of 1/29 while
|
|
106
|
+
`twenty ninths` is 20/9, and `three thirty-seconds` (3/32) needs its hyphen in both
|
|
107
|
+
modes.
|
|
108
|
+
|
|
109
|
+
## Finding numbers in text
|
|
110
|
+
|
|
111
|
+
`find_numbers` scans free text, case-insensitively, and returns the longest valid
|
|
112
|
+
reading at each position. Words that are also everyday words are reported as numbers:
|
|
113
|
+
`no one` contains 1 and `just a second` contains the ordinal 2. A comma-separated list
|
|
114
|
+
can merge when the words also form one number (`sixty, five, four` gives 65 and 4), and
|
|
115
|
+
dotted strings such as version numbers are read as decimals. Filter by `kind` or by
|
|
116
|
+
context when that matters. A decimal or fraction too large for a `float` makes
|
|
117
|
+
`number` raise `WordsNumError`; `value` always holds the exact result.
|
|
118
|
+
|
|
119
|
+
## Unsupported forms
|
|
120
|
+
|
|
121
|
+
These are rejected rather than guessed:
|
|
122
|
+
|
|
123
|
+
- `dozen`, `score`, `gross`, `half a million`, `one and a half million`.
|
|
124
|
+
- Year style without `hundred`: `nineteen eighty four` (write `nineteen hundred eighty
|
|
125
|
+
four`); `find_numbers` reports it as 19 and 84.
|
|
126
|
+
- Long-scale meanings (British `billion` as 10**12) and scales above vigintillion.
|
|
127
|
+
- Spelling-error correction of any kind.
|
|
128
|
+
- Roman numerals and other languages.
|
|
129
|
+
- Spelling non-integers with `num_to_words`.
|
|
130
|
+
|
|
131
|
+
## Command line
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
wordsnum parse WORDS... [--strict] [--exact] [--json]
|
|
135
|
+
wordsnum find [--text TEXT] [--exact] [--json] # reads stdin without --text
|
|
136
|
+
wordsnum say NUMBER [--ordinal] [--british]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Without words, `parse` reads one number per non-empty stdin line. `--exact` prints
|
|
140
|
+
`3/4` and `1.1` instead of floats. Errors are printed to stderr and the exit status
|
|
141
|
+
is 1.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Improvement round: ranked plan
|
|
2
|
+
|
|
3
|
+
Ranked by value to users. Every item keeps the existing API working; new names are added
|
|
4
|
+
alongside `words_to_num` and `WordsNumError`.
|
|
5
|
+
|
|
6
|
+
1. **A real grammar instead of a running sum.** The 0.1 parser added words together, so
|
|
7
|
+
`"one two"` gave 3 and `"hundred thousand hundred"` gave a number. Replace it with a
|
|
8
|
+
grammar that checks word order: groups below a thousand, scales in strictly
|
|
9
|
+
decreasing order, `"and"` only where British usage puts it, `"zero"` only on its own.
|
|
10
|
+
2. **Scales to vigintillion.** Short scale from thousand up to vigintillion (10**63),
|
|
11
|
+
with ordinal forms (`"millionth"`, `"decillionth"`). Covers "at least decillion".
|
|
12
|
+
3. **Fractions.** `"three quarters"`, `"a half"`, `"two thirds"`, `"seven hundredths"`,
|
|
13
|
+
`"three thirty-seconds"`, and mixed numbers (`"two and three quarters"`).
|
|
14
|
+
4. **Exact results.** `words_to_fraction` and `words_to_decimal` return `Fraction` and
|
|
15
|
+
`Decimal`, so `"one point one"` is exactly 11/10 and never a binary float.
|
|
16
|
+
5. **Typed errors that point at the bad word.** `UnknownWordError` (not a number word)
|
|
17
|
+
and `MisplacedWordError` (a number word in the wrong place), both carrying `word`,
|
|
18
|
+
`start` and `end` offsets into the original text.
|
|
19
|
+
6. **Strict and lenient modes.** Lenient (default) tolerates case, extra spacing,
|
|
20
|
+
hyphens and commas. Strict accepts only the canonical lowercase spelling. Neither
|
|
21
|
+
mode guesses typos.
|
|
22
|
+
7. **Mixed digits and words.** `"3 million"`, `"1.5 billion"`, `"2,500"`, `"21st"`.
|
|
23
|
+
8. **Decimals with a scale.** `"two point five million"`, and `"oh"` as a digit after
|
|
24
|
+
the point (`"one point oh five"`).
|
|
25
|
+
9. **Finding numbers inside text.** `find_numbers` returns every number phrase in free
|
|
26
|
+
text with its span, value and kind, using the longest valid reading.
|
|
27
|
+
10. **A structured result.** `parse` returns a `ParsedNumber` with the exact value, the
|
|
28
|
+
kind (`cardinal`, `ordinal`, `decimal`, `fraction`) and the source span.
|
|
29
|
+
11. **The inverse for integers.** `num_to_words` spells an int (cardinal or ordinal,
|
|
30
|
+
American or British) in exactly the canonical form strict mode accepts, so the two
|
|
31
|
+
directions round-trip.
|
|
32
|
+
12. **A command line tool.** `wordsnum parse`, `wordsnum find` (reads stdin) and
|
|
33
|
+
`wordsnum say`, with JSON output and errors on stderr.
|
|
34
|
+
13. **Documentation.** Docstrings on every public name and a GUIDE covering the grammar,
|
|
35
|
+
the modes, the supported and unsupported forms.
|
|
36
|
+
14. **Tests as contracts.** A contract test over every public promise, a seeded
|
|
37
|
+
round-trip against an independent spelling generator on thousands of numbers, and
|
|
38
|
+
an end-to-end test that builds the wheel, installs it into a fresh virtual
|
|
39
|
+
environment and drives the installed command.
|
|
40
|
+
|
|
41
|
+
Out of scope, documented as unsupported: "dozen", "score", "half a million", long-scale
|
|
42
|
+
billions, Roman numerals, spelling of non-integers, and guessing misspelled words.
|
wordsnum-0.2.0/LICENSE
ADDED
|
@@ -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.
|
wordsnum-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wordsnum
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Parse spelled-out English numbers into int or float, fully typed and dependency-free
|
|
5
|
+
Project-URL: Homepage, https://lianes8studio.com
|
|
6
|
+
Project-URL: Repository, https://github.com/JaviLianes8/wordsnum
|
|
7
|
+
Project-URL: Issues, https://github.com/JaviLianes8/wordsnum/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JaviLianes8/wordsnum/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
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build; extra == 'dev'
|
|
21
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
24
|
+
Requires-Dist: twine; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# wordsnum
|
|
28
|
+
|
|
29
|
+
Parse spelled-out English numbers into int or float, fully typed and dependency-free.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install wordsnum
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from wordsnum import words_to_num
|
|
41
|
+
|
|
42
|
+
words_to_num("two hundred fifty") # 250
|
|
43
|
+
words_to_num("two thousand and twenty-one") # 2021
|
|
44
|
+
words_to_num("nineteen hundred eighty four") # 1984
|
|
45
|
+
words_to_num("a hundred") # 100
|
|
46
|
+
words_to_num("minus forty two") # -42
|
|
47
|
+
words_to_num("twenty-first") # 21 (ordinals)
|
|
48
|
+
words_to_num("three point one four") # 3.14
|
|
49
|
+
words_to_num("two point five million") # 2500000.0
|
|
50
|
+
words_to_num("two and three quarters") # 2.75
|
|
51
|
+
words_to_num("3 million") # 3000000
|
|
52
|
+
words_to_num("one decillion") # 10**33
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Cardinals and ordinals come back as `int`; decimals and fractions as `float`.
|
|
56
|
+
|
|
57
|
+
### Exact values, kinds and spans
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from wordsnum import parse, words_to_decimal, words_to_fraction
|
|
61
|
+
|
|
62
|
+
words_to_fraction("two thirds") # Fraction(2, 3)
|
|
63
|
+
words_to_decimal("one point one") # Decimal('1.1')
|
|
64
|
+
|
|
65
|
+
result = parse(" Seven Hundredths ")
|
|
66
|
+
result.value # Fraction(7, 100)
|
|
67
|
+
result.kind # 'fraction' (also 'cardinal', 'ordinal', 'decimal')
|
|
68
|
+
result.text, result.start, result.end # ('Seven Hundredths', 2, 18)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Numbers inside text
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from wordsnum import find_numbers
|
|
75
|
+
|
|
76
|
+
for match in find_numbers("We sold twenty-one boxes, then 3 million more."):
|
|
77
|
+
print(match.start, match.end, match.number, match.text)
|
|
78
|
+
# 8 18 21 twenty-one
|
|
79
|
+
# 31 40 3000000 3 million
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Spelling integers
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from wordsnum import num_to_words
|
|
86
|
+
|
|
87
|
+
num_to_words(1234) # 'one thousand two hundred thirty-four'
|
|
88
|
+
num_to_words(1005, british=True) # 'one thousand and five'
|
|
89
|
+
num_to_words(21, ordinal=True) # 'twenty-first'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`num_to_words` writes exactly the canonical form that strict parsing accepts, so the
|
|
93
|
+
two directions round-trip.
|
|
94
|
+
|
|
95
|
+
### Strict and lenient
|
|
96
|
+
|
|
97
|
+
By default case, extra spaces, hyphens and commas are tolerated. `strict=True` accepts
|
|
98
|
+
only the canonical spelling (lowercase, single spaces, `twenty-one` hyphenated, no
|
|
99
|
+
commas). Neither mode guesses typos: `"fourty"` is always an error.
|
|
100
|
+
|
|
101
|
+
### Errors that point at the problem
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from wordsnum import MisplacedWordError, UnknownWordError, words_to_num
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
words_to_num("two hundred banana")
|
|
108
|
+
except UnknownWordError as error:
|
|
109
|
+
print(error.word, error.start, error.end) # banana 12 18
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
words_to_num("one thousand one million")
|
|
113
|
+
except MisplacedWordError as error:
|
|
114
|
+
print(error) # scale words must get smaller from left to right: 'million' at position 17
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Both subclass `WordsNumError`, which subclasses `ValueError`.
|
|
118
|
+
|
|
119
|
+
## Command line
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
wordsnum parse two thousand and twenty-one # 2021
|
|
123
|
+
wordsnum parse three quarters --exact # 3/4
|
|
124
|
+
echo "minus forty two" | wordsnum parse # -42
|
|
125
|
+
echo "I owe you twenty-one pounds" | wordsnum find
|
|
126
|
+
wordsnum say 21 --ordinal # twenty-first
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Run `wordsnum --help` for every option. Errors go to stderr with exit status 1.
|
|
130
|
+
|
|
131
|
+
See [GUIDE.md](GUIDE.md) for the full grammar and the list of unsupported forms.
|
|
132
|
+
|
|
133
|
+
## Why
|
|
134
|
+
|
|
135
|
+
Turning words back into numbers is the reverse of spelling them out, and the existing
|
|
136
|
+
options are thin. `word2number` has not seen a release since 2017, ships no type hints,
|
|
137
|
+
and mishandles several common phrasings. `numerizer` is a port of a Ruby gem, is
|
|
138
|
+
untyped, and returns strings rather than numbers. `text2num` is maintained but builds
|
|
139
|
+
on a Rust extension, which is a heavy dependency for a small parsing job.
|
|
140
|
+
|
|
141
|
+
`wordsnum` is a small, pure-Python parser with zero runtime dependencies, a `py.typed`
|
|
142
|
+
marker, a clean `mypy --strict` build, a real grammar that rejects malformed input
|
|
143
|
+
instead of summing words, and exact results when you need them.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT. See [LICENSE](LICENSE).
|
|
148
|
+
|
|
149
|
+
## Links
|
|
150
|
+
|
|
151
|
+
- Website: https://lianes8studio.com
|
|
152
|
+
- GitHub: https://github.com/JaviLianes8
|
|
153
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
wordsnum-0.2.0/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# wordsnum
|
|
2
|
+
|
|
3
|
+
Parse spelled-out English numbers into int or float, fully typed and dependency-free.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install wordsnum
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from wordsnum import words_to_num
|
|
15
|
+
|
|
16
|
+
words_to_num("two hundred fifty") # 250
|
|
17
|
+
words_to_num("two thousand and twenty-one") # 2021
|
|
18
|
+
words_to_num("nineteen hundred eighty four") # 1984
|
|
19
|
+
words_to_num("a hundred") # 100
|
|
20
|
+
words_to_num("minus forty two") # -42
|
|
21
|
+
words_to_num("twenty-first") # 21 (ordinals)
|
|
22
|
+
words_to_num("three point one four") # 3.14
|
|
23
|
+
words_to_num("two point five million") # 2500000.0
|
|
24
|
+
words_to_num("two and three quarters") # 2.75
|
|
25
|
+
words_to_num("3 million") # 3000000
|
|
26
|
+
words_to_num("one decillion") # 10**33
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Cardinals and ordinals come back as `int`; decimals and fractions as `float`.
|
|
30
|
+
|
|
31
|
+
### Exact values, kinds and spans
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from wordsnum import parse, words_to_decimal, words_to_fraction
|
|
35
|
+
|
|
36
|
+
words_to_fraction("two thirds") # Fraction(2, 3)
|
|
37
|
+
words_to_decimal("one point one") # Decimal('1.1')
|
|
38
|
+
|
|
39
|
+
result = parse(" Seven Hundredths ")
|
|
40
|
+
result.value # Fraction(7, 100)
|
|
41
|
+
result.kind # 'fraction' (also 'cardinal', 'ordinal', 'decimal')
|
|
42
|
+
result.text, result.start, result.end # ('Seven Hundredths', 2, 18)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Numbers inside text
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from wordsnum import find_numbers
|
|
49
|
+
|
|
50
|
+
for match in find_numbers("We sold twenty-one boxes, then 3 million more."):
|
|
51
|
+
print(match.start, match.end, match.number, match.text)
|
|
52
|
+
# 8 18 21 twenty-one
|
|
53
|
+
# 31 40 3000000 3 million
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Spelling integers
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from wordsnum import num_to_words
|
|
60
|
+
|
|
61
|
+
num_to_words(1234) # 'one thousand two hundred thirty-four'
|
|
62
|
+
num_to_words(1005, british=True) # 'one thousand and five'
|
|
63
|
+
num_to_words(21, ordinal=True) # 'twenty-first'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`num_to_words` writes exactly the canonical form that strict parsing accepts, so the
|
|
67
|
+
two directions round-trip.
|
|
68
|
+
|
|
69
|
+
### Strict and lenient
|
|
70
|
+
|
|
71
|
+
By default case, extra spaces, hyphens and commas are tolerated. `strict=True` accepts
|
|
72
|
+
only the canonical spelling (lowercase, single spaces, `twenty-one` hyphenated, no
|
|
73
|
+
commas). Neither mode guesses typos: `"fourty"` is always an error.
|
|
74
|
+
|
|
75
|
+
### Errors that point at the problem
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from wordsnum import MisplacedWordError, UnknownWordError, words_to_num
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
words_to_num("two hundred banana")
|
|
82
|
+
except UnknownWordError as error:
|
|
83
|
+
print(error.word, error.start, error.end) # banana 12 18
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
words_to_num("one thousand one million")
|
|
87
|
+
except MisplacedWordError as error:
|
|
88
|
+
print(error) # scale words must get smaller from left to right: 'million' at position 17
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Both subclass `WordsNumError`, which subclasses `ValueError`.
|
|
92
|
+
|
|
93
|
+
## Command line
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
wordsnum parse two thousand and twenty-one # 2021
|
|
97
|
+
wordsnum parse three quarters --exact # 3/4
|
|
98
|
+
echo "minus forty two" | wordsnum parse # -42
|
|
99
|
+
echo "I owe you twenty-one pounds" | wordsnum find
|
|
100
|
+
wordsnum say 21 --ordinal # twenty-first
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run `wordsnum --help` for every option. Errors go to stderr with exit status 1.
|
|
104
|
+
|
|
105
|
+
See [GUIDE.md](GUIDE.md) for the full grammar and the list of unsupported forms.
|
|
106
|
+
|
|
107
|
+
## Why
|
|
108
|
+
|
|
109
|
+
Turning words back into numbers is the reverse of spelling them out, and the existing
|
|
110
|
+
options are thin. `word2number` has not seen a release since 2017, ships no type hints,
|
|
111
|
+
and mishandles several common phrasings. `numerizer` is a port of a Ruby gem, is
|
|
112
|
+
untyped, and returns strings rather than numbers. `text2num` is maintained but builds
|
|
113
|
+
on a Rust extension, which is a heavy dependency for a small parsing job.
|
|
114
|
+
|
|
115
|
+
`wordsnum` is a small, pure-Python parser with zero runtime dependencies, a `py.typed`
|
|
116
|
+
marker, a clean `mypy --strict` build, a real grammar that rejects malformed input
|
|
117
|
+
instead of summing words, and exact results when you need them.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT. See [LICENSE](LICENSE).
|
|
122
|
+
|
|
123
|
+
## Links
|
|
124
|
+
|
|
125
|
+
- Website: https://lianes8studio.com
|
|
126
|
+
- GitHub: https://github.com/JaviLianes8
|
|
127
|
+
- LinkedIn: https://www.linkedin.com/in/jlianes/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wordsnum"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Parse spelled-out English numbers into int or float, fully typed and dependency-free"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
|
|
14
|
+
keywords = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["ruff", "mypy", "pytest", "build", "twine"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
wordsnum = "wordsnum._cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://lianes8studio.com"
|
|
31
|
+
Repository = "https://github.com/JaviLianes8/wordsnum"
|
|
32
|
+
Issues = "https://github.com/JaviLianes8/wordsnum/issues"
|
|
33
|
+
Changelog = "https://github.com/JaviLianes8/wordsnum/blob/main/CHANGELOG.md"
|
|
34
|
+
LinkedIn = "https://www.linkedin.com/in/jlianes/"
|
|
35
|
+
Funding = "https://buymeacoffee.com/jlianesglrs"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/wordsnum"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
python_version = "3.10"
|
|
49
|
+
strict = true
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
pythonpath = ["src"]
|
|
54
|
+
addopts = "-q"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Local quality gate. Run before every commit. No CI runs in the cloud.
|
|
2
|
+
$ErrorActionPreference = "Stop"
|
|
3
|
+
|
|
4
|
+
ruff format --check .
|
|
5
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
6
|
+
ruff check .
|
|
7
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
8
|
+
mypy --strict src
|
|
9
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
10
|
+
pytest
|
|
11
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Build and upload to PyPI. Run scripts/check.ps1 first.
|
|
2
|
+
# The name must not already exist on PyPI; the token is read from a secret store, never git.
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
# Works on both Windows PowerShell 5.1 and pwsh 7 (generic catch, no PS7-only types).
|
|
6
|
+
$taken = $true
|
|
7
|
+
try {
|
|
8
|
+
Invoke-WebRequest -UseBasicParsing "https://pypi.org/pypi/wordsnum/json" | Out-Null
|
|
9
|
+
} catch {
|
|
10
|
+
if ($_.Exception.Response.StatusCode.value__ -eq 404) { $taken = $false } else { throw }
|
|
11
|
+
}
|
|
12
|
+
if ($taken) { throw "The name 'wordsnum' already exists on PyPI. Choose another before publishing." }
|
|
13
|
+
|
|
14
|
+
$token = $env:PYPI_TOKEN
|
|
15
|
+
if (-not $token -and (Test-Path "C:\secrets\pypi-token.txt")) {
|
|
16
|
+
$token = (Get-Content "C:\secrets\pypi-token.txt" -Raw).Trim()
|
|
17
|
+
}
|
|
18
|
+
if (-not $token) {
|
|
19
|
+
throw "No PyPI token. Set `$env:PYPI_TOKEN or save it to C:\secrets\pypi-token.txt (never commit it)."
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
|
|
23
|
+
python -m build
|
|
24
|
+
python -m twine check dist/*
|
|
25
|
+
|
|
26
|
+
$env:TWINE_USERNAME = "__token__"
|
|
27
|
+
$env:TWINE_PASSWORD = $token
|
|
28
|
+
try { python -m twine upload dist/* } finally { Remove-Item Env:\TWINE_PASSWORD -ErrorAction SilentlyContinue }
|