🐐 Fractions
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import re
|
||||
import random
|
||||
from fractions import Fraction
|
||||
|
||||
from rply.token import BaseBox
|
||||
|
||||
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValNul, ValFunc
|
||||
from centvrion.errors import CentvrionError
|
||||
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac
|
||||
|
||||
NUMERALS = {
|
||||
"I": 1,
|
||||
@@ -34,7 +36,7 @@ def rep_join(l):
|
||||
OP_STR = {
|
||||
"SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-",
|
||||
"SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/",
|
||||
"SYMBOL_COLON": ":",
|
||||
"SYMBOL_AMPERSAND": "&",
|
||||
"KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS",
|
||||
"KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT",
|
||||
}
|
||||
@@ -42,12 +44,12 @@ OP_STR = {
|
||||
def single_num_to_int(i, m):
|
||||
if i[-1] == "_":
|
||||
if not m:
|
||||
raise ValueError(
|
||||
raise CentvrionError(
|
||||
"Cannot calculate numbers above 3999 without 'MAGNVM' module"
|
||||
)
|
||||
|
||||
if i[0] != "I":
|
||||
raise ValueError(
|
||||
raise CentvrionError(
|
||||
"Cannot use 'I' with thousands operator, use 'M' instead"
|
||||
)
|
||||
|
||||
@@ -58,16 +60,16 @@ def single_num_to_int(i, m):
|
||||
def num_to_int(n, m, s=False):
|
||||
if n.startswith("-"):
|
||||
if not s:
|
||||
raise ValueError("Cannot use negative numbers without 'SVBNVLLA' module")
|
||||
raise CentvrionError("Cannot use negative numbers without 'SVBNVLLA' module")
|
||||
return -num_to_int(n[1:], m, s)
|
||||
chars = re.findall(r"[IVXLCDM]_*", n)
|
||||
if ''.join(chars) != n:
|
||||
raise ValueError("Invalid numeral", n)
|
||||
raise CentvrionError(f"Invalid numeral: {n!r}")
|
||||
nums = [single_num_to_int(i, m) for i in chars]
|
||||
new_nums = nums.copy()
|
||||
for x, num in enumerate(nums[:-3]):
|
||||
if all(num == nums[x+i] for i in range(0,4)):
|
||||
raise ValueError(n, "is not a valid roman numeral")
|
||||
raise CentvrionError(f"{n!r} is not a valid roman numeral")
|
||||
|
||||
while True:
|
||||
for x, num in enumerate(nums[:-1]):
|
||||
@@ -77,7 +79,7 @@ def num_to_int(n, m, s=False):
|
||||
new_nums[x+1] = 0
|
||||
break
|
||||
else:
|
||||
raise ValueError(n, "is not a valid roman numeral")
|
||||
raise CentvrionError(f"{n!r} is not a valid roman numeral")
|
||||
|
||||
if new_nums != nums:
|
||||
nums = [i for i in new_nums if i != 0]
|
||||
@@ -86,18 +88,18 @@ def num_to_int(n, m, s=False):
|
||||
break
|
||||
|
||||
if nums != sorted(nums)[::-1]:
|
||||
raise Exception(n, "is not a valid roman numeral")
|
||||
raise CentvrionError(f"{n!r} is not a valid roman numeral")
|
||||
|
||||
return sum(nums)
|
||||
|
||||
def int_to_num(n, m, s=False):
|
||||
if n < 0:
|
||||
if not s:
|
||||
raise ValueError("Cannot display negative numbers without 'SVBNVLLA' module")
|
||||
raise CentvrionError("Cannot display negative numbers without 'SVBNVLLA' module")
|
||||
return "-" + int_to_num(-n, m, s)
|
||||
if n > 3999:
|
||||
if not m:
|
||||
raise ValueError(
|
||||
raise CentvrionError(
|
||||
"Cannot display numbers above 3999 without 'MAGNVM' module"
|
||||
)
|
||||
thousands_chars = re.findall(r"[IVXLCDM]_*", int_to_num(n//1000, m, s))
|
||||
@@ -123,6 +125,8 @@ def make_string(val, magnvm=False, svbnvlla=False):
|
||||
return val.value()
|
||||
elif isinstance(val, ValInt):
|
||||
return int_to_num(val.value(), magnvm, svbnvlla)
|
||||
elif isinstance(val, ValFrac):
|
||||
return fraction_to_frac(val.value(), magnvm, svbnvlla)
|
||||
elif isinstance(val, ValBool):
|
||||
return "VERVS" if val.value() else "FALSVS"
|
||||
elif isinstance(val, ValNul):
|
||||
@@ -131,7 +135,62 @@ def make_string(val, magnvm=False, svbnvlla=False):
|
||||
inner = ' '.join(make_string(i, magnvm, svbnvlla) for i in val.value())
|
||||
return f"[{inner}]"
|
||||
else:
|
||||
raise TypeError(f"Cannot display {val!r}")
|
||||
raise CentvrionError(f"Cannot display {val!r}")
|
||||
|
||||
FRAC_SYMBOLS = [("S", 6), (":", 2), (".", 1)]
|
||||
|
||||
def frac_to_fraction(s, magnvm=False, svbnvlla=False):
|
||||
match = re.match(r'^([IVXLCDM][IVXLCDM_]*)(.*)', s)
|
||||
if match:
|
||||
int_str, frac_str = match.group(1), match.group(2)
|
||||
total = Fraction(num_to_int(int_str, magnvm, svbnvlla))
|
||||
else:
|
||||
total = Fraction(0)
|
||||
frac_str = s
|
||||
|
||||
for level_idx, level in enumerate(frac_str.split('|')):
|
||||
divisor = 12 ** (level_idx + 1)
|
||||
level_val = 0
|
||||
prev_weight = 12
|
||||
for ch in level:
|
||||
weight = next((w for sym, w in FRAC_SYMBOLS if sym == ch), None)
|
||||
if weight is None:
|
||||
raise CentvrionError(f"Invalid fraction symbol: {ch!r}")
|
||||
if weight > prev_weight:
|
||||
raise CentvrionError("Fraction symbols must be written highest to lowest")
|
||||
prev_weight = weight
|
||||
level_val += weight
|
||||
if level_val >= 12:
|
||||
raise CentvrionError("Fraction level value must be less than 1 (< 12/12)")
|
||||
total += Fraction(level_val, divisor)
|
||||
|
||||
return total
|
||||
|
||||
def fraction_to_frac(f, magnvm=False, svbnvlla=False):
|
||||
if f < 0:
|
||||
if not svbnvlla:
|
||||
raise CentvrionError("Cannot display negative fractions without 'SVBNVLLA' module")
|
||||
return "-" + fraction_to_frac(-f, magnvm, svbnvlla)
|
||||
|
||||
integer_part = int(f)
|
||||
remainder = f - integer_part
|
||||
int_str = int_to_num(integer_part, magnvm, svbnvlla) if integer_part > 0 else ""
|
||||
|
||||
levels = []
|
||||
current = remainder
|
||||
for _ in range(6):
|
||||
if current == 0:
|
||||
break
|
||||
current = current * 12
|
||||
level_int = int(current)
|
||||
current = current - level_int
|
||||
s_count = level_int // 6
|
||||
remaining = level_int % 6
|
||||
colon_count = remaining // 2
|
||||
dot_count = remaining % 2
|
||||
levels.append("S" * s_count + ":" * colon_count + "." * dot_count)
|
||||
|
||||
return int_str + "|".join(levels)
|
||||
|
||||
|
||||
class Node(BaseBox):
|
||||
@@ -200,6 +259,8 @@ class DataRangeArray(Node):
|
||||
def _eval(self, vtable):
|
||||
vtable, from_val = self.from_value.eval(vtable)
|
||||
vtable, to_val = self.to_value.eval(vtable)
|
||||
if not isinstance(from_val, ValInt) or not isinstance(to_val, ValInt):
|
||||
raise CentvrionError("Range bounds must be numbers")
|
||||
return vtable, ValList([ValInt(i) for i in range(from_val.value(), to_val.value())])
|
||||
|
||||
|
||||
@@ -237,6 +298,27 @@ class Numeral(Node):
|
||||
return vtable, ValInt(num_to_int(self.value, "MAGNVM" in vtable["#modules"], "SVBNVLLA" in vtable["#modules"]))
|
||||
|
||||
|
||||
class Fractio(Node):
|
||||
def __init__(self, value) -> None:
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.value == other.value
|
||||
|
||||
def __repr__(self):
|
||||
return f"Fractio({self.value!r})"
|
||||
|
||||
def print(self):
|
||||
return self.value
|
||||
|
||||
def _eval(self, vtable):
|
||||
if "FRACTIO" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use fraction literals without 'FRACTIO' module")
|
||||
m = "MAGNVM" in vtable["#modules"]
|
||||
s = "SVBNVLLA" in vtable["#modules"]
|
||||
return vtable, ValFrac(frac_to_fraction(self.value, m, s))
|
||||
|
||||
|
||||
class Bool(Node):
|
||||
def __init__(self, value) -> None:
|
||||
self.value = value
|
||||
@@ -282,6 +364,8 @@ class ID(Node):
|
||||
return self.name
|
||||
|
||||
def _eval(self, vtable):
|
||||
if self.name not in vtable:
|
||||
raise CentvrionError(f"Undefined variable: {self.name}")
|
||||
return vtable, vtable[self.name]
|
||||
|
||||
|
||||
@@ -325,10 +409,15 @@ class DesignaIndex(Node):
|
||||
def _eval(self, vtable):
|
||||
vtable, index = self.index.eval(vtable)
|
||||
vtable, val = self.value.eval(vtable)
|
||||
if self.id.name not in vtable:
|
||||
raise CentvrionError(f"Undefined variable: {self.id.name}")
|
||||
target = vtable[self.id.name]
|
||||
if not isinstance(target, ValList):
|
||||
raise CentvrionError(f"{self.id.name} is not an array")
|
||||
i = index.value()
|
||||
lst = list(vtable[self.id.name].value())
|
||||
lst = list(target.value())
|
||||
if i < 1 or i > len(lst):
|
||||
raise IndexError(f"Index {i} out of range for array of length {len(lst)}")
|
||||
raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
|
||||
lst[i - 1] = val
|
||||
vtable[self.id.name] = ValList(lst)
|
||||
return vtable, ValNul()
|
||||
@@ -440,29 +529,49 @@ class BinOp(Node):
|
||||
match self.op:
|
||||
case "SYMBOL_PLUS":
|
||||
if isinstance(lv, str) or isinstance(rv, str):
|
||||
raise TypeError("Use : for string concatenation, not +")
|
||||
raise CentvrionError("Use & for string concatenation, not +")
|
||||
if isinstance(lv, list) or isinstance(rv, list):
|
||||
raise CentvrionError("Cannot use + on arrays")
|
||||
if lv is None and rv is None:
|
||||
return vtable, ValNul()
|
||||
return vtable, ValInt((lv or 0) + (rv or 0))
|
||||
case "SYMBOL_COLON":
|
||||
result = (lv or 0) + (rv or 0)
|
||||
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
|
||||
case "SYMBOL_AMPERSAND":
|
||||
magnvm = "MAGNVM" in vtable["#modules"]
|
||||
svbnvlla = "SVBNVLLA" in vtable["#modules"]
|
||||
def _coerce(v):
|
||||
if v is None: return ""
|
||||
if isinstance(v, bool): return "VERITAS" if v else "FALSITAS"
|
||||
if isinstance(v, int): return int_to_num(v, magnvm, svbnvlla)
|
||||
if isinstance(v, Fraction): return fraction_to_frac(v, magnvm, svbnvlla)
|
||||
if isinstance(v, list): return make_string(ValList(v), magnvm, svbnvlla)
|
||||
return v
|
||||
return vtable, ValStr(_coerce(lv) + _coerce(rv))
|
||||
case "SYMBOL_MINUS":
|
||||
return vtable, ValInt((lv or 0) - (rv or 0))
|
||||
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
|
||||
raise CentvrionError("Cannot use - on strings or arrays")
|
||||
result = (lv or 0) - (rv or 0)
|
||||
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
|
||||
case "SYMBOL_TIMES":
|
||||
return vtable, ValInt((lv or 0) * (rv or 0))
|
||||
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
|
||||
raise CentvrionError("Cannot use * on strings or arrays")
|
||||
result = (lv or 0) * (rv or 0)
|
||||
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
|
||||
case "SYMBOL_DIVIDE":
|
||||
# TODO: Fractio
|
||||
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
|
||||
raise CentvrionError("Cannot use / on strings or arrays")
|
||||
if (rv or 0) == 0:
|
||||
raise CentvrionError("Division by zero")
|
||||
if isinstance(lv, Fraction) or isinstance(rv, Fraction) or "FRACTIO" in vtable["#modules"]:
|
||||
return vtable, ValFrac(Fraction(lv or 0) / Fraction(rv or 0))
|
||||
return vtable, ValInt((lv or 0) // (rv or 0))
|
||||
case "KEYWORD_MINVS":
|
||||
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
|
||||
raise CentvrionError("Cannot compare strings or arrays with MINVS")
|
||||
return vtable, ValBool((lv or 0) < (rv or 0))
|
||||
case "KEYWORD_PLVS":
|
||||
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
|
||||
raise CentvrionError("Cannot compare strings or arrays with PLVS")
|
||||
return vtable, ValBool((lv or 0) > (rv or 0))
|
||||
case "KEYWORD_EST":
|
||||
return vtable, ValBool(lv == rv)
|
||||
@@ -489,8 +598,10 @@ class UnaryMinus(Node):
|
||||
|
||||
def _eval(self, vtable):
|
||||
if "SVBNVLLA" not in vtable["#modules"]:
|
||||
raise ValueError("Cannot use unary minus without 'SVBNVLLA' module")
|
||||
raise CentvrionError("Cannot use unary minus without 'SVBNVLLA' module")
|
||||
vtable, val = self.expr.eval(vtable)
|
||||
if not isinstance(val, ValInt):
|
||||
raise CentvrionError("Unary minus requires a number")
|
||||
return vtable, ValInt(-val.value())
|
||||
|
||||
|
||||
@@ -529,10 +640,14 @@ class ArrayIndex(Node):
|
||||
def _eval(self, vtable):
|
||||
vtable, array = self.array.eval(vtable)
|
||||
vtable, index = self.index.eval(vtable)
|
||||
if not isinstance(array, ValList):
|
||||
raise CentvrionError("Cannot index a non-array value")
|
||||
if not isinstance(index, ValInt):
|
||||
raise CentvrionError("Array index must be a number")
|
||||
i = index.value()
|
||||
lst = array.value()
|
||||
if i < 1 or i > len(lst):
|
||||
raise IndexError(f"Index {i} out of range for array of length {len(lst)}")
|
||||
raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
|
||||
return vtable, lst[i - 1]
|
||||
|
||||
|
||||
@@ -628,6 +743,8 @@ class PerStatement(Node):
|
||||
|
||||
def _eval(self, vtable):
|
||||
vtable, array = self.data_list.eval(vtable)
|
||||
if not isinstance(array, ValList):
|
||||
raise CentvrionError("PER requires an array")
|
||||
variable_name = self.variable_name.name
|
||||
last_val = ValNul()
|
||||
for item in array:
|
||||
@@ -662,9 +779,13 @@ class Invoca(Node):
|
||||
|
||||
def _eval(self, vtable):
|
||||
params = [p.eval(vtable)[1] for p in self.parameters]
|
||||
if self.name.name not in vtable:
|
||||
raise CentvrionError(f"Undefined function: {self.name.name}")
|
||||
func = vtable[self.name.name]
|
||||
if not isinstance(func, ValFunc):
|
||||
raise CentvrionError(f"{self.name.name} is not a function")
|
||||
if len(params) != len(func.params):
|
||||
raise TypeError(
|
||||
raise CentvrionError(
|
||||
f"{self.name.name} expects {len(func.params)} argument(s), got {len(params)}"
|
||||
)
|
||||
func_vtable = vtable.copy()
|
||||
@@ -702,7 +823,11 @@ class BuiltIn(Node):
|
||||
|
||||
match self.builtin:
|
||||
case "AVDI_NVMERVS":
|
||||
return vtable, ValInt(num_to_int(input(), magnvm, svbnvlla))
|
||||
raw = input()
|
||||
try:
|
||||
return vtable, ValInt(num_to_int(raw, magnvm, svbnvlla))
|
||||
except ValueError:
|
||||
raise CentvrionError(f"Invalid numeral input: {raw!r}")
|
||||
case "AVDI":
|
||||
return vtable, ValStr(input())
|
||||
case "DICE":
|
||||
@@ -716,17 +841,25 @@ class BuiltIn(Node):
|
||||
return vtable, ValNul()
|
||||
case "FORTIS_NVMERVS":
|
||||
if "FORS" not in vtable["#modules"]:
|
||||
raise ValueError(
|
||||
"Cannot use 'FORTIS_NVMERVS' without module 'FORS'"
|
||||
)
|
||||
return vtable, ValInt(random.randint(params[0].value(), params[1].value()))
|
||||
raise CentvrionError("Cannot use 'FORTIS_NVMERVS' without module 'FORS'")
|
||||
a, b = params[0].value(), params[1].value()
|
||||
if not isinstance(a, int) or not isinstance(b, int):
|
||||
raise CentvrionError("FORTIS_NVMERVS requires two numbers")
|
||||
if a > b:
|
||||
raise CentvrionError(f"FORTIS_NVMERVS: first argument ({a}) must be ≤ second ({b})")
|
||||
return vtable, ValInt(random.randint(a, b))
|
||||
case "FORTIS_ELECTIONIS":
|
||||
if "FORS" not in vtable["#modules"]:
|
||||
raise ValueError(
|
||||
"Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'"
|
||||
)
|
||||
return vtable, params[0].value()[random.randint(0, len(params[0].value()) - 1)]
|
||||
raise CentvrionError("Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'")
|
||||
if not isinstance(params[0], ValList):
|
||||
raise CentvrionError("FORTIS_ELECTIONIS requires an array")
|
||||
lst = params[0].value()
|
||||
if len(lst) == 0:
|
||||
raise CentvrionError("FORTIS_ELECTIONIS: cannot select from an empty array")
|
||||
return vtable, lst[random.randint(0, len(lst) - 1)]
|
||||
case "LONGITVDO":
|
||||
if not isinstance(params[0], ValList):
|
||||
raise CentvrionError("LONGITVDO requires an array")
|
||||
return vtable, ValInt(len(params[0].value()))
|
||||
case _:
|
||||
raise NotImplementedError(self.builtin)
|
||||
|
||||
Reference in New Issue
Block a user