🐐 Fractions
This commit is contained in:
189
tests.py
189
tests.py
@@ -4,16 +4,20 @@ from io import StringIO
|
||||
from unittest.mock import patch
|
||||
from parameterized import parameterized
|
||||
|
||||
from fractions import Fraction
|
||||
|
||||
from centvrion.ast_nodes import (
|
||||
ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini,
|
||||
Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID,
|
||||
Invoca, ModuleCall, Nullus, Numeral, PerStatement,
|
||||
Program, Redi, SiStatement, String, UnaryMinus, UnaryNot,
|
||||
Fractio, frac_to_fraction, fraction_to_frac,
|
||||
num_to_int, int_to_num, make_string,
|
||||
)
|
||||
from centvrion.errors import CentvrionError
|
||||
from centvrion.lexer import Lexer
|
||||
from centvrion.parser import Parser
|
||||
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc
|
||||
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac
|
||||
|
||||
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
|
||||
random.seed(1)
|
||||
@@ -368,22 +372,35 @@ class TestBuiltins(unittest.TestCase):
|
||||
# --- Errors ---
|
||||
|
||||
error_tests = [
|
||||
("x", KeyError), # undefined variable
|
||||
("INVOCA f ()", KeyError), # undefined function
|
||||
("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA
|
||||
("DESIGNA x III", SyntaxError), # parse error: missing VT
|
||||
("DICE(M + M + M + M)", ValueError), # output > 3999 without MAGNVM
|
||||
("IIII", ValueError), # invalid Roman numeral in source
|
||||
("FORTIS_NVMERVS(I, X)", ValueError), # requires FORS module
|
||||
("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", TypeError), # too many args
|
||||
("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", TypeError), # too few args
|
||||
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", TypeError), # args to zero-param function
|
||||
("SI NVLLVS TVNC { DESIGNA r VT I }", TypeError), # NVLLVS cannot be used as boolean
|
||||
("NVLLVS AVT VERITAS", TypeError), # NVLLVS cannot be used as boolean in AVT
|
||||
('"hello" + " world"', TypeError), # use : for string concatenation, not +
|
||||
("[I, II][III]", IndexError), # index too high
|
||||
("CVM SVBNVLLA\n[I, II][-I]", IndexError), # negative index
|
||||
("[I, II][-I]", ValueError), # negative value
|
||||
("x", CentvrionError), # undefined variable
|
||||
("INVOCA f ()", CentvrionError), # undefined function
|
||||
("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA
|
||||
("DESIGNA x III", SyntaxError), # parse error: missing VT
|
||||
("DICE(M + M + M + M)", CentvrionError), # output > 3999 without MAGNVM
|
||||
("IIII", CentvrionError), # invalid Roman numeral in source
|
||||
("FORTIS_NVMERVS(I, X)", CentvrionError), # requires FORS module
|
||||
("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", CentvrionError), # too many args
|
||||
("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", CentvrionError), # too few args
|
||||
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function
|
||||
("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean
|
||||
("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT
|
||||
('"hello" + " world"', CentvrionError), # use : for string concatenation, not +
|
||||
("[I, II][III]", CentvrionError), # index too high
|
||||
("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index
|
||||
("[I, II][-I]", CentvrionError), # negative value
|
||||
("I / NVLLVS", CentvrionError), # division by zero (NVLLVS coerces to 0)
|
||||
("I / [I, II]", CentvrionError), # division with array operand
|
||||
("I - \"hello\"", CentvrionError), # subtraction with string
|
||||
("I * \"hello\"", CentvrionError), # multiplication with string
|
||||
("\"hello\" MINVS \"world\"", CentvrionError), # comparison with strings
|
||||
("I[I]", CentvrionError), # indexing a non-array
|
||||
("DESIGNA x VT I\nDESIGNA x[I] VT II", CentvrionError), # index-assign to non-array
|
||||
("FORTIS_ELECTIONIS([])", CentvrionError), # FORS required for FORTIS_ELECTIONIS
|
||||
("CVM FORS\nFORTIS_ELECTIONIS([])", CentvrionError), # FORTIS_ELECTIONIS on empty array
|
||||
("CVM FORS\nFORTIS_NVMERVS(X, I)", CentvrionError), # FORTIS_NVMERVS a > b
|
||||
("PER i IN I FACE { DICE(i) }", CentvrionError), # PER over non-array
|
||||
("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array
|
||||
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
|
||||
]
|
||||
|
||||
class TestErrors(unittest.TestCase):
|
||||
@@ -463,7 +480,7 @@ class TestNumerals(unittest.TestCase):
|
||||
num_to_int("IM", False)
|
||||
|
||||
def test_negative_without_svbnvlla_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(CentvrionError):
|
||||
num_to_int("-IV", False)
|
||||
|
||||
def test_negative_with_svbnvlla(self):
|
||||
@@ -606,14 +623,14 @@ class TestArithmeticEdge(unittest.TestCase):
|
||||
# --- String concatenation ---
|
||||
|
||||
string_concat_tests = [
|
||||
('"hello" : " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_COLON"))]), ValStr("hello world")),
|
||||
('"hello" & " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_AMPERSAND"))]), ValStr("hello world")),
|
||||
# NVLLVS coerces to "" in string context
|
||||
('NVLLVS : "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_COLON"))]), ValStr("hello")),
|
||||
('"hello" : NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_COLON"))]), ValStr("hello")),
|
||||
('NVLLVS : NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_COLON"))]), ValStr("")),
|
||||
('NVLLVS & "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
|
||||
('"hello" & NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
|
||||
('NVLLVS & NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("")),
|
||||
# integers coerce to Roman numerals in string context
|
||||
('"value: " : V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_COLON"))]), ValStr("value: V")),
|
||||
('X : " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_COLON"))]), ValStr("X items")),
|
||||
('"value: " & V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_AMPERSAND"))]), ValStr("value: V")),
|
||||
('X & " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_AMPERSAND"))]), ValStr("X items")),
|
||||
]
|
||||
|
||||
class TestStringConcat(unittest.TestCase):
|
||||
@@ -1174,5 +1191,129 @@ class TestNon(unittest.TestCase):
|
||||
run_test(self, source, nodes, value)
|
||||
|
||||
|
||||
# --- FRACTIO module ---
|
||||
|
||||
fractio_tests = [
|
||||
# Basic fraction literals
|
||||
("CVM FRACTIO\nIIIS",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("IIIS"))]),
|
||||
ValFrac(Fraction(7, 2))),
|
||||
("CVM FRACTIO\nS",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S"))]),
|
||||
ValFrac(Fraction(1, 2))),
|
||||
("CVM FRACTIO\nS:.",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S:."))]),
|
||||
ValFrac(Fraction(3, 4))),
|
||||
("CVM FRACTIO\n.",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("."))]),
|
||||
ValFrac(Fraction(1, 12))),
|
||||
("CVM FRACTIO\n:.",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio(":."))]),
|
||||
ValFrac(Fraction(1, 4))),
|
||||
# Integer part with fraction
|
||||
("CVM FRACTIO\nVIIS:|::",
|
||||
Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("VIIS:|::"))]),
|
||||
ValFrac(Fraction(7) + Fraction(100, 144))),
|
||||
# Arithmetic
|
||||
("CVM FRACTIO\nIIIS + S",
|
||||
None, ValFrac(Fraction(4))),
|
||||
("CVM FRACTIO\nIIIS - S",
|
||||
None, ValFrac(Fraction(3))),
|
||||
("CVM FRACTIO\nS * IV",
|
||||
None, ValFrac(Fraction(2))),
|
||||
# Division returns fraction
|
||||
("CVM FRACTIO\nI / IV",
|
||||
None, ValFrac(Fraction(1, 4))),
|
||||
("CVM FRACTIO\nI / III",
|
||||
None, ValFrac(Fraction(1, 3))),
|
||||
# Integer division still works without fractions in operands... but with FRACTIO returns ValFrac
|
||||
("CVM FRACTIO\nX / II",
|
||||
None, ValFrac(Fraction(5))),
|
||||
# String concatenation with fraction
|
||||
("CVM FRACTIO\nDICE(IIIS & \"!\")",
|
||||
None, ValStr("IIIS!"), "IIIS!\n"),
|
||||
]
|
||||
|
||||
class TestFractio(unittest.TestCase):
|
||||
@parameterized.expand(fractio_tests)
|
||||
def test_fractio(self, source, nodes, value, output=""):
|
||||
run_test(self, source, nodes, value, output)
|
||||
|
||||
|
||||
fractio_comparison_tests = [
|
||||
# fraction vs fraction
|
||||
("CVM FRACTIO\nIIIS PLVS III", None, ValBool(True)), # 3.5 > 3
|
||||
("CVM FRACTIO\nIII MINVS IIIS", None, ValBool(True)), # 3 < 3.5
|
||||
("CVM FRACTIO\nIIIS MINVS IV", None, ValBool(True)), # 3.5 < 4
|
||||
("CVM FRACTIO\nIV PLVS IIIS", None, ValBool(True)), # 4 > 3.5
|
||||
("CVM FRACTIO\nIIIS PLVS IIIS", None, ValBool(False)), # 3.5 > 3.5 is False
|
||||
("CVM FRACTIO\nIIIS MINVS IIIS", None, ValBool(False)), # 3.5 < 3.5 is False
|
||||
# equality: fraction == fraction
|
||||
("CVM FRACTIO\nIIIS EST IIIS", None, ValBool(True)),
|
||||
("CVM FRACTIO\nIIIS EST IV", None, ValBool(False)),
|
||||
# equality: fraction == whole number (ValFrac(4) vs ValInt(4))
|
||||
("CVM FRACTIO\nIIIS + S EST IV", None, ValBool(True)), # 3.5+0.5 == 4
|
||||
("CVM FRACTIO\nS + S EST I", None, ValBool(True)), # 0.5+0.5 == 1
|
||||
]
|
||||
|
||||
class TestFractioComparisons(unittest.TestCase):
|
||||
@parameterized.expand(fractio_comparison_tests)
|
||||
def test_fractio_comparison(self, source, nodes, value):
|
||||
run_test(self, source, nodes, value)
|
||||
|
||||
|
||||
class TestFractioErrors(unittest.TestCase):
|
||||
def test_fraction_without_module(self):
|
||||
source = "IIIS\n"
|
||||
lexer = Lexer().get_lexer()
|
||||
tokens = lexer.lex(source)
|
||||
program = Parser().parse(tokens)
|
||||
with self.assertRaises(CentvrionError):
|
||||
program.eval()
|
||||
|
||||
def test_frac_to_fraction_ordering(self):
|
||||
with self.assertRaises(CentvrionError):
|
||||
frac_to_fraction(".S") # . before S violates highest-to-lowest
|
||||
|
||||
def test_frac_to_fraction_level_overflow(self):
|
||||
with self.assertRaises(CentvrionError):
|
||||
frac_to_fraction("SSSSSS") # 6*S = 36/12 >= 1 per level... wait S can only appear once
|
||||
# Actually "SS" means S twice, which is 12/12 = 1, violating < 12/12 constraint
|
||||
|
||||
|
||||
class TestFractioHelpers(unittest.TestCase):
|
||||
def test_frac_to_fraction_iiis(self):
|
||||
self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2))
|
||||
|
||||
def test_frac_to_fraction_s_colon_dot(self):
|
||||
self.assertEqual(frac_to_fraction("S:."), Fraction(3, 4))
|
||||
|
||||
def test_frac_to_fraction_dot(self):
|
||||
self.assertEqual(frac_to_fraction("."), Fraction(1, 12))
|
||||
|
||||
def test_frac_to_fraction_multilevel(self):
|
||||
self.assertEqual(frac_to_fraction("VIIS:|::"), Fraction(7) + Fraction(100, 144))
|
||||
|
||||
def test_fraction_to_frac_iiis(self):
|
||||
self.assertEqual(fraction_to_frac(Fraction(7, 2)), "IIIS")
|
||||
|
||||
def test_fraction_to_frac_s_colon_dot(self):
|
||||
self.assertEqual(fraction_to_frac(Fraction(3, 4)), "S:.")
|
||||
|
||||
def test_fraction_to_frac_dot(self):
|
||||
self.assertEqual(fraction_to_frac(Fraction(1, 12)), ".")
|
||||
|
||||
def test_fraction_to_frac_multilevel(self):
|
||||
self.assertEqual(
|
||||
fraction_to_frac(Fraction(7) + Fraction(100, 144)),
|
||||
"VIIS:|::"
|
||||
)
|
||||
|
||||
def test_roundtrip(self):
|
||||
# Only canonical forms roundtrip — fraction_to_frac always uses max colons before dots
|
||||
for s in ["IIIS", "S:.", ".", "::", "VIIS:|::", "S"]:
|
||||
self.assertEqual(fraction_to_frac(frac_to_fraction(s)), s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user