diff --git a/CHANGES b/CHANGES index ea87492..c30c7b9 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,9 @@ plusminus Change Log using (), (], [) or [] notation ("in" with sets is still supported). + - Deleted the example_parsers.py module in the examples directory, and split + the parsers out into separate modules in that directory. + - Added __version_info__ structure, following pattern of sys.version_info field names. diff --git a/plusminus/examples/business_arithmetic_parser.py b/plusminus/examples/business_arithmetic_parser.py new file mode 100644 index 0000000..14ba375 --- /dev/null +++ b/plusminus/examples/business_arithmetic_parser.py @@ -0,0 +1,63 @@ +# +# business_arithmetic_parser.py +# +# Copyright 2021, Paul McGuire +# +from plusminus import BaseArithmeticParser, safe_pow + + +class BusinessArithmeticParser(BaseArithmeticParser): + """ + A parser for evaluating common financial and retail calculations: + + 50% of 20 + 20 * (1-20%) + (100-20)% of 20 + 5 / 20% + FV(20000, 3%, 30) + FV(20000, 3%/12, 30*12) + + Functions: + FV(present_value, rate_per_period, number_of_periods) + future value of an amount, n periods in the future, at an interest rate per period + PV(future_value, rate_per_period, number_of_periods) + present value of a future amount, n periods in the future, at an interest rate per period + PP(present_value, rate_per_period, number_of_periods) + periodic value of n amounts, one per period, for n periods, at an interest rate per period + """ + + def customize(self): + def pv(future_value, rate, n_periods): + return future_value / safe_pow(1 + rate, n_periods) + + def fv(present_value, rate, n_periods): + return present_value * safe_pow(1 + rate, n_periods) + + def pp(present_value, rate, n_periods): + return rate * present_value / (1 - safe_pow(1 + rate, -n_periods)) + + super().customize() + self.add_operator("of", 2, BaseArithmeticParser.LEFT, lambda a, b: a * b) + self.add_operator("%", 1, BaseArithmeticParser.LEFT, lambda a: a / 100) + + self.add_function("PV", 3, pv) + self.add_function("FV", 3, fv) + self.add_function("PP", 3, pp) + + +if __name__ == '__main__': + + parser = BusinessArithmeticParser() + parser.runTests( + """\ + 25% + 20 * 50% + 50% of 20 + 20 * (1-20%) + (100-20)% of 20 + 5 / 20% + FV(20000, 3%, 30) + FV(20000, 3%/12, 30*12) + """, + postParse=lambda _, result: result[0].evaluate(), + ) diff --git a/plusminus/examples/combinatorics_arithmetic_parser.py b/plusminus/examples/combinatorics_arithmetic_parser.py new file mode 100644 index 0000000..54b0eec --- /dev/null +++ b/plusminus/examples/combinatorics_arithmetic_parser.py @@ -0,0 +1,50 @@ +# +# combinatorics_arithmetic_parser.py +# +# Copyright 2021, Paul McGuire +# +from plusminus import BaseArithmeticParser, ArithmeticParser, constrained_factorial + + +class CombinatoricsArithmeticParser(BaseArithmeticParser): + """ + Parser for evaluating expressions of combinatorics problems, for numbers of + permutations (nPm) and combinations (nCm): + + nPm = n! / (n-m)! + 8P4 = number of (ordered) permutations of selecting 4 items from a collection of 8 + + nCm = n! / m!(n-m)! + 8C4 = number of (unordered) combinations of selecting 4 items from a collection of 8 + """ + + def customize(self): + super().customize() + # fmt: off + self.add_operator("P", 2, BaseArithmeticParser.LEFT, + lambda a, b: int(constrained_factorial(a) / constrained_factorial(a - b))) + self.add_operator("C", 2, BaseArithmeticParser.LEFT, + lambda a, b: int(constrained_factorial(a) + / constrained_factorial(b) + / constrained_factorial(a - b))) + self.add_operator(*ArithmeticParser.Operators.FACTORIAL) + # fmt: on + + +if __name__ == '__main__': + + parser = CombinatoricsArithmeticParser() + parser.runTests( + """\ + 3! + -3! + 3!! + 6! / (6-2)! + 6 P 2 + 6! / (2!*(6-2)!) + 6 C 2 + 6P6 + 6C6 + """, + postParse=lambda _, result: result[0].evaluate(), + ) diff --git a/plusminus/examples/date_time_arithmetic_parser.py b/plusminus/examples/date_time_arithmetic_parser.py new file mode 100644 index 0000000..9be8d29 --- /dev/null +++ b/plusminus/examples/date_time_arithmetic_parser.py @@ -0,0 +1,59 @@ +# +# date_time_arithmetic_parser.py +# +# Copyright 2021, Paul McGuire +# +from plusminus import BaseArithmeticParser + + +class DateTimeArithmeticParser(BaseArithmeticParser): + """ + Parser for evaluating expressions in dates and times, using operators d, h, m, and s + to define terms for amounts of days, hours, minutes, and seconds: + + now() + today() + now() + 10s + now() + 24h + + All numeric expressions will be treated as UTC integer timestamps. To display + timestamps as ISO strings, use str(): + + str(now()) + str(today() + 3d) + """ + + SECONDS_PER_MINUTE = 60 + SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60 + SECONDS_PER_DAY = SECONDS_PER_HOUR * 24 + + def customize(self): + from datetime import datetime + + # fmt: off + self.add_operator("d", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_DAY) + self.add_operator("h", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_HOUR) + self.add_operator("m", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_MINUTE) + self.add_operator("s", 1, BaseArithmeticParser.LEFT, lambda t: t) + + self.add_function("now", 0, lambda: datetime.utcnow().timestamp()) + self.add_function("today", 0, + lambda: datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + self.add_function("str", 1, lambda dt: str(datetime.fromtimestamp(dt))) + # fmt: on + + +if __name__ == "__main__": + + parser = DateTimeArithmeticParser() + parser.runTests( + """\ + now() + str(now()) + str(today()) + "A day from now: " + str(now() + 1d) + "A day and an hour from now: " + str(now() + 1d + 1h) + str(now() + 3*(1d + 1h)) + """, + postParse=lambda _, result: result[0].evaluate(), + ) diff --git a/plusminus/examples/dice_roll_parser.py b/plusminus/examples/dice_roll_parser.py new file mode 100644 index 0000000..540f1c8 --- /dev/null +++ b/plusminus/examples/dice_roll_parser.py @@ -0,0 +1,40 @@ +# +# dice_roll_parser.py +# +# Copyright 2021, Paul McGuire +# +from plusminus import BaseArithmeticParser + + +class DiceRollParser(BaseArithmeticParser): + """ + Parser for evaluating expressions representing rolls of dice, as used in many board and + role-playing games, such as: + + d20 + 3d20 + 5d6 + d20 + """ + + def customize(self): + import random + + # fmt: off + self.add_operator("d", 1, BaseArithmeticParser.RIGHT, lambda a: random.randint(1, a)) + self.add_operator("d", 2, BaseArithmeticParser.LEFT, + lambda a, b: sum(random.randint(1, b) for _ in range(a))) + # fmt: on + + +if __name__ == '__main__': + + parser = DiceRollParser() + parser.runTests( + """\ + d20 + 3d6 + d20+3d4 + 2d100 + """, + postParse=lambda _, result: result[0].evaluate(), + ) diff --git a/plusminus/examples/example_parsers.py b/plusminus/examples/example_parsers.py deleted file mode 100644 index de68402..0000000 --- a/plusminus/examples/example_parsers.py +++ /dev/null @@ -1,189 +0,0 @@ -# -# example_parsers.py -# -# Example domain-specific parsers extending the plusminus base classes -# -# Copyright 2020, Paul McGuire -# -from plusminus import ( - BaseArithmeticParser, - ArithmeticParser, - safe_pow, - constrained_factorial, -) - -__all__ = "DiceRollParser DateTimeArithmeticParser CombinatoricsArithmeticParser BusinessArithmeticParser".split() - - -class DiceRollParser(BaseArithmeticParser): - """ - Parser for evaluating expressions representing rolls of dice, as used in many board and - role-playing games, such as: - - d20 - 3d20 - 5d6 + d20 - """ - - def customize(self): - import random - - # fmt: off - self.add_operator("d", 1, BaseArithmeticParser.RIGHT, lambda a: random.randint(1, a)) - self.add_operator("d", 2, BaseArithmeticParser.LEFT, - lambda a, b: sum(random.randint(1, b) for _ in range(a))) - # fmt: on - - -class DateTimeArithmeticParser(BaseArithmeticParser): - """ - Parser for evaluating expressions in dates and times, using operators d, h, m, and s - to define terms for amounts of days, hours, minutes, and seconds: - - now() - today() - now() + 10s - now() + 24h - - All numeric expressions will be treated as UTC integer timestamps. To display - timestamps as ISO strings, use str(): - - str(now()) - str(today() + 3d) - """ - - SECONDS_PER_MINUTE = 60 - SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60 - SECONDS_PER_DAY = SECONDS_PER_HOUR * 24 - - def customize(self): - from datetime import datetime - - # fmt: off - self.add_operator("d", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_DAY) - self.add_operator("h", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_HOUR) - self.add_operator("m", 1, BaseArithmeticParser.LEFT, lambda t: t * DateTimeArithmeticParser.SECONDS_PER_MINUTE) - self.add_operator("s", 1, BaseArithmeticParser.LEFT, lambda t: t) - - self.add_function("now", 0, lambda: datetime.utcnow().timestamp()) - self.add_function("today", 0, - lambda: datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) - self.add_function("str", 1, lambda dt: str(datetime.fromtimestamp(dt))) - # fmt: on - - -class CombinatoricsArithmeticParser(BaseArithmeticParser): - """ - Parser for evaluating expressions of combinatorics problems, for numbers of - permutations (nPm) and combinations (nCm): - - nPm = n! / (n-m)! - 8P4 = number of (ordered) permutations of selecting 4 items from a collection of 8 - - nCm = n! / m!(n-m)! - 8C4 = number of (unordered) combinations of selecting 4 items from a collection of 8 - """ - - def customize(self): - super().customize() - # fmt: off - self.add_operator("P", 2, BaseArithmeticParser.LEFT, - lambda a, b: int(constrained_factorial(a) / constrained_factorial(a - b))) - self.add_operator("C", 2, BaseArithmeticParser.LEFT, - lambda a, b: int(constrained_factorial(a) - / constrained_factorial(b) - / constrained_factorial(a - b))) - self.add_operator(*ArithmeticParser.Operators.FACTORIAL) - # fmt: on - - -class BusinessArithmeticParser(BaseArithmeticParser): - """ - A parser for evaluating common financial and retail calculations: - - 50% of 20 - 20 * (1-20%) - (100-20)% of 20 - 5 / 20% - FV(20000, 3%, 30) - FV(20000, 3%/12, 30*12) - - Functions: - FV(present_value, rate_per_period, number_of_periods) - future value of an amount, n periods in the future, at an interest rate per period - PV(future_value, rate_per_period, number_of_periods) - present value of a future amount, n periods in the future, at an interest rate per period - PP(present_value, rate_per_period, number_of_periods) - periodic value of n amounts, one per period, for n periods, at an interest rate per period - """ - - def customize(self): - def pv(fv, rate, n_periods): - return fv / safe_pow(1 + rate, n_periods) - - def fv(pv, rate, n_periods): - return pv * safe_pow(1 + rate, n_periods) - - def pp(pv, rate, n_periods): - return rate * pv / (1 - safe_pow(1 + rate, -n_periods)) - - super().customize() - self.add_operator("of", 2, BaseArithmeticParser.LEFT, lambda a, b: a * b) - self.add_operator("%", 1, BaseArithmeticParser.LEFT, lambda a: a / 100) - - self.add_function("PV", 3, pv) - self.add_function("FV", 3, fv) - self.add_function("PP", 3, pp) - - -if __name__ == "__main__": - - parser = DiceRollParser() - parser.runTests( - ["d20", "3d6", "d20+3d4", "2d100"], - postParse=lambda _, result: result[0].evaluate(), - ) - - parser = DateTimeArithmeticParser() - parser.runTests( - """\ - now() - str(now()) - str(today()) - "A day from now: " + str(now() + 1d) - "A day and an hour from now: " + str(now() + 1d + 1h) - str(now() + 3*(1d + 1h)) - """, - postParse=lambda _, result: result[0].evaluate(), - ) - - parser = CombinatoricsArithmeticParser() - parser.runTests( - """\ - 3! - -3! - 3!! - 6! / (6-2)! - 6 P 2 - 6! / (2!*(6-2)!) - 6 C 2 - 6P6 - 6C6 - """, - postParse=lambda _, result: result[0].evaluate(), - ) - - parser = BusinessArithmeticParser() - parser.runTests( - """\ - 25% - 20 * 50% - 50% of 20 - 20 * (1-20%) - (100-20)% of 20 - 5 / 20% - FV(20000, 3%, 30) - FV(20000, 3%/12, 30*12) - """, - postParse=lambda _, result: result[0].evaluate(), - ) diff --git a/test/arith_tests.py b/test/arith_tests.py index 17dc460..2f7fcda 100644 --- a/test/arith_tests.py +++ b/test/arith_tests.py @@ -5,18 +5,13 @@ # # Copyright 2020, Paul McGuire # -from plusminus import * -from plusminus.examples.example_parsers import ( - DiceRollParser, - CombinatoricsArithmeticParser, - BusinessArithmeticParser, - DateTimeArithmeticParser, -) from pprint import pprint -import sys - -sys.setrecursionlimit(3000) +from plusminus import BasicArithmeticParser +from plusminus.examples.dice_roll_parser import DiceRollParser +from plusminus.examples.combinatorics_arithmetic_parser import CombinatoricsArithmeticParser +from plusminus.examples.business_arithmetic_parser import BusinessArithmeticParser +from plusminus.examples.date_time_arithmetic_parser import DateTimeArithmeticParser def post_parse_evaluate(teststr, result): diff --git a/test/test_example_parsers/test_business_arithmetic_parser.py b/test/test_example_parsers/test_business_arithmetic_parser.py index 9221845..e3bf439 100644 --- a/test/test_example_parsers/test_business_arithmetic_parser.py +++ b/test/test_example_parsers/test_business_arithmetic_parser.py @@ -1,7 +1,7 @@ # test_business_arithmetic_parser.py import pytest -from plusminus.examples.example_parsers import BusinessArithmeticParser +from plusminus.examples.business_arithmetic_parser import BusinessArithmeticParser @pytest.fixture diff --git a/test/test_example_parsers/test_combinatorics_parser.py b/test/test_example_parsers/test_combinatorics_parser.py index d9b2388..f366aa3 100644 --- a/test/test_example_parsers/test_combinatorics_parser.py +++ b/test/test_example_parsers/test_combinatorics_parser.py @@ -1,7 +1,7 @@ # test_combinatorics_parser.py import pytest -from plusminus.examples.example_parsers import CombinatoricsArithmeticParser +from plusminus.examples.combinatorics_arithmetic_parser import CombinatoricsArithmeticParser @pytest.fixture diff --git a/test/test_example_parsers/test_datetime_parser.py b/test/test_example_parsers/test_datetime_parser.py index 9e30fc8..caa6bf9 100644 --- a/test/test_example_parsers/test_datetime_parser.py +++ b/test/test_example_parsers/test_datetime_parser.py @@ -2,7 +2,7 @@ import datetime import pytest -from plusminus.examples.example_parsers import DateTimeArithmeticParser +from plusminus.examples.date_time_arithmetic_parser import DateTimeArithmeticParser @pytest.fixture diff --git a/test/test_example_parsers/test_dice_roll_parser.py b/test/test_example_parsers/test_dice_roll_parser.py index 8dd8140..ff8d52e 100644 --- a/test/test_example_parsers/test_dice_roll_parser.py +++ b/test/test_example_parsers/test_dice_roll_parser.py @@ -1,6 +1,6 @@ import pytest -from plusminus.examples.example_parsers import DiceRollParser +from plusminus.examples.dice_roll_parser import DiceRollParser @pytest.fixture