numly 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.
- numly/__init__.py +64 -0
- numly/arabic_indic.py +113 -0
- numly/chinese.py +187 -0
- numly/convert.py +187 -0
- numly/egyptian.py +163 -0
- numly/greek.py +152 -0
- numly/roman.py +118 -0
- numly-0.1.0.dist-info/METADATA +107 -0
- numly-0.1.0.dist-info/RECORD +11 -0
- numly-0.1.0.dist-info/WHEEL +5 -0
- numly-0.1.0.dist-info/top_level.txt +1 -0
numly/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly
|
|
3
|
+
~~~~~
|
|
4
|
+
A Python library for working with numbers across numeral systems.
|
|
5
|
+
|
|
6
|
+
Supported systems
|
|
7
|
+
-----------------
|
|
8
|
+
decimal : Standard base-10
|
|
9
|
+
roman : Roman numerals (I V X L C D M)
|
|
10
|
+
arabic_indic : Eastern Arabic digits (٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩)
|
|
11
|
+
chinese : Traditional Chinese (零 一 二 三 四 … 万)
|
|
12
|
+
greek : Greek alphabetic (Α Β Γ … Ω + ͵ for thousands)
|
|
13
|
+
egyptian : Egyptian hieroglyphs (𓏺 𓎆 𓍢 𓆼 𓂭 𓆐 𓁨)
|
|
14
|
+
|
|
15
|
+
Quick start
|
|
16
|
+
-----------
|
|
17
|
+
import numly
|
|
18
|
+
|
|
19
|
+
numly.to_roman(2024) # 'MMXXIV'
|
|
20
|
+
numly.to_chinese(42) # '四十二'
|
|
21
|
+
numly.convert("MMXXIV", "roman", "greek") # '͵ΒΚΔʹ'
|
|
22
|
+
numly.to_all(42) # dict of every system
|
|
23
|
+
|
|
24
|
+
Individual modules
|
|
25
|
+
------------------
|
|
26
|
+
from numly.roman import to_roman, from_roman
|
|
27
|
+
from numly.arabic_indic import to_arabic_indic, from_arabic_indic
|
|
28
|
+
from numly.chinese import to_chinese, from_chinese
|
|
29
|
+
from numly.greek import to_greek, from_greek
|
|
30
|
+
from numly.egyptian import to_egyptian, from_egyptian, symbol_breakdown
|
|
31
|
+
from numly.convert import convert, to_all, SYSTEMS
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
__author__ = "numly contributors"
|
|
36
|
+
__license__ = "MIT"
|
|
37
|
+
|
|
38
|
+
# ── individual converters ──────────────────────────────────────────────────
|
|
39
|
+
from numly.roman import to_roman, from_roman, is_valid as is_valid_roman
|
|
40
|
+
from numly.arabic_indic import to_arabic_indic, from_arabic_indic, is_valid as is_valid_arabic_indic
|
|
41
|
+
from numly.chinese import to_chinese, from_chinese, is_valid as is_valid_chinese
|
|
42
|
+
from numly.greek import to_greek, from_greek, is_valid as is_valid_greek
|
|
43
|
+
from numly.egyptian import to_egyptian, from_egyptian, is_valid as is_valid_egyptian
|
|
44
|
+
from numly.egyptian import symbol_breakdown
|
|
45
|
+
|
|
46
|
+
# ── universal converter ────────────────────────────────────────────────────
|
|
47
|
+
from numly.convert import convert, to_all, SYSTEMS, supported_systems
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
# roman
|
|
51
|
+
"to_roman", "from_roman", "is_valid_roman",
|
|
52
|
+
# arabic-indic
|
|
53
|
+
"to_arabic_indic", "from_arabic_indic", "is_valid_arabic_indic",
|
|
54
|
+
# chinese
|
|
55
|
+
"to_chinese", "from_chinese", "is_valid_chinese",
|
|
56
|
+
# greek
|
|
57
|
+
"to_greek", "from_greek", "is_valid_greek",
|
|
58
|
+
# egyptian
|
|
59
|
+
"to_egyptian", "from_egyptian", "is_valid_egyptian", "symbol_breakdown",
|
|
60
|
+
# universal
|
|
61
|
+
"convert", "to_all", "SYSTEMS", "supported_systems",
|
|
62
|
+
# meta
|
|
63
|
+
"__version__",
|
|
64
|
+
]
|
numly/arabic_indic.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.arabic_indic
|
|
3
|
+
~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Convert integers to Eastern Arabic numerals (٠١٢٣٤٥٦٧٨٩) and back.
|
|
5
|
+
|
|
6
|
+
These are the digits used in Arabic, Persian, and Urdu scripts.
|
|
7
|
+
Unlike Roman or Chinese, this is a pure digit-substitution system —
|
|
8
|
+
the positional value works exactly like decimal.
|
|
9
|
+
|
|
10
|
+
Range: 0 – unlimited
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
>>> from numly.arabic_indic import to_arabic_indic, from_arabic_indic
|
|
14
|
+
>>> to_arabic_indic(2024)
|
|
15
|
+
'٢٠٢٤'
|
|
16
|
+
>>> from_arabic_indic('٤٢')
|
|
17
|
+
42
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Western: 0 1 2 3 4 5 6 7 8 9
|
|
21
|
+
# Eastern: ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩
|
|
22
|
+
_DIGITS = "٠١٢٣٤٥٦٧٨٩"
|
|
23
|
+
_TO_ASCII = {ch: str(i) for i, ch in enumerate(_DIGITS)}
|
|
24
|
+
|
|
25
|
+
SYSTEM = "arabic_indic"
|
|
26
|
+
MIN = 0
|
|
27
|
+
MAX = None # unlimited
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_arabic_indic(num: int) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Convert an integer to an Eastern Arabic numeral string.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
num: Non-negative integer (no upper limit).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Eastern Arabic numeral string, e.g. '٤٢'.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
TypeError: If num is not an int.
|
|
42
|
+
ValueError: If num is negative.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> to_arabic_indic(0)
|
|
46
|
+
'٠'
|
|
47
|
+
>>> to_arabic_indic(42)
|
|
48
|
+
'٤٢'
|
|
49
|
+
>>> to_arabic_indic(2024)
|
|
50
|
+
'٢٠٢٤'
|
|
51
|
+
"""
|
|
52
|
+
if not isinstance(num, int):
|
|
53
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
54
|
+
if num < 0:
|
|
55
|
+
raise ValueError("Only non-negative integers are supported")
|
|
56
|
+
return "".join(_DIGITS[int(d)] for d in str(num))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def from_arabic_indic(s: str) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Convert an Eastern Arabic numeral string to an integer.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
s: Eastern Arabic numeral string, e.g. '٤٢'.
|
|
65
|
+
Also accepts plain ASCII digits or a mix of both.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Integer value.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
TypeError: If s is not a str.
|
|
72
|
+
ValueError: If s contains non-numeric characters.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
>>> from_arabic_indic('٤٢')
|
|
76
|
+
42
|
|
77
|
+
>>> from_arabic_indic('٢٠٢٤')
|
|
78
|
+
2024
|
|
79
|
+
"""
|
|
80
|
+
if not isinstance(s, str):
|
|
81
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
82
|
+
s = s.strip()
|
|
83
|
+
if not s:
|
|
84
|
+
raise ValueError("Empty string")
|
|
85
|
+
|
|
86
|
+
ascii_s = "".join(_TO_ASCII.get(ch, ch) for ch in s)
|
|
87
|
+
try:
|
|
88
|
+
return int(ascii_s)
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise ValueError(f"Invalid Arabic-Indic numeral: {s!r}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_valid(s: str) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check whether a string is a valid Eastern Arabic numeral.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> is_valid('٤٢')
|
|
99
|
+
True
|
|
100
|
+
>>> is_valid('abc')
|
|
101
|
+
False
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
from_arabic_indic(s)
|
|
105
|
+
return True
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
for n in [0, 1, 42, 100, 2024, 999_999]:
|
|
112
|
+
r = to_arabic_indic(n)
|
|
113
|
+
print(f"{n:<10} → {r:<12} → {from_arabic_indic(r)}")
|
numly/chinese.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.chinese
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
Convert integers to Traditional Chinese numerals and back.
|
|
5
|
+
|
|
6
|
+
Uses the standard literary/financial positional system:
|
|
7
|
+
零一二三四五六七八九 (digits)
|
|
8
|
+
十 百 千 万 (place units)
|
|
9
|
+
|
|
10
|
+
Range: 0 – 99,999,999
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
>>> from numly.chinese import to_chinese, from_chinese
|
|
14
|
+
>>> to_chinese(42)
|
|
15
|
+
'四十二'
|
|
16
|
+
>>> from_chinese('一万零一')
|
|
17
|
+
10001
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_DIGITS = "零一二三四五六七八九"
|
|
21
|
+
_UNIT_MAP = {"十": 10, "百": 100, "千": 1000, "万": 10_000}
|
|
22
|
+
|
|
23
|
+
SYSTEM = "chinese"
|
|
24
|
+
MIN, MAX = 0, 99_999_999
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── internal helper ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
def _encode_section(n: int) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Encode an integer 1–9999 to Chinese without any outer 万 context.
|
|
32
|
+
Handles internal zeros (e.g. 101 → 一百零一).
|
|
33
|
+
"""
|
|
34
|
+
units = [(1000, "千"), (100, "百"), (10, "十"), (1, "")]
|
|
35
|
+
result = ""
|
|
36
|
+
zero_pending = False
|
|
37
|
+
|
|
38
|
+
for val, unit in units:
|
|
39
|
+
d = n // val
|
|
40
|
+
n %= val
|
|
41
|
+
if d:
|
|
42
|
+
if zero_pending:
|
|
43
|
+
result += "零"
|
|
44
|
+
zero_pending = False
|
|
45
|
+
result += _DIGITS[d] + unit
|
|
46
|
+
elif result: # non-leading zero in the middle
|
|
47
|
+
zero_pending = True
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _decode_section(s: str) -> int:
|
|
53
|
+
"""
|
|
54
|
+
Decode a Chinese string that contains no 万 character (up to 9999).
|
|
55
|
+
Handles leading 零 and bare 十 (e.g. '十二' → 12).
|
|
56
|
+
"""
|
|
57
|
+
cn_val = {ch: i for i, ch in enumerate(_DIGITS)}
|
|
58
|
+
result, digit = 0, 0
|
|
59
|
+
|
|
60
|
+
for ch in s:
|
|
61
|
+
if ch in cn_val:
|
|
62
|
+
digit = cn_val[ch]
|
|
63
|
+
elif ch in _UNIT_MAP and ch != "万":
|
|
64
|
+
unit = _UNIT_MAP[ch]
|
|
65
|
+
if digit == 0:
|
|
66
|
+
digit = 1 # bare 十 → treat as 一十
|
|
67
|
+
result += digit * unit
|
|
68
|
+
digit = 0
|
|
69
|
+
|
|
70
|
+
result += digit # final ones digit (may be 0 for 零)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── public API ─────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def to_chinese(num: int) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Convert an integer to a Traditional Chinese numeral string.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
num: Integer between 0 and 99,999,999.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Chinese numeral string, e.g. '四十二'.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TypeError: If num is not an int.
|
|
88
|
+
ValueError: If num is outside 0–99,999,999.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> to_chinese(0)
|
|
92
|
+
'零'
|
|
93
|
+
>>> to_chinese(42)
|
|
94
|
+
'四十二'
|
|
95
|
+
>>> to_chinese(10001)
|
|
96
|
+
'一万零一'
|
|
97
|
+
>>> to_chinese(20240)
|
|
98
|
+
'二万零二百四十'
|
|
99
|
+
>>> to_chinese(99999999)
|
|
100
|
+
'九千九百九十九万九千九百九十九'
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(num, int):
|
|
103
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
104
|
+
if num == 0:
|
|
105
|
+
return "零"
|
|
106
|
+
if not MIN <= num <= MAX:
|
|
107
|
+
raise ValueError(f"Chinese numerals support {MIN}–{MAX:,}, got {num}")
|
|
108
|
+
|
|
109
|
+
wan = num // 10_000
|
|
110
|
+
rest = num % 10_000
|
|
111
|
+
|
|
112
|
+
if wan == 0:
|
|
113
|
+
return _encode_section(rest)
|
|
114
|
+
|
|
115
|
+
result = _encode_section(wan) + "万"
|
|
116
|
+
|
|
117
|
+
if rest == 0:
|
|
118
|
+
return result
|
|
119
|
+
# bridge 零 when rest has a missing thousands digit
|
|
120
|
+
if rest < 1_000 or rest // 100 == 0:
|
|
121
|
+
return result + "零" + _encode_section(rest)
|
|
122
|
+
return result + _encode_section(rest)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def from_chinese(s: str) -> int:
|
|
126
|
+
"""
|
|
127
|
+
Convert a Traditional Chinese numeral string to an integer.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
s: Chinese numeral string, e.g. '四十二'.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Integer value.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
TypeError: If s is not a str.
|
|
137
|
+
ValueError: If s is empty or malformed.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
>>> from_chinese('零')
|
|
141
|
+
0
|
|
142
|
+
>>> from_chinese('四十二')
|
|
143
|
+
42
|
|
144
|
+
>>> from_chinese('一万零一')
|
|
145
|
+
10001
|
|
146
|
+
>>> from_chinese('九千九百九十九万九千九百九十九')
|
|
147
|
+
99999999
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(s, str):
|
|
150
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
151
|
+
s = s.strip()
|
|
152
|
+
if not s:
|
|
153
|
+
raise ValueError("Empty string")
|
|
154
|
+
if s == "零":
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
if "万" in s:
|
|
158
|
+
idx = s.index("万")
|
|
159
|
+
left = s[:idx]
|
|
160
|
+
right = s[idx + 1:] # may start with bridge 零
|
|
161
|
+
return _decode_section(left) * 10_000 + _decode_section(right)
|
|
162
|
+
|
|
163
|
+
return _decode_section(s)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_valid(s: str) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check whether a string is a valid Chinese numeral.
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
>>> is_valid('四十二')
|
|
172
|
+
True
|
|
173
|
+
>>> is_valid('hello')
|
|
174
|
+
False
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
val = from_chinese(s)
|
|
178
|
+
return to_chinese(val) == s
|
|
179
|
+
except (TypeError, ValueError):
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
tests = [0, 1, 10, 42, 100, 101, 1001, 10001, 20240, 99_999_999]
|
|
185
|
+
for n in tests:
|
|
186
|
+
r = to_chinese(n)
|
|
187
|
+
print(f"{n:<12} → {r:<28} → {from_chinese(r)}")
|
numly/convert.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.convert
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
Universal converter — convert any number between all supported systems.
|
|
5
|
+
|
|
6
|
+
Supported systems
|
|
7
|
+
-----------------
|
|
8
|
+
'decimal' Standard base-10 range: 0 – unlimited
|
|
9
|
+
'roman' Roman numerals range: 1 – 3,999
|
|
10
|
+
'arabic_indic' Eastern Arabic digits range: 0 – unlimited
|
|
11
|
+
'chinese' Chinese numerals range: 0 – 99,999,999
|
|
12
|
+
'greek' Greek alphabetic range: 1 – 9,999
|
|
13
|
+
'egyptian' Egyptian hieroglyphs range: 1 – 9,999,999
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
>>> from numly.convert import convert, to_all
|
|
17
|
+
>>> convert(42, "decimal", "roman")
|
|
18
|
+
'XLII'
|
|
19
|
+
>>> convert("MMXXIV", "roman", "chinese")
|
|
20
|
+
'二千零二十四'
|
|
21
|
+
>>> convert("四十二", "chinese", "greek")
|
|
22
|
+
'ΜΒʹ'
|
|
23
|
+
>>> convert("𓎆𓎆𓎆𓎆𓏺𓏺", "egyptian", "roman")
|
|
24
|
+
'XLII'
|
|
25
|
+
>>> to_all(42)
|
|
26
|
+
{'decimal': 42, 'roman': 'XLII', 'arabic_indic': '٤٢', ...}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from numly.roman import to_roman, from_roman
|
|
30
|
+
from numly.arabic_indic import to_arabic_indic, from_arabic_indic
|
|
31
|
+
from numly.chinese import to_chinese, from_chinese
|
|
32
|
+
from numly.greek import to_greek, from_greek
|
|
33
|
+
from numly.egyptian import to_egyptian, from_egyptian
|
|
34
|
+
|
|
35
|
+
# ── registry ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
_ENCODERS: dict = {
|
|
38
|
+
"roman": to_roman,
|
|
39
|
+
"arabic_indic": to_arabic_indic,
|
|
40
|
+
"chinese": to_chinese,
|
|
41
|
+
"greek": to_greek,
|
|
42
|
+
"egyptian": to_egyptian,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_DECODERS: dict = {
|
|
46
|
+
"decimal": int,
|
|
47
|
+
"roman": from_roman,
|
|
48
|
+
"arabic_indic": from_arabic_indic,
|
|
49
|
+
"chinese": from_chinese,
|
|
50
|
+
"greek": from_greek,
|
|
51
|
+
"egyptian": from_egyptian,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#: Frozenset of all supported system names (including 'decimal').
|
|
55
|
+
SYSTEMS: frozenset = frozenset(_DECODERS.keys())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── public API ─────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def convert(value, from_system: str, to_system: str):
|
|
61
|
+
"""
|
|
62
|
+
Convert a number from one numeral system to another.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
value: The number to convert.
|
|
66
|
+
Pass an ``int`` when from_system is 'decimal',
|
|
67
|
+
otherwise pass a ``str``.
|
|
68
|
+
from_system: Source system name (see ``SYSTEMS``).
|
|
69
|
+
to_system: Target system name (see ``SYSTEMS``).
|
|
70
|
+
Pass ``'all'`` to receive a dict of every system.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str — converted numeral string.
|
|
74
|
+
int — when to_system is 'decimal'.
|
|
75
|
+
dict — when to_system is 'all'.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If from_system or to_system is unrecognised.
|
|
79
|
+
TypeError / ValueError: Propagated from the individual modules
|
|
80
|
+
if the value is malformed or out of range.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
>>> convert(42, "decimal", "roman")
|
|
84
|
+
'XLII'
|
|
85
|
+
>>> convert("XLII", "roman", "decimal")
|
|
86
|
+
42
|
|
87
|
+
>>> convert("XLII", "roman", "chinese")
|
|
88
|
+
'四十二'
|
|
89
|
+
>>> convert("四十二", "chinese", "egyptian")
|
|
90
|
+
'𓎆𓎆𓎆𓎆𓏺𓏺'
|
|
91
|
+
>>> convert("𓎆𓎆𓎆𓎆𓏺𓏺", "egyptian", "greek")
|
|
92
|
+
'ΜΒʹ'
|
|
93
|
+
>>> convert(42, "decimal", "all")
|
|
94
|
+
{'decimal': 42, 'roman': 'XLII', ...}
|
|
95
|
+
"""
|
|
96
|
+
from_system = from_system.lower().strip()
|
|
97
|
+
if to_system != "all":
|
|
98
|
+
to_system = to_system.lower().strip()
|
|
99
|
+
|
|
100
|
+
if from_system not in _DECODERS:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Unknown source system {from_system!r}. "
|
|
103
|
+
f"Choose from: {sorted(SYSTEMS)}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Step 1 — decode to a plain Python int
|
|
107
|
+
n = _DECODERS[from_system](value)
|
|
108
|
+
|
|
109
|
+
# Step 2 — encode to target
|
|
110
|
+
if to_system == "all":
|
|
111
|
+
return to_all(n)
|
|
112
|
+
if to_system == "decimal":
|
|
113
|
+
return n
|
|
114
|
+
if to_system not in _ENCODERS:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Unknown target system {to_system!r}. "
|
|
117
|
+
f"Choose from: {sorted(SYSTEMS)}"
|
|
118
|
+
)
|
|
119
|
+
return _ENCODERS[to_system](n)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def to_all(value, from_system: str = "decimal") -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Convert a number to every supported numeral system at once.
|
|
125
|
+
|
|
126
|
+
Systems whose range does not include the value are returned as ``None``.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
value: The number to convert.
|
|
130
|
+
from_system: Source system (default ``'decimal'``).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict with keys: 'decimal', 'roman', 'arabic_indic',
|
|
134
|
+
'chinese', 'greek', 'egyptian'.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> to_all(42)
|
|
138
|
+
{'decimal': 42, 'roman': 'XLII', 'arabic_indic': '٤٢',
|
|
139
|
+
'chinese': '四十二', 'greek': 'ΜΒʹ', 'egyptian': '𓎆𓎆𓎆𓎆𓏺𓏺'}
|
|
140
|
+
|
|
141
|
+
>>> to_all(5000)['greek'] # out of range → None
|
|
142
|
+
None
|
|
143
|
+
"""
|
|
144
|
+
from_system = from_system.lower().strip()
|
|
145
|
+
n = _DECODERS[from_system](value)
|
|
146
|
+
|
|
147
|
+
out: dict = {"decimal": n}
|
|
148
|
+
for name, encoder in _ENCODERS.items():
|
|
149
|
+
try:
|
|
150
|
+
out[name] = encoder(n)
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
out[name] = None # value out of range for that system
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def supported_systems() -> list[str]:
|
|
157
|
+
"""Return a sorted list of all supported system names."""
|
|
158
|
+
return sorted(SYSTEMS)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
samples = [1, 42, 100, 2024, 3999]
|
|
163
|
+
|
|
164
|
+
print(f"{'n':<8} {'roman':<12} {'arabic_indic':<14} {'chinese':<22} {'greek':<10} {'egyptian'}")
|
|
165
|
+
print("─" * 90)
|
|
166
|
+
for n in samples:
|
|
167
|
+
r = to_all(n)
|
|
168
|
+
print(
|
|
169
|
+
f"{r['decimal']:<8}"
|
|
170
|
+
f"{str(r['roman']):<12}"
|
|
171
|
+
f"{str(r['arabic_indic']):<14}"
|
|
172
|
+
f"{str(r['chinese']):<22}"
|
|
173
|
+
f"{str(r['greek']):<10}"
|
|
174
|
+
f"{r['egyptian']}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
print()
|
|
178
|
+
cross = [
|
|
179
|
+
("MMXXIV", "roman", "chinese"),
|
|
180
|
+
("四十二", "chinese", "egyptian"),
|
|
181
|
+
("ΜΒʹ", "greek", "roman"),
|
|
182
|
+
("𓎆𓎆𓎆𓎆𓏺𓏺", "egyptian", "arabic_indic"),
|
|
183
|
+
("٤٢", "arabic_indic", "greek"),
|
|
184
|
+
]
|
|
185
|
+
print("Cross-system conversions:")
|
|
186
|
+
for val, frm, to in cross:
|
|
187
|
+
print(f" {val!r:<28} ({frm:<13}) → {to:<13} : {convert(val, frm, to)}")
|
numly/egyptian.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.egyptian
|
|
3
|
+
~~~~~~~~~~~~~~
|
|
4
|
+
Convert integers to Egyptian hieroglyphic numerals and back.
|
|
5
|
+
|
|
6
|
+
Pure additive system — no subtraction (unlike Roman).
|
|
7
|
+
Symbols are written largest to smallest, left to right.
|
|
8
|
+
|
|
9
|
+
𓁨 = 1,000,000 (Heh — god with arms raised)
|
|
10
|
+
𓆐 = 100,000 (tadpole / frog)
|
|
11
|
+
𓂭 = 10,000 (bent finger)
|
|
12
|
+
𓆼 = 1,000 (lotus flower)
|
|
13
|
+
𓍢 = 100 (coil of rope)
|
|
14
|
+
𓎆 = 10 (heel bone / hobble)
|
|
15
|
+
𓏺 = 1 (vertical stroke / tally)
|
|
16
|
+
|
|
17
|
+
Range: 1 – 9,999,999
|
|
18
|
+
|
|
19
|
+
Note: Rendering requires a Unicode font that supports the
|
|
20
|
+
Egyptian Hieroglyphs block (U+13000–U+1342F), such as
|
|
21
|
+
Noto Sans Egyptian Hieroglyphs.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> from numly.egyptian import to_egyptian, from_egyptian
|
|
25
|
+
>>> to_egyptian(42)
|
|
26
|
+
'𓎆𓎆𓎆𓎆𓏺𓏺'
|
|
27
|
+
>>> from_egyptian('𓎆𓎆𓎆𓎆𓏺𓏺')
|
|
28
|
+
42
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_ENC: list[tuple[int, str]] = [
|
|
32
|
+
(1_000_000, "𓁨"), # Heh
|
|
33
|
+
(100_000, "𓆐"), # tadpole
|
|
34
|
+
(10_000, "𓂭"), # bent finger
|
|
35
|
+
(1_000, "𓆼"), # lotus
|
|
36
|
+
(100, "𓍢"), # rope coil
|
|
37
|
+
(10, "𓎆"), # heel bone
|
|
38
|
+
(1, "𓏺"), # tally stroke
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
_DEC: dict[str, int] = {sym: val for val, sym in _ENC}
|
|
42
|
+
|
|
43
|
+
SYSTEM = "egyptian"
|
|
44
|
+
MIN, MAX = 1, 9_999_999
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_egyptian(num: int) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Convert an integer to an Egyptian hieroglyphic numeral string.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
num: Positive integer between 1 and 9,999,999.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Hieroglyphic numeral string, e.g. '𓎆𓎆𓎆𓎆𓏺𓏺'.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
TypeError: If num is not an int.
|
|
59
|
+
ValueError: If num is outside 1–9,999,999.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
>>> to_egyptian(1)
|
|
63
|
+
'𓏺'
|
|
64
|
+
>>> to_egyptian(42)
|
|
65
|
+
'𓎆𓎆𓎆𓎆𓏺𓏺'
|
|
66
|
+
>>> to_egyptian(1234)
|
|
67
|
+
'𓆼𓍢𓍢𓎆𓎆𓎆𓏺𓏺𓏺𓏺'
|
|
68
|
+
>>> to_egyptian(1000000)
|
|
69
|
+
'𓁨'
|
|
70
|
+
"""
|
|
71
|
+
if not isinstance(num, int):
|
|
72
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
73
|
+
if not MIN <= num <= MAX:
|
|
74
|
+
raise ValueError(f"Egyptian numerals support {MIN}–{MAX:,}, got {num}")
|
|
75
|
+
|
|
76
|
+
result = ""
|
|
77
|
+
for val, sym in _ENC:
|
|
78
|
+
count, num = divmod(num, val)
|
|
79
|
+
result += sym * count
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def from_egyptian(s: str) -> int:
|
|
84
|
+
"""
|
|
85
|
+
Convert an Egyptian hieroglyphic numeral string to an integer.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
s: Hieroglyphic string, e.g. '𓎆𓎆𓎆𓎆𓏺𓏺'.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Integer value.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
TypeError: If s is not a str.
|
|
95
|
+
ValueError: If s contains unknown hieroglyphs or is empty.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> from_egyptian('𓏺')
|
|
99
|
+
1
|
|
100
|
+
>>> from_egyptian('𓎆𓎆𓎆𓎆𓏺𓏺')
|
|
101
|
+
42
|
|
102
|
+
>>> from_egyptian('𓆼𓍢𓍢𓎆𓎆𓎆𓏺𓏺𓏺𓏺')
|
|
103
|
+
1234
|
|
104
|
+
"""
|
|
105
|
+
if not isinstance(s, str):
|
|
106
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
107
|
+
s = s.strip()
|
|
108
|
+
if not s:
|
|
109
|
+
raise ValueError("Empty string")
|
|
110
|
+
|
|
111
|
+
result = 0
|
|
112
|
+
for ch in s:
|
|
113
|
+
if ch not in _DEC:
|
|
114
|
+
raise ValueError(f"Unknown Egyptian hieroglyph: {ch!r}")
|
|
115
|
+
result += _DEC[ch]
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def symbol_breakdown(num: int) -> dict[str, int]:
|
|
120
|
+
"""
|
|
121
|
+
Return a breakdown of how many of each symbol are used.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
num: Positive integer between 1 and 9,999,999.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict mapping symbol → count (only non-zero entries).
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
>>> symbol_breakdown(1234)
|
|
131
|
+
{'𓆼': 1, '𓍢': 2, '𓎆': 3, '𓏺': 4}
|
|
132
|
+
"""
|
|
133
|
+
result = {}
|
|
134
|
+
for val, sym in _ENC:
|
|
135
|
+
count, num = divmod(num, val)
|
|
136
|
+
if count:
|
|
137
|
+
result[sym] = count
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def is_valid(s: str) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Check whether a string is a valid Egyptian hieroglyphic numeral.
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
>>> is_valid('𓎆𓎆𓏺𓏺')
|
|
147
|
+
True
|
|
148
|
+
>>> is_valid('hello')
|
|
149
|
+
False
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
from_egyptian(s)
|
|
153
|
+
return True
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
tests = [1, 7, 10, 42, 100, 1234, 9999, 1_000_000]
|
|
160
|
+
for n in tests:
|
|
161
|
+
r = to_egyptian(n)
|
|
162
|
+
bd = symbol_breakdown(n)
|
|
163
|
+
print(f"{n:<10} → {r} breakdown: {bd}")
|
numly/greek.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.greek
|
|
3
|
+
~~~~~~~~~~~
|
|
4
|
+
Convert integers to Greek alphabetic (Milesian) numerals and back.
|
|
5
|
+
|
|
6
|
+
The Milesian system assigns numeric values to Greek letters:
|
|
7
|
+
|
|
8
|
+
Units (1–9) : Α Β Γ Δ Ε Ϛ Ζ Η Θ
|
|
9
|
+
Tens (10–90): Ι Κ Λ Μ Ν Ξ Ο Π Ϙ
|
|
10
|
+
Hundreds(100–900): Ρ Σ Τ Υ Φ Χ Ψ Ω Ϡ
|
|
11
|
+
Thousands (1000–9000): ͵Α ͵Β ͵Γ ͵Δ ͵Ε ͵Ϛ ͵Ζ ͵Η ͵Θ
|
|
12
|
+
|
|
13
|
+
A keraia (ʹ) follows the numeral to distinguish it from words.
|
|
14
|
+
The lower numeral sign ͵ (U+0375) precedes the letter for thousands.
|
|
15
|
+
|
|
16
|
+
Range: 1 – 9,999
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
>>> from numly.greek import to_greek, from_greek
|
|
20
|
+
>>> to_greek(42)
|
|
21
|
+
'ΜΒʹ'
|
|
22
|
+
>>> from_greek('͵ΒΚΔʹ')
|
|
23
|
+
2024
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ── lookup tables ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
_UNITS = ["", "Α", "Β", "Γ", "Δ", "Ε", "Ϛ", "Ζ", "Η", "Θ"]
|
|
29
|
+
_TENS = ["", "Ι", "Κ", "Λ", "Μ", "Ν", "Ξ", "Ο", "Π", "Ϙ"]
|
|
30
|
+
_HUNDS = ["", "Ρ", "Σ", "Τ", "Υ", "Φ", "Χ", "Ψ", "Ω", "Ϡ"]
|
|
31
|
+
_THOU = ["", "͵Α", "͵Β", "͵Γ", "͵Δ", "͵Ε", "͵Ϛ", "͵Ζ", "͵Η", "͵Θ"]
|
|
32
|
+
|
|
33
|
+
_KERAIA = "ʹ" # U+02B9 MODIFIER LETTER PRIME
|
|
34
|
+
_NUMERAL_SIGN = "͵" # U+0375 GREEK LOWER NUMERAL SIGN
|
|
35
|
+
|
|
36
|
+
# Reverse map: Greek letter → integer value
|
|
37
|
+
_VALUE: dict[str, int] = {}
|
|
38
|
+
for _i, _ch in enumerate(_UNITS[1:], 1): _VALUE[_ch] = _i
|
|
39
|
+
for _i, _ch in enumerate(_TENS[1:], 1): _VALUE[_ch] = _i * 10
|
|
40
|
+
for _i, _ch in enumerate(_HUNDS[1:], 1): _VALUE[_ch] = _i * 100
|
|
41
|
+
|
|
42
|
+
SYSTEM = "greek"
|
|
43
|
+
MIN, MAX = 1, 9_999
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── public API ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def to_greek(num: int) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Convert an integer to a Greek alphabetic numeral string.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
num: Positive integer between 1 and 9,999.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Greek numeral string with trailing keraia, e.g. 'ΜΒʹ'.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
TypeError: If num is not an int.
|
|
60
|
+
ValueError: If num is outside 1–9999.
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
>>> to_greek(1)
|
|
64
|
+
'Αʹ'
|
|
65
|
+
>>> to_greek(42)
|
|
66
|
+
'ΜΒʹ'
|
|
67
|
+
>>> to_greek(999)
|
|
68
|
+
'ϠϘΘʹ'
|
|
69
|
+
>>> to_greek(2024)
|
|
70
|
+
'͵ΒΚΔʹ'
|
|
71
|
+
>>> to_greek(9999)
|
|
72
|
+
'͵ΘϠϘΘʹ'
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(num, int):
|
|
75
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
76
|
+
if not MIN <= num <= MAX:
|
|
77
|
+
raise ValueError(f"Greek numerals support {MIN}–{MAX}, got {num}")
|
|
78
|
+
|
|
79
|
+
t = num // 1000
|
|
80
|
+
h = (num % 1000) // 100
|
|
81
|
+
d = (num % 100) // 10
|
|
82
|
+
u = num % 10
|
|
83
|
+
|
|
84
|
+
return _THOU[t] + _HUNDS[h] + _TENS[d] + _UNITS[u] + _KERAIA
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def from_greek(s: str) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Convert a Greek alphabetic numeral string to an integer.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
s: Greek numeral string, with or without keraia, e.g. 'ΜΒʹ'.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Integer value.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
TypeError: If s is not a str.
|
|
99
|
+
ValueError: If s contains unknown characters or is malformed.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
>>> from_greek('ΜΒʹ')
|
|
103
|
+
42
|
|
104
|
+
>>> from_greek('͵ΒΚΔʹ')
|
|
105
|
+
2024
|
|
106
|
+
>>> from_greek('Αʹ')
|
|
107
|
+
1
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(s, str):
|
|
110
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
111
|
+
s = s.rstrip(_KERAIA).strip()
|
|
112
|
+
if not s:
|
|
113
|
+
raise ValueError("Empty string")
|
|
114
|
+
|
|
115
|
+
result, i = 0, 0
|
|
116
|
+
while i < len(s):
|
|
117
|
+
ch = s[i]
|
|
118
|
+
if ch == _NUMERAL_SIGN: # thousands prefix ͵
|
|
119
|
+
i += 1
|
|
120
|
+
if i >= len(s) or s[i] not in _VALUE:
|
|
121
|
+
raise ValueError(f"Invalid Greek numeral: missing letter after ͵")
|
|
122
|
+
result += _VALUE[s[i]] * 1000
|
|
123
|
+
elif ch in _VALUE:
|
|
124
|
+
result += _VALUE[ch]
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"Unknown Greek numeral character: {ch!r}")
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_valid(s: str) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Check whether a string is a valid Greek alphabetic numeral.
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
>>> is_valid('ΜΒʹ')
|
|
138
|
+
True
|
|
139
|
+
>>> is_valid('hello')
|
|
140
|
+
False
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
return to_greek(from_greek(s)) == s.strip()
|
|
144
|
+
except (TypeError, ValueError):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
tests = [1, 6, 9, 10, 42, 90, 100, 399, 999, 1000, 2024, 9999]
|
|
150
|
+
for n in tests:
|
|
151
|
+
r = to_greek(n)
|
|
152
|
+
print(f"{n:<8} → {r:<12} → {from_greek(r)}")
|
numly/roman.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.roman
|
|
3
|
+
~~~~~~~~~~~
|
|
4
|
+
Convert integers to Roman numerals and back.
|
|
5
|
+
|
|
6
|
+
Range: 1 – 3,999
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
>>> from numly.roman import to_roman, from_roman
|
|
10
|
+
>>> to_roman(2024)
|
|
11
|
+
'MMXXIV'
|
|
12
|
+
>>> from_roman('XLII')
|
|
13
|
+
42
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
_ENC = [
|
|
17
|
+
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
|
|
18
|
+
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
|
|
19
|
+
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
|
20
|
+
]
|
|
21
|
+
_DEC = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
|
|
22
|
+
|
|
23
|
+
SYSTEM = "roman"
|
|
24
|
+
MIN, MAX = 1, 3_999
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_roman(num: int) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Convert an integer to a Roman numeral string.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
num: Positive integer between 1 and 3,999.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Roman numeral string, e.g. 'XLII'.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError: If num is not an int.
|
|
39
|
+
ValueError: If num is outside 1–3999.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
>>> to_roman(42)
|
|
43
|
+
'XLII'
|
|
44
|
+
>>> to_roman(2024)
|
|
45
|
+
'MMXXIV'
|
|
46
|
+
>>> to_roman(3999)
|
|
47
|
+
'MMMCMXCIX'
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(num, int):
|
|
50
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
51
|
+
if not MIN <= num <= MAX:
|
|
52
|
+
raise ValueError(f"Roman numerals support {MIN}–{MAX}, got {num}")
|
|
53
|
+
|
|
54
|
+
result = ""
|
|
55
|
+
for val, sym in _ENC:
|
|
56
|
+
while num >= val:
|
|
57
|
+
result += sym
|
|
58
|
+
num -= val
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def from_roman(s: str) -> int:
|
|
63
|
+
"""
|
|
64
|
+
Convert a Roman numeral string to an integer.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
s: Roman numeral string (case-insensitive), e.g. 'xlii'.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Integer value.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
TypeError: If s is not a str.
|
|
74
|
+
ValueError: If s contains invalid characters or is empty.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
>>> from_roman('XLII')
|
|
78
|
+
42
|
|
79
|
+
>>> from_roman('mmxxiv')
|
|
80
|
+
2024
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(s, str):
|
|
83
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
84
|
+
s = s.upper().strip()
|
|
85
|
+
if not s:
|
|
86
|
+
raise ValueError("Empty string")
|
|
87
|
+
for ch in s:
|
|
88
|
+
if ch not in _DEC:
|
|
89
|
+
raise ValueError(f"Invalid Roman numeral character: {ch!r}")
|
|
90
|
+
|
|
91
|
+
result, prev = 0, 0
|
|
92
|
+
for ch in reversed(s):
|
|
93
|
+
v = _DEC[ch]
|
|
94
|
+
result = result - v if v < prev else result + v
|
|
95
|
+
prev = v
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_valid(s: str) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Check whether a string is a valid Roman numeral.
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
>>> is_valid('XIV')
|
|
105
|
+
True
|
|
106
|
+
>>> is_valid('ABC')
|
|
107
|
+
False
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
return to_roman(from_roman(s)).upper() == s.upper().strip()
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
for n in [1, 4, 9, 42, 399, 2024, 3999]:
|
|
117
|
+
r = to_roman(n)
|
|
118
|
+
print(f"{n:<8} → {r:<14} → {from_roman(r)}")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: numly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert numbers across numeral systems — Roman, Chinese, Greek, Egyptian, Arabic-Indic and more.
|
|
5
|
+
Home-page: https://github.com/TheMadrasTechie/numly
|
|
6
|
+
Author: TheMadrasTechie
|
|
7
|
+
Author-email: you@example.com
|
|
8
|
+
Keywords: numerals,roman,chinese,greek,egyptian,arabic-indic,number,converter
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: keywords
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="numly_rounded_square.svg" alt="numly logo" width="120" height="120"/>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<h1 align="center">numly</h1>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="https://img.shields.io/pypi/v/numly" alt="PyPI version"/>
|
|
32
|
+
<img src="https://img.shields.io/pypi/pyversions/numly" alt="Python versions"/>
|
|
33
|
+
<img src="https://img.shields.io/github/license/yourusername/numly" alt="License"/>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
A Python library to work with numbers across formats — decimal, roman, words, binary, hex, and more.
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install numly
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import numly
|
|
54
|
+
|
|
55
|
+
# Convert to Roman numerals
|
|
56
|
+
numly.to_roman(2024) # → 'MMXXIV'
|
|
57
|
+
|
|
58
|
+
# Convert to words
|
|
59
|
+
numly.to_words(42) # → 'forty-two'
|
|
60
|
+
|
|
61
|
+
# Convert to binary
|
|
62
|
+
numly.to_binary(255) # → '11111111'
|
|
63
|
+
|
|
64
|
+
# Convert to hex
|
|
65
|
+
numly.to_hex(255) # → 'FF'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- 🔢 Decimal → Roman numerals
|
|
73
|
+
- 🔤 Decimal → Words (English)
|
|
74
|
+
- 💻 Decimal → Binary / Octal / Hex
|
|
75
|
+
- 🔁 Reverse conversions (Roman → Decimal, etc.)
|
|
76
|
+
- 🌍 Locale-aware number formatting *(coming soon)*
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
| Function | Input | Output |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `to_roman(n)` | `int` | `str` — e.g. `'XIV'` |
|
|
85
|
+
| `from_roman(s)` | `str` | `int` — e.g. `14` |
|
|
86
|
+
| `to_words(n)` | `int` | `str` — e.g. `'forty-two'` |
|
|
87
|
+
| `to_binary(n)` | `int` | `str` — e.g. `'1110'` |
|
|
88
|
+
| `to_octal(n)` | `int` | `str` — e.g. `'16'` |
|
|
89
|
+
| `to_hex(n)` | `int` | `str` — e.g. `'E'` |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
Contributions are welcome! Feel free to open an issue or submit a pull request.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/yourusername/numly.git
|
|
99
|
+
cd numly
|
|
100
|
+
pip install -e .
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT License © 2026 TheMadrasTechie
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
numly/__init__.py,sha256=Em-_1edoHiwNc78ZuPiLCuSIMlol87DtxsRtWlgXUAQ,2769
|
|
2
|
+
numly/arabic_indic.py,sha256=9kGWCT7bz71MuWKUhw_yxIyFwz9L19DrgEHSI4Zei8E,2855
|
|
3
|
+
numly/chinese.py,sha256=0JSUhfj0ryMca4uIRYDZG8-ix9fBw0s69I4Z9_57wpI,5150
|
|
4
|
+
numly/convert.py,sha256=tIrU4gfFMe2F7Nt5iWZzprCykoYA1jFlpceaR6DtIu0,6400
|
|
5
|
+
numly/egyptian.py,sha256=loWwpHi-gaQIV5d7mELK8d4QiyHstGxWM67Wnr9u8fQ,4238
|
|
6
|
+
numly/greek.py,sha256=pENWN19_Z_ZuahPCV1Skg7smjiyrIp9f-RB08-jhK-M,4493
|
|
7
|
+
numly/roman.py,sha256=sBxrO0idsefcB4RNGtG-Z6kuYhKmBYogX1fFOT5I_yU,2733
|
|
8
|
+
numly-0.1.0.dist-info/METADATA,sha256=DrW-cd69PeFHKrsC9o056yeLIjsDj3QhHycss9WuQXE,2602
|
|
9
|
+
numly-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
numly-0.1.0.dist-info/top_level.txt,sha256=ob_L0EynydpPFHV5aX-Be18Kim6Y1uQ5QMyYom5K6fQ,6
|
|
11
|
+
numly-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numly
|