numly 0.2.0__tar.gz → 0.2.1__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.
- {numly-0.2.0 → numly-0.2.1}/PKG-INFO +1 -1
- numly-0.2.1/numly/__init__.py +97 -0
- numly-0.2.1/numly/babylonian.py +157 -0
- numly-0.2.1/numly/base.py +284 -0
- numly-0.2.1/numly/mayan.py +202 -0
- numly-0.2.1/numly/tamil.py +189 -0
- numly-0.2.1/numly/words.py +207 -0
- {numly-0.2.0 → numly-0.2.1}/numly.egg-info/PKG-INFO +1 -1
- {numly-0.2.0 → numly-0.2.1}/numly.egg-info/SOURCES.txt +5 -0
- {numly-0.2.0 → numly-0.2.1}/setup.py +1 -1
- numly-0.2.0/numly/__init__.py +0 -64
- {numly-0.2.0 → numly-0.2.1}/README.md +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/arabic_indic.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/chinese.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/convert.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/egyptian.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/greek.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly/roman.py +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly.egg-info/dependency_links.txt +0 -0
- {numly-0.2.0 → numly-0.2.1}/numly.egg-info/top_level.txt +0 -0
- {numly-0.2.0 → numly-0.2.1}/setup.cfg +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
tamil : Ancient Tamil numerals (௧ ௨ ௩ … ௰ ௱ ௲)
|
|
15
|
+
babylonian : Babylonian cuneiform (𒁹 𒌋 — base 60)
|
|
16
|
+
mayan : Mayan vigesimal (𝋠 𝋡 𝋢 … 𝋳 — base 20)
|
|
17
|
+
binary : Base-2
|
|
18
|
+
octal : Base-8
|
|
19
|
+
hex : Base-16
|
|
20
|
+
words : English words (Western & Indian systems)
|
|
21
|
+
|
|
22
|
+
Quick start
|
|
23
|
+
-----------
|
|
24
|
+
import numly
|
|
25
|
+
|
|
26
|
+
numly.to_roman(2024) # 'MMXXIV'
|
|
27
|
+
numly.to_chinese(42) # '四十二'
|
|
28
|
+
numly.to_tamil(42) # '௪௰௨'
|
|
29
|
+
numly.to_babylonian(42) # '𒌋𒌋𒌋𒌋𒁹𒁹'
|
|
30
|
+
numly.to_mayan(42) # '𝋂 𝋂'
|
|
31
|
+
numly.to_binary(42) # '101010'
|
|
32
|
+
numly.to_hex(255) # 'FF'
|
|
33
|
+
numly.to_words(1_234_567) # 'one million two hundred...'
|
|
34
|
+
numly.to_words(1_234_567, 'indian') # 'twelve lakh thirty four thousand...'
|
|
35
|
+
numly.convert("MMXXIV", "roman", "greek") # '͵ΒΚΔʹ'
|
|
36
|
+
numly.to_all(42) # dict of every system
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__version__ = "0.2.0"
|
|
40
|
+
__author__ = "TheMadrasTechie"
|
|
41
|
+
__license__ = "MIT"
|
|
42
|
+
|
|
43
|
+
# ── numeral systems ────────────────────────────────────────────────────────
|
|
44
|
+
from numly.roman import to_roman, from_roman, is_valid as is_valid_roman
|
|
45
|
+
from numly.arabic_indic import to_arabic_indic, from_arabic_indic, is_valid as is_valid_arabic_indic
|
|
46
|
+
from numly.chinese import to_chinese, from_chinese, is_valid as is_valid_chinese
|
|
47
|
+
from numly.greek import to_greek, from_greek, is_valid as is_valid_greek
|
|
48
|
+
from numly.egyptian import to_egyptian, from_egyptian, is_valid as is_valid_egyptian
|
|
49
|
+
from numly.egyptian import symbol_breakdown
|
|
50
|
+
from numly.tamil import to_tamil, from_tamil, is_valid as is_valid_tamil
|
|
51
|
+
from numly.babylonian import to_babylonian, from_babylonian, is_valid as is_valid_babylonian
|
|
52
|
+
from numly.mayan import to_mayan, from_mayan, is_valid as is_valid_mayan
|
|
53
|
+
from numly.mayan import to_mayan_text
|
|
54
|
+
|
|
55
|
+
# ── base conversions ───────────────────────────────────────────────────────
|
|
56
|
+
from numly.base import (
|
|
57
|
+
to_binary, from_binary,
|
|
58
|
+
to_octal, from_octal,
|
|
59
|
+
to_hex, from_hex,
|
|
60
|
+
to_base, from_base,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# ── english words ──────────────────────────────────────────────────────────
|
|
64
|
+
from numly.words import to_words, to_words_western, to_words_indian
|
|
65
|
+
|
|
66
|
+
# ── universal converter ────────────────────────────────────────────────────
|
|
67
|
+
from numly.convert import convert, to_all, SYSTEMS, supported_systems
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
# roman
|
|
71
|
+
"to_roman", "from_roman", "is_valid_roman",
|
|
72
|
+
# arabic-indic
|
|
73
|
+
"to_arabic_indic", "from_arabic_indic", "is_valid_arabic_indic",
|
|
74
|
+
# chinese
|
|
75
|
+
"to_chinese", "from_chinese", "is_valid_chinese",
|
|
76
|
+
# greek
|
|
77
|
+
"to_greek", "from_greek", "is_valid_greek",
|
|
78
|
+
# egyptian
|
|
79
|
+
"to_egyptian", "from_egyptian", "is_valid_egyptian", "symbol_breakdown",
|
|
80
|
+
# tamil
|
|
81
|
+
"to_tamil", "from_tamil", "is_valid_tamil",
|
|
82
|
+
# babylonian
|
|
83
|
+
"to_babylonian", "from_babylonian", "is_valid_babylonian",
|
|
84
|
+
# mayan
|
|
85
|
+
"to_mayan", "from_mayan", "is_valid_mayan", "to_mayan_text",
|
|
86
|
+
# base conversions
|
|
87
|
+
"to_binary", "from_binary",
|
|
88
|
+
"to_octal", "from_octal",
|
|
89
|
+
"to_hex", "from_hex",
|
|
90
|
+
"to_base", "from_base",
|
|
91
|
+
# words
|
|
92
|
+
"to_words", "to_words_western", "to_words_indian",
|
|
93
|
+
# universal
|
|
94
|
+
"convert", "to_all", "SYSTEMS", "supported_systems",
|
|
95
|
+
# meta
|
|
96
|
+
"__version__",
|
|
97
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.babylonian
|
|
3
|
+
~~~~~~~~~~~~~~~~
|
|
4
|
+
Convert integers to Babylonian cuneiform numerals and back.
|
|
5
|
+
|
|
6
|
+
Babylonian mathematics used a base-60 (sexagesimal) positional system.
|
|
7
|
+
Within each "digit" (0–59), values are written additively:
|
|
8
|
+
𒁹 = 1 (vertical wedge) repeated up to 9×
|
|
9
|
+
𒌋 = 10 (Winkelhaken / corner) repeated up to 5×
|
|
10
|
+
|
|
11
|
+
Digits are separated by a space. The system has no true zero
|
|
12
|
+
(a blank position is represented by the placeholder 𒑱).
|
|
13
|
+
|
|
14
|
+
Unicode block: Cuneiform (U+12000–U+123FF)
|
|
15
|
+
|
|
16
|
+
Range: 1 – 216,000 (60³ — three sexagesimal digits)
|
|
17
|
+
|
|
18
|
+
Examples
|
|
19
|
+
--------
|
|
20
|
+
>>> from numly.babylonian import to_babylonian, from_babylonian
|
|
21
|
+
>>> to_babylonian(42)
|
|
22
|
+
'𒌋𒌋𒌋𒌋𒁹𒁹'
|
|
23
|
+
>>> to_babylonian(61)
|
|
24
|
+
'𒁹 𒁹'
|
|
25
|
+
>>> from_babylonian('𒁹 𒁹')
|
|
26
|
+
61
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_ONE = "𒁹" # U+12079 CUNEIFORM SIGN DIŠ (1)
|
|
30
|
+
_TEN = "𒌋" # U+1230B CUNEIFORM SIGN U (10)
|
|
31
|
+
_ZERO = "𒑱" # U+12471 CUNEIFORM PUNCTUATION MARK STACKED DISHES (placeholder 0)
|
|
32
|
+
_SEP = " " # digit separator
|
|
33
|
+
|
|
34
|
+
SYSTEM = "babylonian"
|
|
35
|
+
BASE = 60
|
|
36
|
+
MIN, MAX = 1, BASE ** 3 # 1 – 216,000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _encode_digit(n: int) -> str:
|
|
40
|
+
"""Encode one sexagesimal digit (0–59) to cuneiform."""
|
|
41
|
+
if n == 0:
|
|
42
|
+
return _ZERO
|
|
43
|
+
tens = n // 10
|
|
44
|
+
ones = n % 10
|
|
45
|
+
return _TEN * tens + _ONE * ones
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _decode_digit(s: str) -> int:
|
|
49
|
+
"""Decode one sexagesimal digit string to integer."""
|
|
50
|
+
if s == _ZERO:
|
|
51
|
+
return 0
|
|
52
|
+
result = 0
|
|
53
|
+
for ch in s:
|
|
54
|
+
if ch == _TEN:
|
|
55
|
+
result += 10
|
|
56
|
+
elif ch == _ONE:
|
|
57
|
+
result += 1
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"Unknown cuneiform character: {ch!r}")
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def to_babylonian(num: int) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Convert an integer to a Babylonian cuneiform numeral string.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
num: Positive integer between 1 and 216,000.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Cuneiform string with sexagesimal digits separated by spaces,
|
|
72
|
+
e.g. '𒌋𒌋𒌋𒌋𒁹𒁹' (42) or '𒁹 𒁹' (61).
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
TypeError: If num is not an int.
|
|
76
|
+
ValueError: If num is outside 1–216,000.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> to_babylonian(1)
|
|
80
|
+
'𒁹'
|
|
81
|
+
>>> to_babylonian(10)
|
|
82
|
+
'𒌋'
|
|
83
|
+
>>> to_babylonian(42)
|
|
84
|
+
'𒌋𒌋𒌋𒌋𒁹𒁹'
|
|
85
|
+
>>> to_babylonian(60)
|
|
86
|
+
'𒁹 𒑱'
|
|
87
|
+
>>> to_babylonian(61)
|
|
88
|
+
'𒁹 𒁹'
|
|
89
|
+
>>> to_babylonian(3600)
|
|
90
|
+
'𒁹 𒑱 𒑱'
|
|
91
|
+
"""
|
|
92
|
+
if not isinstance(num, int):
|
|
93
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
94
|
+
if not MIN <= num <= MAX:
|
|
95
|
+
raise ValueError(f"Babylonian numerals support {MIN}–{MAX:,}, got {num}")
|
|
96
|
+
|
|
97
|
+
digits = []
|
|
98
|
+
n = num
|
|
99
|
+
while n:
|
|
100
|
+
digits.append(_encode_digit(n % BASE))
|
|
101
|
+
n //= BASE
|
|
102
|
+
|
|
103
|
+
return _SEP.join(reversed(digits))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def from_babylonian(s: str) -> int:
|
|
107
|
+
"""
|
|
108
|
+
Convert a Babylonian cuneiform numeral string to an integer.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
s: Cuneiform string with digits separated by spaces,
|
|
112
|
+
e.g. '𒁹 𒁹'.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Integer value.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
TypeError: If s is not a str.
|
|
119
|
+
ValueError: If s contains unknown characters or is empty.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
>>> from_babylonian('𒌋𒌋𒌋𒌋𒁹𒁹')
|
|
123
|
+
42
|
|
124
|
+
>>> from_babylonian('𒁹 𒁹')
|
|
125
|
+
61
|
|
126
|
+
>>> from_babylonian('𒁹 𒑱 𒑱')
|
|
127
|
+
3600
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(s, str):
|
|
130
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
131
|
+
s = s.strip()
|
|
132
|
+
if not s:
|
|
133
|
+
raise ValueError("Empty string")
|
|
134
|
+
|
|
135
|
+
parts = s.split(_SEP)
|
|
136
|
+
result = 0
|
|
137
|
+
for part in parts:
|
|
138
|
+
result = result * BASE + _decode_digit(part)
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_valid(s: str) -> bool:
|
|
143
|
+
"""Check whether a string is a valid Babylonian cuneiform numeral."""
|
|
144
|
+
try:
|
|
145
|
+
from_babylonian(s)
|
|
146
|
+
return True
|
|
147
|
+
except (TypeError, ValueError):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── quick demo ─────────────────────────────────────────────────────────────
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
tests = [1, 10, 42, 59, 60, 61, 600, 3600, 3661, 216_000]
|
|
154
|
+
for n in tests:
|
|
155
|
+
r = to_babylonian(n)
|
|
156
|
+
back = from_babylonian(r)
|
|
157
|
+
print(f"{n:<8} → {r:<30} → {back}")
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.base
|
|
3
|
+
~~~~~~~~~~
|
|
4
|
+
Convert integers to Binary, Octal, and Hexadecimal and back.
|
|
5
|
+
|
|
6
|
+
binary : base-2 (digits: 0 1)
|
|
7
|
+
octal : base-8 (digits: 0–7)
|
|
8
|
+
hex : base-16 (digits: 0–9 A–F)
|
|
9
|
+
|
|
10
|
+
Range: 0 – unlimited (all three support arbitrarily large integers)
|
|
11
|
+
|
|
12
|
+
Examples
|
|
13
|
+
--------
|
|
14
|
+
>>> from numly.base import to_binary, to_octal, to_hex
|
|
15
|
+
>>> to_binary(42)
|
|
16
|
+
'101010'
|
|
17
|
+
>>> to_octal(42)
|
|
18
|
+
'52'
|
|
19
|
+
>>> to_hex(42)
|
|
20
|
+
'2A'
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
SYSTEM = "base"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Binary ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
def to_binary(num: int, prefix: bool = False) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Convert an integer to a binary string.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
num: Non-negative integer.
|
|
34
|
+
prefix: If True, prepend '0b'. Default False.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Binary string, e.g. '101010'.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> to_binary(0)
|
|
41
|
+
'0'
|
|
42
|
+
>>> to_binary(42)
|
|
43
|
+
'101010'
|
|
44
|
+
>>> to_binary(42, prefix=True)
|
|
45
|
+
'0b101010'
|
|
46
|
+
>>> to_binary(255)
|
|
47
|
+
'11111111'
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(num, int):
|
|
50
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
51
|
+
if num < 0:
|
|
52
|
+
raise ValueError("Negative numbers are not supported")
|
|
53
|
+
result = bin(num)[2:]
|
|
54
|
+
return ("0b" + result) if prefix else result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def from_binary(s: str) -> int:
|
|
58
|
+
"""
|
|
59
|
+
Convert a binary string to an integer.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
s: Binary string, with or without '0b' prefix.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Integer value.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
>>> from_binary('101010')
|
|
69
|
+
42
|
|
70
|
+
>>> from_binary('0b101010')
|
|
71
|
+
42
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(s, str):
|
|
74
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
75
|
+
s = s.strip().lower().removeprefix("0b")
|
|
76
|
+
if not s:
|
|
77
|
+
raise ValueError("Empty string")
|
|
78
|
+
try:
|
|
79
|
+
return int(s, 2)
|
|
80
|
+
except ValueError:
|
|
81
|
+
raise ValueError(f"Invalid binary string: {s!r}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ── Octal ──────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def to_octal(num: int, prefix: bool = False) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Convert an integer to an octal string.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
num: Non-negative integer.
|
|
92
|
+
prefix: If True, prepend '0o'. Default False.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Octal string, e.g. '52'.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> to_octal(0)
|
|
99
|
+
'0'
|
|
100
|
+
>>> to_octal(42)
|
|
101
|
+
'52'
|
|
102
|
+
>>> to_octal(42, prefix=True)
|
|
103
|
+
'0o52'
|
|
104
|
+
>>> to_octal(255)
|
|
105
|
+
'377'
|
|
106
|
+
"""
|
|
107
|
+
if not isinstance(num, int):
|
|
108
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
109
|
+
if num < 0:
|
|
110
|
+
raise ValueError("Negative numbers are not supported")
|
|
111
|
+
result = oct(num)[2:]
|
|
112
|
+
return ("0o" + result) if prefix else result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def from_octal(s: str) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Convert an octal string to an integer.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
s: Octal string, with or without '0o' prefix.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Integer value.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
>>> from_octal('52')
|
|
127
|
+
42
|
|
128
|
+
>>> from_octal('0o52')
|
|
129
|
+
42
|
|
130
|
+
"""
|
|
131
|
+
if not isinstance(s, str):
|
|
132
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
133
|
+
s = s.strip().lower().removeprefix("0o")
|
|
134
|
+
if not s:
|
|
135
|
+
raise ValueError("Empty string")
|
|
136
|
+
try:
|
|
137
|
+
return int(s, 8)
|
|
138
|
+
except ValueError:
|
|
139
|
+
raise ValueError(f"Invalid octal string: {s!r}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ── Hexadecimal ────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def to_hex(num: int, prefix: bool = False, upper: bool = True) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Convert an integer to a hexadecimal string.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
num: Non-negative integer.
|
|
150
|
+
prefix: If True, prepend '0x'. Default False.
|
|
151
|
+
upper: If True (default), use uppercase A–F.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Hexadecimal string, e.g. '2A'.
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
>>> to_hex(0)
|
|
158
|
+
'0'
|
|
159
|
+
>>> to_hex(42)
|
|
160
|
+
'2A'
|
|
161
|
+
>>> to_hex(255)
|
|
162
|
+
'FF'
|
|
163
|
+
>>> to_hex(255, prefix=True)
|
|
164
|
+
'0xFF'
|
|
165
|
+
>>> to_hex(255, upper=False)
|
|
166
|
+
'ff'
|
|
167
|
+
"""
|
|
168
|
+
if not isinstance(num, int):
|
|
169
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
170
|
+
if num < 0:
|
|
171
|
+
raise ValueError("Negative numbers are not supported")
|
|
172
|
+
result = hex(num)[2:]
|
|
173
|
+
result = result.upper() if upper else result.lower()
|
|
174
|
+
return ("0x" + result) if prefix else result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def from_hex(s: str) -> int:
|
|
178
|
+
"""
|
|
179
|
+
Convert a hexadecimal string to an integer.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
s: Hex string, with or without '0x' prefix (case-insensitive).
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Integer value.
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
>>> from_hex('2A')
|
|
189
|
+
42
|
|
190
|
+
>>> from_hex('0xff')
|
|
191
|
+
255
|
|
192
|
+
>>> from_hex('FF')
|
|
193
|
+
255
|
|
194
|
+
"""
|
|
195
|
+
if not isinstance(s, str):
|
|
196
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
197
|
+
s = s.strip().lower().removeprefix("0x")
|
|
198
|
+
if not s:
|
|
199
|
+
raise ValueError("Empty string")
|
|
200
|
+
try:
|
|
201
|
+
return int(s, 16)
|
|
202
|
+
except ValueError:
|
|
203
|
+
raise ValueError(f"Invalid hexadecimal string: {s!r}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── Generic base converter ─────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
def to_base(num: int, base: int) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Convert an integer to any base between 2 and 36.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
num: Non-negative integer.
|
|
214
|
+
base: Target base (2–36).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
String representation in the given base.
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
>>> to_base(42, 2)
|
|
221
|
+
'101010'
|
|
222
|
+
>>> to_base(42, 16)
|
|
223
|
+
'2A'
|
|
224
|
+
>>> to_base(255, 36)
|
|
225
|
+
'73'
|
|
226
|
+
"""
|
|
227
|
+
if not isinstance(num, int):
|
|
228
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
229
|
+
if num < 0:
|
|
230
|
+
raise ValueError("Negative numbers are not supported")
|
|
231
|
+
if not 2 <= base <= 36:
|
|
232
|
+
raise ValueError(f"Base must be between 2 and 36, got {base}")
|
|
233
|
+
if num == 0:
|
|
234
|
+
return "0"
|
|
235
|
+
|
|
236
|
+
digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
237
|
+
result = ""
|
|
238
|
+
while num:
|
|
239
|
+
result = digits[num % base] + result
|
|
240
|
+
num //= base
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def from_base(s: str, base: int) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Convert a string in any base (2–36) to an integer.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
s: String representation.
|
|
250
|
+
base: Source base (2–36).
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Integer value.
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
>>> from_base('101010', 2)
|
|
257
|
+
42
|
|
258
|
+
>>> from_base('2A', 16)
|
|
259
|
+
42
|
|
260
|
+
"""
|
|
261
|
+
if not isinstance(s, str):
|
|
262
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
263
|
+
try:
|
|
264
|
+
return int(s.strip(), base)
|
|
265
|
+
except ValueError:
|
|
266
|
+
raise ValueError(f"Invalid base-{base} string: {s!r}")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ── quick demo ─────────────────────────────────────────────────────────────
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
tests = [0, 1, 10, 42, 255, 1024, 65535]
|
|
272
|
+
|
|
273
|
+
print(f"{'Decimal':<10} {'Binary':<20} {'Octal':<10} {'Hex'}")
|
|
274
|
+
print("─" * 55)
|
|
275
|
+
for n in tests:
|
|
276
|
+
print(f"{n:<10} {to_binary(n):<20} {to_octal(n):<10} {to_hex(n)}")
|
|
277
|
+
|
|
278
|
+
print()
|
|
279
|
+
print("Round-trip checks:")
|
|
280
|
+
for n in tests:
|
|
281
|
+
assert from_binary(to_binary(n)) == n
|
|
282
|
+
assert from_octal(to_octal(n)) == n
|
|
283
|
+
assert from_hex(to_hex(n)) == n
|
|
284
|
+
print(" All passed ✓")
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.mayan
|
|
3
|
+
~~~~~~~~~~~
|
|
4
|
+
Convert integers to Mayan numerals and back.
|
|
5
|
+
|
|
6
|
+
The Mayan vigesimal (base-20) system uses three symbols:
|
|
7
|
+
• (dot) = 1
|
|
8
|
+
— (bar) = 5
|
|
9
|
+
𝋠 (shell) = 0 (one of the earliest uses of zero in history)
|
|
10
|
+
|
|
11
|
+
Within each "digit" (0–19):
|
|
12
|
+
dots stack vertically above bars
|
|
13
|
+
maximum: 3 bars (15) + 4 dots (4) = 19
|
|
14
|
+
|
|
15
|
+
Digits are written top-to-bottom (highest power first).
|
|
16
|
+
In this text representation, digits are separated by | .
|
|
17
|
+
|
|
18
|
+
Unicode: The Mayan Numerals block (U+1D2C0–U+1D2FF) has combined
|
|
19
|
+
glyphs 𝋠 (0) through 𝋳 (19). numly uses these for compact output.
|
|
20
|
+
|
|
21
|
+
Range: 0 – 7,999 (20³ − 1; three vigesimal digits)
|
|
22
|
+
|
|
23
|
+
Examples
|
|
24
|
+
--------
|
|
25
|
+
>>> from numly.mayan import to_mayan, from_mayan
|
|
26
|
+
>>> to_mayan(0)
|
|
27
|
+
'𝋠'
|
|
28
|
+
>>> to_mayan(19)
|
|
29
|
+
'𝋳'
|
|
30
|
+
>>> to_mayan(20)
|
|
31
|
+
'𝋡 𝋠'
|
|
32
|
+
>>> to_mayan(42)
|
|
33
|
+
'𝋢 𝋡'
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# ── Mayan Numerals Unicode block U+1D2C0–U+1D2E3 ──────────────────────────
|
|
37
|
+
# Each code point represents one complete Mayan digit 0–19.
|
|
38
|
+
|
|
39
|
+
_GLYPHS = [chr(0x1D2C0 + i) for i in range(20)] # 𝋠 𝋡 𝋢 … 𝋳
|
|
40
|
+
_GLYPH_VAL = {g: i for i, g in enumerate(_GLYPHS)}
|
|
41
|
+
|
|
42
|
+
_SEP = " " # digit separator in multi-digit numbers
|
|
43
|
+
|
|
44
|
+
SYSTEM = "mayan"
|
|
45
|
+
BASE = 20
|
|
46
|
+
MIN, MAX = 0, BASE ** 3 - 1 # 0 – 7,999
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def to_mayan(num: int) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Convert an integer to a Mayan numeral string.
|
|
52
|
+
|
|
53
|
+
Uses the Unicode Mayan Numerals block (U+1D2C0–U+1D2E3).
|
|
54
|
+
Multi-digit numbers separate vigesimal digits with a space.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
num: Non-negative integer between 0 and 7,999.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Mayan numeral string, e.g. '𝋢 𝋡' for 42.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TypeError: If num is not an int.
|
|
64
|
+
ValueError: If num is outside 0–7,999.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
>>> to_mayan(0)
|
|
68
|
+
'𝋠'
|
|
69
|
+
>>> to_mayan(1)
|
|
70
|
+
'𝋡'
|
|
71
|
+
>>> to_mayan(5)
|
|
72
|
+
'𝋥'
|
|
73
|
+
>>> to_mayan(19)
|
|
74
|
+
'𝋳'
|
|
75
|
+
>>> to_mayan(20)
|
|
76
|
+
'𝋡 𝋠'
|
|
77
|
+
>>> to_mayan(42)
|
|
78
|
+
'𝋢 𝋡'
|
|
79
|
+
>>> to_mayan(400)
|
|
80
|
+
'𝋡 𝋠 𝋠'
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(num, int):
|
|
83
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
84
|
+
if not MIN <= num <= MAX:
|
|
85
|
+
raise ValueError(f"Mayan numerals support {MIN}–{MAX}, got {num}")
|
|
86
|
+
|
|
87
|
+
if num == 0:
|
|
88
|
+
return _GLYPHS[0]
|
|
89
|
+
|
|
90
|
+
digits = []
|
|
91
|
+
n = num
|
|
92
|
+
while n:
|
|
93
|
+
digits.append(_GLYPHS[n % BASE])
|
|
94
|
+
n //= BASE
|
|
95
|
+
|
|
96
|
+
return _SEP.join(reversed(digits))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def from_mayan(s: str) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Convert a Mayan numeral string to an integer.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
s: Mayan numeral string with digits separated by spaces,
|
|
105
|
+
e.g. '𝋢 𝋡'.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Integer value.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
TypeError: If s is not a str.
|
|
112
|
+
ValueError: If s contains unknown characters.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
>>> from_mayan('𝋠')
|
|
116
|
+
0
|
|
117
|
+
>>> from_mayan('𝋳')
|
|
118
|
+
19
|
|
119
|
+
>>> from_mayan('𝋡 𝋠')
|
|
120
|
+
20
|
|
121
|
+
>>> from_mayan('𝋢 𝋡')
|
|
122
|
+
42
|
|
123
|
+
"""
|
|
124
|
+
if not isinstance(s, str):
|
|
125
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
126
|
+
s = s.strip()
|
|
127
|
+
if not s:
|
|
128
|
+
raise ValueError("Empty string")
|
|
129
|
+
|
|
130
|
+
parts = s.split(_SEP)
|
|
131
|
+
result = 0
|
|
132
|
+
for part in parts:
|
|
133
|
+
if part not in _GLYPH_VAL:
|
|
134
|
+
raise ValueError(f"Unknown Mayan glyph: {part!r}")
|
|
135
|
+
result = result * BASE + _GLYPH_VAL[part]
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def to_mayan_text(num: int) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Convert an integer to a human-readable Mayan text representation.
|
|
142
|
+
|
|
143
|
+
Uses dots (•), bars (━), and shell (◎) for maximum compatibility.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
num: Non-negative integer between 0 and 7,999.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Text representation string, e.g. '••━ | •' for 42.
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
>>> to_mayan_text(0)
|
|
153
|
+
'◎'
|
|
154
|
+
>>> to_mayan_text(7)
|
|
155
|
+
'••━'
|
|
156
|
+
>>> to_mayan_text(19)
|
|
157
|
+
'••••━━━'
|
|
158
|
+
>>> to_mayan_text(42)
|
|
159
|
+
'••━ | •'
|
|
160
|
+
"""
|
|
161
|
+
if not isinstance(num, int):
|
|
162
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
163
|
+
if not MIN <= num <= MAX:
|
|
164
|
+
raise ValueError(f"Mayan numerals support {MIN}–{MAX}, got {num}")
|
|
165
|
+
|
|
166
|
+
def _digit_text(d: int) -> str:
|
|
167
|
+
if d == 0:
|
|
168
|
+
return "◎"
|
|
169
|
+
bars = d // 5
|
|
170
|
+
dots = d % 5
|
|
171
|
+
return "•" * dots + "━" * bars
|
|
172
|
+
|
|
173
|
+
if num == 0:
|
|
174
|
+
return "◎"
|
|
175
|
+
|
|
176
|
+
digits = []
|
|
177
|
+
n = num
|
|
178
|
+
while n:
|
|
179
|
+
digits.append(_digit_text(n % BASE))
|
|
180
|
+
n //= BASE
|
|
181
|
+
|
|
182
|
+
return " | ".join(reversed(digits))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_valid(s: str) -> bool:
|
|
186
|
+
"""Check whether a string is a valid Mayan numeral."""
|
|
187
|
+
try:
|
|
188
|
+
val = from_mayan(s)
|
|
189
|
+
return to_mayan(val) == s
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── quick demo ─────────────────────────────────────────────────────────────
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
tests = [0, 1, 5, 7, 19, 20, 42, 100, 400, 7999]
|
|
197
|
+
print(f"{'n':<8} {'Unicode':<20} {'Text (dots/bars)'}")
|
|
198
|
+
print("─" * 50)
|
|
199
|
+
for n in tests:
|
|
200
|
+
u = to_mayan(n)
|
|
201
|
+
t = to_mayan_text(n)
|
|
202
|
+
print(f"{n:<8} {u:<20} {t}")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.tamil
|
|
3
|
+
~~~~~~~~~~~
|
|
4
|
+
Convert integers to Ancient Tamil numerals and back.
|
|
5
|
+
|
|
6
|
+
Tamil numerals use a positional-additive system with dedicated symbols
|
|
7
|
+
for 1–9, 10, 100, and 1000.
|
|
8
|
+
|
|
9
|
+
Unicode block: Tamil (U+0BE6–U+0BF2)
|
|
10
|
+
|
|
11
|
+
௦ = 0 ௧ = 1 ௨ = 2 ௩ = 3 ௪ = 4
|
|
12
|
+
௫ = 5 ௬ = 6 ௭ = 7 ௮ = 8 ௯ = 9
|
|
13
|
+
௰ = 10 ௱ = 100 ௲ = 1000
|
|
14
|
+
|
|
15
|
+
Range: 0 – 9,999
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
--------
|
|
19
|
+
>>> from numly.tamil import to_tamil, from_tamil
|
|
20
|
+
>>> to_tamil(42)
|
|
21
|
+
'௪௰௨'
|
|
22
|
+
>>> from_tamil('௪௰௨')
|
|
23
|
+
42
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ── symbol tables ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
_DIGITS = "௦௧௨௩௪௫௬௭௮௯" # 0–9 (U+0BE6 – U+0BEF)
|
|
29
|
+
_TEN = "௰" # 10 (U+0BF0)
|
|
30
|
+
_HUNDRED = "௱" # 100 (U+0BF1)
|
|
31
|
+
_THOUSAND = "௲" # 1000 (U+0BF2)
|
|
32
|
+
|
|
33
|
+
_CHAR_VALUE = {ch: i for i, ch in enumerate(_DIGITS)}
|
|
34
|
+
_CHAR_VALUE[_TEN] = 10
|
|
35
|
+
_CHAR_VALUE[_HUNDRED] = 100
|
|
36
|
+
_CHAR_VALUE[_THOUSAND] = 1000
|
|
37
|
+
|
|
38
|
+
SYSTEM = "tamil"
|
|
39
|
+
MIN, MAX = 0, 9_999
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── encode ─────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def to_tamil(num: int) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Convert an integer to an Ancient Tamil numeral string.
|
|
47
|
+
|
|
48
|
+
Tamil numerals use multiplier notation:
|
|
49
|
+
42 → ௪ (4) × ௰ (10) + ௨ (2) → '௪௰௨'
|
|
50
|
+
300 → ௩ (3) × ௱ (100) → '௩௱'
|
|
51
|
+
1500→ ௧ (1) × ௲ (1000) + ௫ × ௱ → '௧௲௫௱'
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
num: Integer between 0 and 9,999.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tamil numeral string, e.g. '௪௰௨'.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
TypeError: If num is not an int.
|
|
61
|
+
ValueError: If num is outside 0–9999.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> to_tamil(0)
|
|
65
|
+
'௦'
|
|
66
|
+
>>> to_tamil(7)
|
|
67
|
+
'௭'
|
|
68
|
+
>>> to_tamil(10)
|
|
69
|
+
'௰'
|
|
70
|
+
>>> to_tamil(42)
|
|
71
|
+
'௪௰௨'
|
|
72
|
+
>>> to_tamil(100)
|
|
73
|
+
'௱'
|
|
74
|
+
>>> to_tamil(999)
|
|
75
|
+
'௯௱௯௰௯'
|
|
76
|
+
>>> to_tamil(1234)
|
|
77
|
+
'௧௲௨௱௩௰௪'
|
|
78
|
+
"""
|
|
79
|
+
if not isinstance(num, int):
|
|
80
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
81
|
+
if num == 0:
|
|
82
|
+
return _DIGITS[0]
|
|
83
|
+
if not 1 <= num <= MAX:
|
|
84
|
+
raise ValueError(f"Tamil numerals support 0–{MAX}, got {num}")
|
|
85
|
+
|
|
86
|
+
result = ""
|
|
87
|
+
|
|
88
|
+
# thousands
|
|
89
|
+
t = num // 1000
|
|
90
|
+
if t:
|
|
91
|
+
result += (_DIGITS[t] if t > 1 else "") + _THOUSAND
|
|
92
|
+
num %= 1000
|
|
93
|
+
|
|
94
|
+
# hundreds
|
|
95
|
+
h = num // 100
|
|
96
|
+
if h:
|
|
97
|
+
result += (_DIGITS[h] if h > 1 else "") + _HUNDRED
|
|
98
|
+
num %= 100
|
|
99
|
+
|
|
100
|
+
# tens
|
|
101
|
+
d = num // 10
|
|
102
|
+
if d:
|
|
103
|
+
result += (_DIGITS[d] if d > 1 else "") + _TEN
|
|
104
|
+
num %= 10
|
|
105
|
+
|
|
106
|
+
# ones
|
|
107
|
+
if num:
|
|
108
|
+
result += _DIGITS[num]
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── decode ─────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def from_tamil(s: str) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Convert an Ancient Tamil numeral string to an integer.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
s: Tamil numeral string, e.g. '௪௰௨'.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Integer value.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
TypeError: If s is not a str.
|
|
127
|
+
ValueError: If s contains unknown characters or is empty.
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
>>> from_tamil('௦')
|
|
131
|
+
0
|
|
132
|
+
>>> from_tamil('௪௰௨')
|
|
133
|
+
42
|
|
134
|
+
>>> from_tamil('௧௲௨௱௩௰௪')
|
|
135
|
+
1234
|
|
136
|
+
"""
|
|
137
|
+
if not isinstance(s, str):
|
|
138
|
+
raise TypeError(f"Expected str, got {type(s).__name__!r}")
|
|
139
|
+
s = s.strip()
|
|
140
|
+
if not s:
|
|
141
|
+
raise ValueError("Empty string")
|
|
142
|
+
if s == _DIGITS[0]:
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
for ch in s:
|
|
146
|
+
if ch not in _CHAR_VALUE:
|
|
147
|
+
raise ValueError(f"Unknown Tamil numeral character: {ch!r}")
|
|
148
|
+
|
|
149
|
+
result = 0
|
|
150
|
+
pending = 0 # digit waiting to be multiplied by a unit
|
|
151
|
+
|
|
152
|
+
for ch in s:
|
|
153
|
+
v = _CHAR_VALUE[ch]
|
|
154
|
+
if v in (10, 100, 1000):
|
|
155
|
+
# multiplier — pending digit × unit (1 if no pending digit)
|
|
156
|
+
result += (pending if pending else 1) * v
|
|
157
|
+
pending = 0
|
|
158
|
+
else:
|
|
159
|
+
# plain digit
|
|
160
|
+
pending = v
|
|
161
|
+
|
|
162
|
+
result += pending # any trailing ones digit
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_valid(s: str) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check whether a string is a valid Ancient Tamil numeral.
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
>>> is_valid('௪௰௨')
|
|
172
|
+
True
|
|
173
|
+
>>> is_valid('hello')
|
|
174
|
+
False
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
val = from_tamil(s)
|
|
178
|
+
return to_tamil(val) == s
|
|
179
|
+
except (TypeError, ValueError):
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── quick demo ─────────────────────────────────────────────────────────────
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
tests = [0, 1, 7, 10, 11, 42, 100, 110, 999, 1000, 1234, 9999]
|
|
186
|
+
for n in tests:
|
|
187
|
+
r = to_tamil(n)
|
|
188
|
+
back = from_tamil(r)
|
|
189
|
+
print(f"{n:<8} → {r:<12} → {back}")
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numly.words
|
|
3
|
+
~~~~~~~~~~~
|
|
4
|
+
Convert integers to English words in both Western and Indian systems.
|
|
5
|
+
|
|
6
|
+
Western system : thousand, million, billion, trillion …
|
|
7
|
+
Indian system : thousand, lakh, crore …
|
|
8
|
+
|
|
9
|
+
Range
|
|
10
|
+
-----
|
|
11
|
+
Western : 0 – 999,999,999,999,999 (up to 999 trillion)
|
|
12
|
+
Indian : 0 – 99,99,99,99,99,999 (up to 99 lakh crore)
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
--------
|
|
16
|
+
>>> from numly.words import to_words_western, to_words_indian
|
|
17
|
+
>>> to_words_western(1234567)
|
|
18
|
+
'one million two hundred thirty four thousand five hundred sixty seven'
|
|
19
|
+
>>> to_words_indian(1234567)
|
|
20
|
+
'twelve lakh thirty four thousand five hundred sixty seven'
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# ── lookup tables ──────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
_ONES = [
|
|
26
|
+
"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
27
|
+
"ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
|
|
28
|
+
"seventeen", "eighteen", "nineteen",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_TENS = [
|
|
32
|
+
"", "", "twenty", "thirty", "forty", "fifty",
|
|
33
|
+
"sixty", "seventy", "eighty", "ninety",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
SYSTEM = "words"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── internal helpers ───────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def _below_1000(n: int) -> str:
|
|
42
|
+
"""Convert an integer 0–999 to words (no leading/trailing spaces)."""
|
|
43
|
+
if n == 0:
|
|
44
|
+
return ""
|
|
45
|
+
if n < 20:
|
|
46
|
+
return _ONES[n]
|
|
47
|
+
if n < 100:
|
|
48
|
+
rest = _ONES[n % 10]
|
|
49
|
+
return _TENS[n // 10] + (" " + rest if rest else "")
|
|
50
|
+
rest = _below_1000(n % 100)
|
|
51
|
+
return _ONES[n // 100] + " hundred" + (" " + rest if rest else "")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ── Western system ─────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
_WESTERN_SCALE = [
|
|
57
|
+
(1_000_000_000_000, "trillion"),
|
|
58
|
+
(1_000_000_000, "billion"),
|
|
59
|
+
(1_000_000, "million"),
|
|
60
|
+
(1_000, "thousand"),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_words_western(num: int) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Convert an integer to English words using the Western scale.
|
|
67
|
+
|
|
68
|
+
Uses: thousand → million → billion → trillion
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
num: Integer between 0 and 999,999,999,999,999.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
English words string, e.g. 'one million two hundred thirty four thousand'.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
TypeError: If num is not an int.
|
|
78
|
+
ValueError: If num is negative or out of range.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> to_words_western(0)
|
|
82
|
+
'zero'
|
|
83
|
+
>>> to_words_western(42)
|
|
84
|
+
'forty two'
|
|
85
|
+
>>> to_words_western(1001)
|
|
86
|
+
'one thousand one'
|
|
87
|
+
>>> to_words_western(1_000_000)
|
|
88
|
+
'one million'
|
|
89
|
+
>>> to_words_western(1_234_567)
|
|
90
|
+
'one million two hundred thirty four thousand five hundred sixty seven'
|
|
91
|
+
>>> to_words_western(1_000_000_000)
|
|
92
|
+
'one billion'
|
|
93
|
+
"""
|
|
94
|
+
if not isinstance(num, int):
|
|
95
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
96
|
+
if num < 0:
|
|
97
|
+
raise ValueError("Negative numbers are not supported")
|
|
98
|
+
if num > 999_999_999_999_999:
|
|
99
|
+
raise ValueError("Number too large; maximum is 999,999,999,999,999")
|
|
100
|
+
if num == 0:
|
|
101
|
+
return "zero"
|
|
102
|
+
|
|
103
|
+
parts = []
|
|
104
|
+
for scale, name in _WESTERN_SCALE:
|
|
105
|
+
if num >= scale:
|
|
106
|
+
parts.append(_below_1000(num // scale) + " " + name)
|
|
107
|
+
num %= scale
|
|
108
|
+
if num > 0:
|
|
109
|
+
parts.append(_below_1000(num))
|
|
110
|
+
|
|
111
|
+
return " ".join(parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── Indian system ──────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
_INDIAN_SCALE = [
|
|
117
|
+
(10_00_00_000, "crore"),
|
|
118
|
+
(1_00_000, "lakh"),
|
|
119
|
+
(1_000, "thousand"),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def to_words_indian(num: int) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Convert an integer to English words using the Indian numbering scale.
|
|
126
|
+
|
|
127
|
+
Uses: thousand → lakh → crore
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
num: Integer between 0 and 9,99,99,99,99,99,999.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
English words string using Indian scale, e.g. 'twelve lakh thirty four thousand'.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
TypeError: If num is not an int.
|
|
137
|
+
ValueError: If num is negative or out of range.
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
>>> to_words_indian(0)
|
|
141
|
+
'zero'
|
|
142
|
+
>>> to_words_indian(42)
|
|
143
|
+
'forty two'
|
|
144
|
+
>>> to_words_indian(100_000)
|
|
145
|
+
'one lakh'
|
|
146
|
+
>>> to_words_indian(1_234_567)
|
|
147
|
+
'twelve lakh thirty four thousand five hundred sixty seven'
|
|
148
|
+
>>> to_words_indian(10_000_000)
|
|
149
|
+
'one crore'
|
|
150
|
+
>>> to_words_indian(10_000_000_000)
|
|
151
|
+
'one thousand crore'
|
|
152
|
+
"""
|
|
153
|
+
if not isinstance(num, int):
|
|
154
|
+
raise TypeError(f"Expected int, got {type(num).__name__!r}")
|
|
155
|
+
if num < 0:
|
|
156
|
+
raise ValueError("Negative numbers are not supported")
|
|
157
|
+
if num == 0:
|
|
158
|
+
return "zero"
|
|
159
|
+
|
|
160
|
+
parts = []
|
|
161
|
+
for scale, name in _INDIAN_SCALE:
|
|
162
|
+
if num >= scale:
|
|
163
|
+
chunk = num // scale
|
|
164
|
+
parts.append(_below_1000(chunk) + " " + name)
|
|
165
|
+
num %= scale
|
|
166
|
+
if num > 0:
|
|
167
|
+
parts.append(_below_1000(num))
|
|
168
|
+
|
|
169
|
+
return " ".join(parts)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def to_words(num: int, system: str = "western") -> str:
|
|
173
|
+
"""
|
|
174
|
+
Convert an integer to English words.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
num: Integer to convert.
|
|
178
|
+
system: 'western' (default) or 'indian'.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
English words string.
|
|
182
|
+
|
|
183
|
+
Examples:
|
|
184
|
+
>>> to_words(1_234_567, "western")
|
|
185
|
+
'one million two hundred thirty four thousand five hundred sixty seven'
|
|
186
|
+
>>> to_words(1_234_567, "indian")
|
|
187
|
+
'twelve lakh thirty four thousand five hundred sixty seven'
|
|
188
|
+
"""
|
|
189
|
+
system = system.lower().strip()
|
|
190
|
+
if system == "western":
|
|
191
|
+
return to_words_western(num)
|
|
192
|
+
if system == "indian":
|
|
193
|
+
return to_words_indian(num)
|
|
194
|
+
raise ValueError(f"Unknown system {system!r}. Choose 'western' or 'indian'.")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── quick demo ─────────────────────────────────────────────────────────────
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
tests = [0, 1, 13, 42, 100, 999, 1_000, 10_000, 1_00_000,
|
|
200
|
+
1_234_567, 10_000_000, 1_000_000_000, 1_234_567_890]
|
|
201
|
+
|
|
202
|
+
print(f"{'Number':<20} {'Western':<55} {'Indian'}")
|
|
203
|
+
print("─" * 120)
|
|
204
|
+
for n in tests:
|
|
205
|
+
w = to_words_western(n)
|
|
206
|
+
i = to_words_indian(n)
|
|
207
|
+
print(f"{n:<20} {w:<55} {i}")
|
|
@@ -3,11 +3,16 @@ setup.cfg
|
|
|
3
3
|
setup.py
|
|
4
4
|
numly/__init__.py
|
|
5
5
|
numly/arabic_indic.py
|
|
6
|
+
numly/babylonian.py
|
|
7
|
+
numly/base.py
|
|
6
8
|
numly/chinese.py
|
|
7
9
|
numly/convert.py
|
|
8
10
|
numly/egyptian.py
|
|
9
11
|
numly/greek.py
|
|
12
|
+
numly/mayan.py
|
|
10
13
|
numly/roman.py
|
|
14
|
+
numly/tamil.py
|
|
15
|
+
numly/words.py
|
|
11
16
|
numly.egg-info/PKG-INFO
|
|
12
17
|
numly.egg-info/SOURCES.txt
|
|
13
18
|
numly.egg-info/dependency_links.txt
|
numly-0.2.0/numly/__init__.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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.1"
|
|
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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|