multilingualprogramming 0.2.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.
- multilingualprogramming/__init__.py +74 -0
- multilingualprogramming/__main__.py +194 -0
- multilingualprogramming/codegen/__init__.py +12 -0
- multilingualprogramming/codegen/executor.py +215 -0
- multilingualprogramming/codegen/python_generator.py +592 -0
- multilingualprogramming/codegen/repl.py +489 -0
- multilingualprogramming/codegen/runtime_builtins.py +308 -0
- multilingualprogramming/core/__init__.py +12 -0
- multilingualprogramming/core/ir.py +29 -0
- multilingualprogramming/core/lowering.py +24 -0
- multilingualprogramming/datetime/__init__.py +11 -0
- multilingualprogramming/datetime/date_parser.py +190 -0
- multilingualprogramming/datetime/mp_date.py +210 -0
- multilingualprogramming/datetime/mp_datetime.py +153 -0
- multilingualprogramming/datetime/mp_time.py +147 -0
- multilingualprogramming/datetime/resource_loader.py +18 -0
- multilingualprogramming/exceptions.py +158 -0
- multilingualprogramming/imports.py +150 -0
- multilingualprogramming/keyword/__init__.py +13 -0
- multilingualprogramming/keyword/keyword_registry.py +249 -0
- multilingualprogramming/keyword/keyword_validator.py +59 -0
- multilingualprogramming/keyword/language_pack_validator.py +110 -0
- multilingualprogramming/lexer/__init__.py +11 -0
- multilingualprogramming/lexer/lexer.py +570 -0
- multilingualprogramming/lexer/source_reader.py +91 -0
- multilingualprogramming/lexer/token.py +54 -0
- multilingualprogramming/lexer/token_types.py +38 -0
- multilingualprogramming/numeral/__init__.py +11 -0
- multilingualprogramming/numeral/abstract_numeral.py +232 -0
- multilingualprogramming/numeral/complex_numeral.py +190 -0
- multilingualprogramming/numeral/fraction_numeral.py +165 -0
- multilingualprogramming/numeral/mp_numeral.py +243 -0
- multilingualprogramming/numeral/numeral_converter.py +151 -0
- multilingualprogramming/numeral/roman_numeral.py +301 -0
- multilingualprogramming/numeral/unicode_numeral.py +292 -0
- multilingualprogramming/parser/__init__.py +28 -0
- multilingualprogramming/parser/ast_nodes.py +459 -0
- multilingualprogramming/parser/ast_printer.py +677 -0
- multilingualprogramming/parser/error_messages.py +75 -0
- multilingualprogramming/parser/parser.py +1796 -0
- multilingualprogramming/parser/semantic_analyzer.py +689 -0
- multilingualprogramming/parser/surface_normalizer.py +282 -0
- multilingualprogramming/resources/datetime/eras.json +23 -0
- multilingualprogramming/resources/datetime/formats.json +32 -0
- multilingualprogramming/resources/datetime/months.json +150 -0
- multilingualprogramming/resources/datetime/weekdays.json +90 -0
- multilingualprogramming/resources/parser/error_messages.json +310 -0
- multilingualprogramming/resources/repl/commands.json +636 -0
- multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
- multilingualprogramming/resources/usm/keywords.json +1063 -0
- multilingualprogramming/resources/usm/operators.json +532 -0
- multilingualprogramming/resources/usm/schema.json +34 -0
- multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
- multilingualprogramming/unicode_string.py +140 -0
- multilingualprogramming/version.py +9 -0
- multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
- multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
- multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
- multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
- multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
- multilingualprogramming-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Token data class for the multilingual lexer."""
|
|
8
|
+
|
|
9
|
+
from multilingualprogramming.lexer.token_types import TokenType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Token:
|
|
13
|
+
"""
|
|
14
|
+
Represents a single token produced by the lexer.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
type (TokenType): The type of this token
|
|
18
|
+
value (str): The raw text of the token
|
|
19
|
+
concept (str | None): USM concept ID (for KEYWORD tokens only)
|
|
20
|
+
language (str | None): Detected language of the token
|
|
21
|
+
line (int): Line number (1-based)
|
|
22
|
+
column (int): Column number (1-based)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
26
|
+
def __init__(self, token_type: TokenType, value, line=1, column=1,
|
|
27
|
+
concept=None, language=None):
|
|
28
|
+
self.type = token_type
|
|
29
|
+
self.value = value
|
|
30
|
+
self.line = line
|
|
31
|
+
self.column = column
|
|
32
|
+
self.concept = concept
|
|
33
|
+
self.language = language
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
parts = [f"Token({self.type.name}, {self.value!r}"]
|
|
37
|
+
if self.concept:
|
|
38
|
+
parts.append(f", concept={self.concept!r}")
|
|
39
|
+
if self.language:
|
|
40
|
+
parts.append(f", lang={self.language!r}")
|
|
41
|
+
parts.append(f", {self.line}:{self.column})")
|
|
42
|
+
return "".join(parts)
|
|
43
|
+
|
|
44
|
+
def __eq__(self, other):
|
|
45
|
+
if isinstance(other, Token):
|
|
46
|
+
return (
|
|
47
|
+
self.type == other.type
|
|
48
|
+
and self.value == other.value
|
|
49
|
+
and self.concept == other.concept
|
|
50
|
+
)
|
|
51
|
+
return NotImplemented
|
|
52
|
+
|
|
53
|
+
def __hash__(self):
|
|
54
|
+
return hash((self.type, self.value, self.concept))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Token type definitions for the multilingual lexer."""
|
|
8
|
+
|
|
9
|
+
from enum import Enum, auto
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenType(Enum):
|
|
13
|
+
"""Types of tokens recognized by the lexer."""
|
|
14
|
+
|
|
15
|
+
# Keywords and identifiers
|
|
16
|
+
KEYWORD = auto()
|
|
17
|
+
IDENTIFIER = auto()
|
|
18
|
+
|
|
19
|
+
# Literals
|
|
20
|
+
NUMERAL = auto()
|
|
21
|
+
STRING = auto()
|
|
22
|
+
FSTRING = auto()
|
|
23
|
+
DATE_LITERAL = auto()
|
|
24
|
+
|
|
25
|
+
# Operators
|
|
26
|
+
OPERATOR = auto()
|
|
27
|
+
|
|
28
|
+
# Delimiters
|
|
29
|
+
DELIMITER = auto()
|
|
30
|
+
|
|
31
|
+
# Whitespace-significant tokens
|
|
32
|
+
NEWLINE = auto()
|
|
33
|
+
INDENT = auto()
|
|
34
|
+
DEDENT = auto()
|
|
35
|
+
|
|
36
|
+
# Comments and end-of-file
|
|
37
|
+
COMMENT = auto()
|
|
38
|
+
EOF = auto()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Numeral subpackage for multilingual programming."""
|
|
8
|
+
|
|
9
|
+
from multilingualprogramming.numeral.mp_numeral import MPNumeral
|
|
10
|
+
from multilingualprogramming.numeral.unicode_numeral import UnicodeNumeral
|
|
11
|
+
from multilingualprogramming.numeral.roman_numeral import RomanNumeral
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Abstract Numeral class and methods
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbstractNumeral(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Methods related to numerals
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, numstr: str):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def to_decimal(self):
|
|
23
|
+
"""
|
|
24
|
+
Returns the decimal number associated with the number string
|
|
25
|
+
given by the user
|
|
26
|
+
|
|
27
|
+
return:
|
|
28
|
+
number: number associated with the number string
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
"""
|
|
33
|
+
Returns the original number string
|
|
34
|
+
|
|
35
|
+
return:
|
|
36
|
+
numstr: original number string
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
"""
|
|
41
|
+
Returns the representation of an instance
|
|
42
|
+
|
|
43
|
+
return:
|
|
44
|
+
reprstr: representation of an instance
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def __lshift__(self, numeral):
|
|
49
|
+
"""
|
|
50
|
+
Left-shifting
|
|
51
|
+
|
|
52
|
+
return:
|
|
53
|
+
AbstractNumeral: returns the left shifted value
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def __rshift__(self, numeral):
|
|
58
|
+
"""
|
|
59
|
+
Right-shifting
|
|
60
|
+
|
|
61
|
+
return:
|
|
62
|
+
AbstractNumeral: returns the right shifted value
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def __add__(self, numeral):
|
|
67
|
+
"""
|
|
68
|
+
Add a AbstractNumeral with a numeral or another AbstractNumeral
|
|
69
|
+
|
|
70
|
+
return:
|
|
71
|
+
AbstractNumeral: returns the sum of a AbstractNumeral
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def __mul__(self, numeral):
|
|
76
|
+
"""
|
|
77
|
+
Multiplication
|
|
78
|
+
|
|
79
|
+
return:
|
|
80
|
+
AbstractNumeral: returns the product
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def __sub__(self, numeral):
|
|
85
|
+
"""
|
|
86
|
+
Substraction
|
|
87
|
+
|
|
88
|
+
return:
|
|
89
|
+
AbstractNumeral: returns the difference
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def __truediv__(self, numeral):
|
|
94
|
+
"""
|
|
95
|
+
True division
|
|
96
|
+
|
|
97
|
+
return:
|
|
98
|
+
AbstractNumeral: returns the value after true division
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def __floordiv__(self, numeral):
|
|
103
|
+
"""
|
|
104
|
+
Floor division
|
|
105
|
+
|
|
106
|
+
return:
|
|
107
|
+
AbstractNumeral: returns the value after floor division
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def __neg__(self):
|
|
112
|
+
"""
|
|
113
|
+
Negation
|
|
114
|
+
|
|
115
|
+
return:
|
|
116
|
+
AbstractNumeral: returns the negation
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def __pow__(self, numeral):
|
|
121
|
+
"""
|
|
122
|
+
Power
|
|
123
|
+
|
|
124
|
+
return:
|
|
125
|
+
AbstractNumeral: returns the power
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def __mod__(self, numeral):
|
|
130
|
+
"""
|
|
131
|
+
Modulus
|
|
132
|
+
|
|
133
|
+
return:
|
|
134
|
+
AbstractNumeral: returns the modulus value
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def __xor__(self, numeral):
|
|
139
|
+
"""
|
|
140
|
+
XOR value
|
|
141
|
+
|
|
142
|
+
return:
|
|
143
|
+
AbstractNumeral: returns the XOR value
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def __invert__(self):
|
|
148
|
+
"""
|
|
149
|
+
Bitwise inversion value
|
|
150
|
+
|
|
151
|
+
return:
|
|
152
|
+
AbstractNumeral: returns the bitwise-inverted value
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def __or__(self, numeral):
|
|
157
|
+
"""
|
|
158
|
+
OR value
|
|
159
|
+
|
|
160
|
+
return:
|
|
161
|
+
AbstractNumeral: returns the OR value
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __eq__(self, other):
|
|
165
|
+
"""
|
|
166
|
+
Equality comparison
|
|
167
|
+
|
|
168
|
+
return:
|
|
169
|
+
bool: True if both numerals have equal decimal values
|
|
170
|
+
"""
|
|
171
|
+
if isinstance(other, AbstractNumeral):
|
|
172
|
+
return self.to_decimal() == other.to_decimal()
|
|
173
|
+
return NotImplemented
|
|
174
|
+
|
|
175
|
+
def __lt__(self, other):
|
|
176
|
+
"""
|
|
177
|
+
Less-than comparison
|
|
178
|
+
|
|
179
|
+
return:
|
|
180
|
+
bool: True if this numeral is less than the other
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(other, AbstractNumeral):
|
|
183
|
+
return self.to_decimal() < other.to_decimal()
|
|
184
|
+
return NotImplemented
|
|
185
|
+
|
|
186
|
+
def __le__(self, other):
|
|
187
|
+
"""
|
|
188
|
+
Less-than-or-equal comparison
|
|
189
|
+
|
|
190
|
+
return:
|
|
191
|
+
bool: True if this numeral is less than or equal to the other
|
|
192
|
+
"""
|
|
193
|
+
if isinstance(other, AbstractNumeral):
|
|
194
|
+
return self.to_decimal() <= other.to_decimal()
|
|
195
|
+
return NotImplemented
|
|
196
|
+
|
|
197
|
+
def __gt__(self, other):
|
|
198
|
+
"""
|
|
199
|
+
Greater-than comparison
|
|
200
|
+
|
|
201
|
+
return:
|
|
202
|
+
bool: True if this numeral is greater than the other
|
|
203
|
+
"""
|
|
204
|
+
if isinstance(other, AbstractNumeral):
|
|
205
|
+
return self.to_decimal() > other.to_decimal()
|
|
206
|
+
return NotImplemented
|
|
207
|
+
|
|
208
|
+
def __ge__(self, other):
|
|
209
|
+
"""
|
|
210
|
+
Greater-than-or-equal comparison
|
|
211
|
+
|
|
212
|
+
return:
|
|
213
|
+
bool: True if this numeral is greater than or equal to the other
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(other, AbstractNumeral):
|
|
216
|
+
return self.to_decimal() >= other.to_decimal()
|
|
217
|
+
return NotImplemented
|
|
218
|
+
|
|
219
|
+
def __hash__(self):
|
|
220
|
+
"""
|
|
221
|
+
Hash based on decimal value for consistency with __eq__
|
|
222
|
+
"""
|
|
223
|
+
return hash(self.to_decimal())
|
|
224
|
+
|
|
225
|
+
def __abs__(self):
|
|
226
|
+
"""
|
|
227
|
+
Absolute value
|
|
228
|
+
|
|
229
|
+
return:
|
|
230
|
+
AbstractNumeral: returns the absolute value
|
|
231
|
+
"""
|
|
232
|
+
raise NotImplementedError("Subclasses must implement __abs__")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Functions to handle complex numbers in multiple languages"""
|
|
8
|
+
from multilingualprogramming.numeral.unicode_numeral import UnicodeNumeral
|
|
9
|
+
from multilingualprogramming.exceptions import (
|
|
10
|
+
InvalidNumeralCharacterError,
|
|
11
|
+
MultipleLanguageCharacterMixError,
|
|
12
|
+
)
|
|
13
|
+
from multilingualprogramming.unicode_string import get_unicode_character_string
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ComplexNumeral:
|
|
17
|
+
"""
|
|
18
|
+
Handling complex numbers with multilingual numeral support.
|
|
19
|
+
|
|
20
|
+
Stores real and imaginary parts as UnicodeNumeral instances.
|
|
21
|
+
Format: "real+imaginaryi" or "real-imaginaryi" in any supported script.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
IMAGINARY_MARKERS = ["i", "ⅈ", "ⅉ"]
|
|
25
|
+
|
|
26
|
+
def __init__(self, numstr=None, real=None, imaginary=None):
|
|
27
|
+
"""
|
|
28
|
+
Create a ComplexNumeral.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
numstr (str): A complex number string like "3+4i" or "൩+൪i"
|
|
32
|
+
real (UnicodeNumeral): The real part
|
|
33
|
+
imaginary (UnicodeNumeral): The imaginary part
|
|
34
|
+
"""
|
|
35
|
+
if real is not None and imaginary is not None:
|
|
36
|
+
self.real = real
|
|
37
|
+
self.imaginary = imaginary
|
|
38
|
+
self.language_name = real.language_name
|
|
39
|
+
elif numstr is not None:
|
|
40
|
+
self._parse(numstr)
|
|
41
|
+
else:
|
|
42
|
+
raise InvalidNumeralCharacterError(
|
|
43
|
+
"ComplexNumeral requires either numstr or both real and imaginary"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _parse(self, numstr):
|
|
47
|
+
"""Parse a complex number string."""
|
|
48
|
+
# Remove spaces
|
|
49
|
+
numstr = numstr.strip()
|
|
50
|
+
|
|
51
|
+
# Check for imaginary marker at end
|
|
52
|
+
has_imaginary = False
|
|
53
|
+
for marker in self.IMAGINARY_MARKERS:
|
|
54
|
+
if numstr.endswith(marker):
|
|
55
|
+
numstr = numstr[: -len(marker)]
|
|
56
|
+
has_imaginary = True
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
if not has_imaginary:
|
|
60
|
+
# Pure real number
|
|
61
|
+
self.real = UnicodeNumeral(numstr)
|
|
62
|
+
self.imaginary = UnicodeNumeral("0")
|
|
63
|
+
self.language_name = self.real.language_name
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Find the split between real and imaginary parts
|
|
67
|
+
# Look for + or - that is not at the start
|
|
68
|
+
split_pos = None
|
|
69
|
+
for i in range(len(numstr) - 1, 0, -1):
|
|
70
|
+
if numstr[i] in ("+", "-"):
|
|
71
|
+
split_pos = i
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if split_pos is not None:
|
|
75
|
+
real_str = numstr[:split_pos]
|
|
76
|
+
imag_str = numstr[split_pos:]
|
|
77
|
+
# Strip leading '+' since UnicodeNumeral only accepts '-' as sign
|
|
78
|
+
if imag_str.startswith("+"):
|
|
79
|
+
imag_str = imag_str[1:]
|
|
80
|
+
self.real = UnicodeNumeral(real_str)
|
|
81
|
+
self.imaginary = UnicodeNumeral(imag_str)
|
|
82
|
+
else:
|
|
83
|
+
# Pure imaginary (e.g., "4i")
|
|
84
|
+
self.real = UnicodeNumeral("0")
|
|
85
|
+
self.imaginary = UnicodeNumeral(numstr)
|
|
86
|
+
|
|
87
|
+
# Determine language from whichever part has a non-DIGIT language
|
|
88
|
+
self.language_name = self.real.language_name
|
|
89
|
+
if self.language_name == "DIGIT" and self.imaginary.language_name != "DIGIT":
|
|
90
|
+
self.language_name = self.imaginary.language_name
|
|
91
|
+
elif self.imaginary.language_name is not None:
|
|
92
|
+
if (
|
|
93
|
+
self.real.language_name is not None
|
|
94
|
+
and self.real.language_name != "DIGIT"
|
|
95
|
+
and self.imaginary.language_name != "DIGIT"
|
|
96
|
+
and self.real.language_name != self.imaginary.language_name
|
|
97
|
+
):
|
|
98
|
+
raise MultipleLanguageCharacterMixError(
|
|
99
|
+
"Real and imaginary parts use different scripts"
|
|
100
|
+
)
|
|
101
|
+
if self.imaginary.language_name != "DIGIT":
|
|
102
|
+
self.language_name = self.imaginary.language_name
|
|
103
|
+
|
|
104
|
+
def to_complex(self):
|
|
105
|
+
"""
|
|
106
|
+
Convert to Python complex number.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
complex: Python complex number
|
|
110
|
+
"""
|
|
111
|
+
return complex(self.real.to_decimal(), self.imaginary.to_decimal())
|
|
112
|
+
|
|
113
|
+
def _make_result(self, result_complex):
|
|
114
|
+
"""Create a new ComplexNumeral from a Python complex result."""
|
|
115
|
+
lang = self.language_name or "DIGIT"
|
|
116
|
+
real_val = result_complex.real
|
|
117
|
+
imag_val = result_complex.imag
|
|
118
|
+
|
|
119
|
+
# Convert to int if possible
|
|
120
|
+
if real_val == int(real_val):
|
|
121
|
+
real_val = int(real_val)
|
|
122
|
+
if imag_val == int(imag_val):
|
|
123
|
+
imag_val = int(imag_val)
|
|
124
|
+
|
|
125
|
+
real_str = get_unicode_character_string(lang, real_val)
|
|
126
|
+
imag_str = get_unicode_character_string(lang, imag_val)
|
|
127
|
+
|
|
128
|
+
return ComplexNumeral(
|
|
129
|
+
real=UnicodeNumeral(real_str),
|
|
130
|
+
imaginary=UnicodeNumeral(imag_str),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def __add__(self, other):
|
|
134
|
+
"""Add two ComplexNumerals."""
|
|
135
|
+
result = self.to_complex() + other.to_complex()
|
|
136
|
+
return self._make_result(result)
|
|
137
|
+
|
|
138
|
+
def __sub__(self, other):
|
|
139
|
+
"""Subtract two ComplexNumerals."""
|
|
140
|
+
result = self.to_complex() - other.to_complex()
|
|
141
|
+
return self._make_result(result)
|
|
142
|
+
|
|
143
|
+
def __mul__(self, other):
|
|
144
|
+
"""Multiply two ComplexNumerals."""
|
|
145
|
+
result = self.to_complex() * other.to_complex()
|
|
146
|
+
return self._make_result(result)
|
|
147
|
+
|
|
148
|
+
def __truediv__(self, other):
|
|
149
|
+
"""Divide two ComplexNumerals."""
|
|
150
|
+
result = self.to_complex() / other.to_complex()
|
|
151
|
+
return self._make_result(result)
|
|
152
|
+
|
|
153
|
+
def __abs__(self):
|
|
154
|
+
"""Return the magnitude as a UnicodeNumeral."""
|
|
155
|
+
magnitude = abs(self.to_complex())
|
|
156
|
+
if magnitude == int(magnitude):
|
|
157
|
+
magnitude = int(magnitude)
|
|
158
|
+
lang = self.language_name or "DIGIT"
|
|
159
|
+
return UnicodeNumeral(get_unicode_character_string(lang, magnitude))
|
|
160
|
+
|
|
161
|
+
def conjugate(self):
|
|
162
|
+
"""Return the complex conjugate."""
|
|
163
|
+
result = self.to_complex().conjugate()
|
|
164
|
+
return self._make_result(result)
|
|
165
|
+
|
|
166
|
+
def __eq__(self, other):
|
|
167
|
+
if isinstance(other, ComplexNumeral):
|
|
168
|
+
return self.to_complex() == other.to_complex()
|
|
169
|
+
return NotImplemented
|
|
170
|
+
|
|
171
|
+
def __hash__(self):
|
|
172
|
+
return hash(self.to_complex())
|
|
173
|
+
|
|
174
|
+
def __str__(self):
|
|
175
|
+
"""Return string representation in the original script."""
|
|
176
|
+
real_str = str(self.real)
|
|
177
|
+
imag_str = str(self.imaginary)
|
|
178
|
+
imag_val = self.imaginary.to_decimal()
|
|
179
|
+
|
|
180
|
+
if imag_val == 0:
|
|
181
|
+
return real_str
|
|
182
|
+
if self.real.to_decimal() == 0:
|
|
183
|
+
return f"{imag_str}i"
|
|
184
|
+
|
|
185
|
+
if imag_val >= 0:
|
|
186
|
+
return f"{real_str}+{imag_str}i"
|
|
187
|
+
return f"{real_str}{imag_str}i"
|
|
188
|
+
|
|
189
|
+
def __repr__(self):
|
|
190
|
+
return f'ComplexNumeral("{self}")'
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Functions to handle fractions in multiple languages"""
|
|
8
|
+
|
|
9
|
+
from fractions import Fraction
|
|
10
|
+
from multilingualprogramming.numeral.unicode_numeral import UnicodeNumeral
|
|
11
|
+
from multilingualprogramming.exceptions import InvalidNumeralCharacterError
|
|
12
|
+
from multilingualprogramming.unicode_string import (
|
|
13
|
+
UNICODE_FRACTION_MAP,
|
|
14
|
+
get_unicode_character_string,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FractionNumeral:
|
|
19
|
+
"""
|
|
20
|
+
Handling fractions with multilingual numeral support.
|
|
21
|
+
|
|
22
|
+
Supports:
|
|
23
|
+
- Unicode vulgar fractions: ½, ⅓, ¼, etc.
|
|
24
|
+
- Constructed fractions: "3/4", "൩/൪" (in any script)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, numstr):
|
|
28
|
+
"""
|
|
29
|
+
Create a FractionNumeral.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
numstr (str): A fraction string like "½", "3/4", or "൩/൪"
|
|
33
|
+
"""
|
|
34
|
+
self.numstr = numstr
|
|
35
|
+
self.language_name = None
|
|
36
|
+
self._fraction = None
|
|
37
|
+
self._parse(numstr)
|
|
38
|
+
|
|
39
|
+
def _parse(self, numstr):
|
|
40
|
+
"""Parse a fraction string into a Fraction object."""
|
|
41
|
+
numstr = numstr.strip()
|
|
42
|
+
|
|
43
|
+
# Check for Unicode vulgar fraction characters
|
|
44
|
+
if len(numstr) == 1 and numstr in UNICODE_FRACTION_MAP:
|
|
45
|
+
numerator, denominator = UNICODE_FRACTION_MAP[numstr]
|
|
46
|
+
self._fraction = Fraction(numerator, denominator)
|
|
47
|
+
self.language_name = "DIGIT"
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Check for "numerator/denominator" format
|
|
51
|
+
if "/" in numstr:
|
|
52
|
+
parts = numstr.split("/", 1)
|
|
53
|
+
if len(parts) == 2:
|
|
54
|
+
num_part = UnicodeNumeral(parts[0].strip())
|
|
55
|
+
den_part = UnicodeNumeral(parts[1].strip())
|
|
56
|
+
self.language_name = num_part.language_name
|
|
57
|
+
numerator = num_part.to_decimal()
|
|
58
|
+
denominator = den_part.to_decimal()
|
|
59
|
+
if denominator == 0:
|
|
60
|
+
raise InvalidNumeralCharacterError(
|
|
61
|
+
"Division by zero in fraction"
|
|
62
|
+
)
|
|
63
|
+
self._fraction = Fraction(numerator, denominator)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
raise InvalidNumeralCharacterError(
|
|
67
|
+
f"Cannot parse fraction: {numstr}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def to_fraction(self):
|
|
71
|
+
"""
|
|
72
|
+
Convert to Python Fraction.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Fraction: Python Fraction object
|
|
76
|
+
"""
|
|
77
|
+
return self._fraction
|
|
78
|
+
|
|
79
|
+
def to_decimal(self):
|
|
80
|
+
"""
|
|
81
|
+
Convert to decimal float.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
float: The decimal value
|
|
85
|
+
"""
|
|
86
|
+
return float(self._fraction)
|
|
87
|
+
|
|
88
|
+
def simplify(self):
|
|
89
|
+
"""
|
|
90
|
+
Return a simplified version of this fraction.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
FractionNumeral: Simplified fraction
|
|
94
|
+
"""
|
|
95
|
+
simplified = Fraction(
|
|
96
|
+
self._fraction.numerator, self._fraction.denominator
|
|
97
|
+
)
|
|
98
|
+
lang = self.language_name or "DIGIT"
|
|
99
|
+
num_str = get_unicode_character_string(lang, simplified.numerator)
|
|
100
|
+
den_str = get_unicode_character_string(lang, simplified.denominator)
|
|
101
|
+
return FractionNumeral(f"{num_str}/{den_str}")
|
|
102
|
+
|
|
103
|
+
def _make_result(self, result_fraction):
|
|
104
|
+
"""Create a new FractionNumeral from a Python Fraction."""
|
|
105
|
+
lang = self.language_name or "DIGIT"
|
|
106
|
+
num_str = get_unicode_character_string(lang, result_fraction.numerator)
|
|
107
|
+
den_str = get_unicode_character_string(
|
|
108
|
+
lang, result_fraction.denominator
|
|
109
|
+
)
|
|
110
|
+
return FractionNumeral(f"{num_str}/{den_str}")
|
|
111
|
+
|
|
112
|
+
def __add__(self, other):
|
|
113
|
+
"""Add two FractionNumerals."""
|
|
114
|
+
result = self._fraction + other._fraction
|
|
115
|
+
return self._make_result(result)
|
|
116
|
+
|
|
117
|
+
def __sub__(self, other):
|
|
118
|
+
"""Subtract two FractionNumerals."""
|
|
119
|
+
result = self._fraction - other._fraction
|
|
120
|
+
return self._make_result(result)
|
|
121
|
+
|
|
122
|
+
def __mul__(self, other):
|
|
123
|
+
"""Multiply two FractionNumerals."""
|
|
124
|
+
result = self._fraction * other._fraction
|
|
125
|
+
return self._make_result(result)
|
|
126
|
+
|
|
127
|
+
def __truediv__(self, other):
|
|
128
|
+
"""Divide two FractionNumerals."""
|
|
129
|
+
result = self._fraction / other._fraction
|
|
130
|
+
return self._make_result(result)
|
|
131
|
+
|
|
132
|
+
def __eq__(self, other):
|
|
133
|
+
if isinstance(other, FractionNumeral):
|
|
134
|
+
return self._fraction == other._fraction
|
|
135
|
+
return NotImplemented
|
|
136
|
+
|
|
137
|
+
def __lt__(self, other):
|
|
138
|
+
if isinstance(other, FractionNumeral):
|
|
139
|
+
return self._fraction < other._fraction
|
|
140
|
+
return NotImplemented
|
|
141
|
+
|
|
142
|
+
def __le__(self, other):
|
|
143
|
+
if isinstance(other, FractionNumeral):
|
|
144
|
+
return self._fraction <= other._fraction
|
|
145
|
+
return NotImplemented
|
|
146
|
+
|
|
147
|
+
def __gt__(self, other):
|
|
148
|
+
if isinstance(other, FractionNumeral):
|
|
149
|
+
return self._fraction > other._fraction
|
|
150
|
+
return NotImplemented
|
|
151
|
+
|
|
152
|
+
def __ge__(self, other):
|
|
153
|
+
if isinstance(other, FractionNumeral):
|
|
154
|
+
return self._fraction >= other._fraction
|
|
155
|
+
return NotImplemented
|
|
156
|
+
|
|
157
|
+
def __hash__(self):
|
|
158
|
+
return hash(self._fraction)
|
|
159
|
+
|
|
160
|
+
def __str__(self):
|
|
161
|
+
"""Return string representation."""
|
|
162
|
+
return self.numstr
|
|
163
|
+
|
|
164
|
+
def __repr__(self):
|
|
165
|
+
return f'FractionNumeral("{self.numstr}")'
|