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.
Files changed (61) hide show
  1. multilingualprogramming/__init__.py +74 -0
  2. multilingualprogramming/__main__.py +194 -0
  3. multilingualprogramming/codegen/__init__.py +12 -0
  4. multilingualprogramming/codegen/executor.py +215 -0
  5. multilingualprogramming/codegen/python_generator.py +592 -0
  6. multilingualprogramming/codegen/repl.py +489 -0
  7. multilingualprogramming/codegen/runtime_builtins.py +308 -0
  8. multilingualprogramming/core/__init__.py +12 -0
  9. multilingualprogramming/core/ir.py +29 -0
  10. multilingualprogramming/core/lowering.py +24 -0
  11. multilingualprogramming/datetime/__init__.py +11 -0
  12. multilingualprogramming/datetime/date_parser.py +190 -0
  13. multilingualprogramming/datetime/mp_date.py +210 -0
  14. multilingualprogramming/datetime/mp_datetime.py +153 -0
  15. multilingualprogramming/datetime/mp_time.py +147 -0
  16. multilingualprogramming/datetime/resource_loader.py +18 -0
  17. multilingualprogramming/exceptions.py +158 -0
  18. multilingualprogramming/imports.py +150 -0
  19. multilingualprogramming/keyword/__init__.py +13 -0
  20. multilingualprogramming/keyword/keyword_registry.py +249 -0
  21. multilingualprogramming/keyword/keyword_validator.py +59 -0
  22. multilingualprogramming/keyword/language_pack_validator.py +110 -0
  23. multilingualprogramming/lexer/__init__.py +11 -0
  24. multilingualprogramming/lexer/lexer.py +570 -0
  25. multilingualprogramming/lexer/source_reader.py +91 -0
  26. multilingualprogramming/lexer/token.py +54 -0
  27. multilingualprogramming/lexer/token_types.py +38 -0
  28. multilingualprogramming/numeral/__init__.py +11 -0
  29. multilingualprogramming/numeral/abstract_numeral.py +232 -0
  30. multilingualprogramming/numeral/complex_numeral.py +190 -0
  31. multilingualprogramming/numeral/fraction_numeral.py +165 -0
  32. multilingualprogramming/numeral/mp_numeral.py +243 -0
  33. multilingualprogramming/numeral/numeral_converter.py +151 -0
  34. multilingualprogramming/numeral/roman_numeral.py +301 -0
  35. multilingualprogramming/numeral/unicode_numeral.py +292 -0
  36. multilingualprogramming/parser/__init__.py +28 -0
  37. multilingualprogramming/parser/ast_nodes.py +459 -0
  38. multilingualprogramming/parser/ast_printer.py +677 -0
  39. multilingualprogramming/parser/error_messages.py +75 -0
  40. multilingualprogramming/parser/parser.py +1796 -0
  41. multilingualprogramming/parser/semantic_analyzer.py +689 -0
  42. multilingualprogramming/parser/surface_normalizer.py +282 -0
  43. multilingualprogramming/resources/datetime/eras.json +23 -0
  44. multilingualprogramming/resources/datetime/formats.json +32 -0
  45. multilingualprogramming/resources/datetime/months.json +150 -0
  46. multilingualprogramming/resources/datetime/weekdays.json +90 -0
  47. multilingualprogramming/resources/parser/error_messages.json +310 -0
  48. multilingualprogramming/resources/repl/commands.json +636 -0
  49. multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
  50. multilingualprogramming/resources/usm/keywords.json +1063 -0
  51. multilingualprogramming/resources/usm/operators.json +532 -0
  52. multilingualprogramming/resources/usm/schema.json +34 -0
  53. multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
  54. multilingualprogramming/unicode_string.py +140 -0
  55. multilingualprogramming/version.py +9 -0
  56. multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
  57. multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
  58. multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
  59. multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
  60. multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
  61. 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}")'