Commit 2bc65876 authored by Sander Mathijs van Veen's avatar Sander Mathijs van Veen

Merge branch 'master' of kompiler.org:trs

parents 073810b9 2304214e
# vim: set fileencoding=utf-8 :
- Fix BisonSyntaxError location tracking. - Fix BisonSyntaxError location tracking.
- Sort polynom by its exponents? - Sort polynom by its exponents?
- No possibilities found for:
>>> a2b3 + a2b3
a ^ 2 * b ^ 3 + a ^ 2 * b ^ 3
- 2 + 3 + 4 rewrites to 5 instead of 5 + 4
-> the problem is that the 'root' of the application is actually a subtree
of the entire expression. This means that the parent of each possibility
root (or 'subtree') must me stored to be able to replace the subtree.
- MESSAGES needs to be expanded. - MESSAGES needs to be expanded.
- rewrite match_combine_polynomes to an even more generic form: - Fix division by zero caused by "0/0": Catch exception in front-end
match_combine_factors.
- "--ab + c" has no rewrite possibility. The graph of "--ab + c" is also
not valid:
-
+
╭─┴╮
* c
╭┴╮
- b
a
- The following expression gives a cycle in the possibilities:
>>> ab + ba
possibilities:
Group "ab" is multiplied by 1 and 1, combine them.
>>> (1 + 1) * ab
(1 + 1)ab
possibilities:
Combine the constants 1 and 1.
Group "1" is multiplied by 1 and 1, combine them.
Expand a(1 + 1).
Expand b(1 + 1).
- Fix division by zero caused by "0/0".
smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py smvv@multivac ~/work/trs $ printf "a/0\n??" | ./main.py
Traceback (most recent call last): Traceback (most recent call last):
...@@ -84,3 +48,56 @@ smvv@multivac ~/work/trs $ printf "0/1\n??" | ./main.py ...@@ -84,3 +48,56 @@ smvv@multivac ~/work/trs $ printf "0/1\n??" | ./main.py
<Possibility root="0 / 1" handler=divide_numerics args=(0, 1)> <Possibility root="0 / 1" handler=divide_numerics args=(0, 1)>
Division of 0 by 1 reduces to 0. Division of 0 by 1 reduces to 0.
Division of 0 by 1 reduces to 0. Division of 0 by 1 reduces to 0.
- Fractions constant rewrite rules.
- >>> (sin x) ^ 2 + (cos x) ^ 2
sin(x) ^ 2 + cos(x) ^ 2
>>> sin(x) ^ 2 + cos(x) ^ 2
sin(x ^ 2) + cos(x ^ 2)
- ExpressionNode.equals() werkend maken voor alle cases (negatie).
- validation: preorder traversal implementatie vergelijken met andere
implementaties.
- Fix the following loop using strategy (reduce_fraction_constants):
>>> 2 / 7 - 4 / 11
2 / 7 - 4 / 11
>>> @
22 / 77 - 28 / 77
>>> @
2 / 7 - 28 / 77
>>> @
2 / 7 + 4 / 11
- Cancel terms before multiplying constants: (3 * ...) / (3 * ...) -> ... / ...
>>> (7/3)*(3/5)
7 / 3 * (3 / 5)
>>> ??
Expand fraction with nominator greater than denominator 7 / 3 to an integer
plus a fraction.
Multiply fractions 7 / 3 and 3 / 5.
>>> @
7 * 3 / (3 * 5)
>>> ?
Multiply constant 7 with 3.
>>> @
21 / (3 * 5)
>>> @
21 / 15
>>> @
7 / 5
- filter_duplicates does not seem to work anymore...
- Fix error while parsing unicode PI:
>>> sin(1/2 * pi)
sin(1 / 2 * π)
>>> @
unknown char � ignored.
unknown char � ignored.
ERROR: 41.7-41.8: "syntax error, unexpected TIMES" near "*".
ERROR: 41.14-41.15: "syntax error, unexpected RPAREN" near ")".
- No matches for sin(pi), sin(2pi), sin(4pi), etc...
graph_drawing @ bb3c5d23
Subproject commit 11940973bdfef9432438b054c65b28af2eb97d0c Subproject commit bb3c5d23dc2a15e0e634f72b1ca48e5b22817642
This diff is collapsed.
This diff is collapsed.
from node import TYPE_OPERATOR
# Each rule will append its hint message to the following dictionary. The # Each rule will append its hint message to the following dictionary. The
# function pointer to the apply function of the rule is used as key. The # function pointer to the apply function of the rule is used as key. The
# corresponding value is a string, which will be used to produce the hint # corresponding value is a string, which will be used to produce the hint
...@@ -51,16 +54,27 @@ def filter_duplicates(possibilities): ...@@ -51,16 +54,27 @@ def filter_duplicates(possibilities):
return unique return unique
def pick_suggestion(possibilities): def find_parent_node(root, child):
if not possibilities: nodes = [root]
return
while nodes:
node = nodes.pop()
while node:
# TODO: pick the best suggestion. if node.type != TYPE_OPERATOR:
suggestion = 0 break
return possibilities[suggestion]
if child in node:
return node
def apply_suggestion(root, subtree_map, suggestion): if len(node) > 1:
nodes.append(node[1])
node = node[0]
def apply_suggestion(root, suggestion):
# TODO: clone the root node before modifying. After deep copying the root # TODO: clone the root node before modifying. After deep copying the root
# node, the subtree_map cannot be used since the hash() of each node in the # node, the subtree_map cannot be used since the hash() of each node in the
# deep copied root node has changed. # deep copied root node has changed.
...@@ -68,10 +82,7 @@ def apply_suggestion(root, subtree_map, suggestion): ...@@ -68,10 +82,7 @@ def apply_suggestion(root, subtree_map, suggestion):
subtree = suggestion.handler(suggestion.root, suggestion.args) subtree = suggestion.handler(suggestion.root, suggestion.args)
if suggestion.root in subtree_map: parent_node = find_parent_node(root, suggestion.root)
parent_node = subtree_map[suggestion.root]
else:
parent_node = None
# There is either a parent node or the subtree is the root node. # There is either a parent node or the subtree is the root node.
# FIXME: FAIL: test_diagnostic_test_application in tests/test_b1_ch08.py # FIXME: FAIL: test_diagnostic_test_application in tests/test_b1_ch08.py
...@@ -85,4 +96,5 @@ def apply_suggestion(root, subtree_map, suggestion): ...@@ -85,4 +96,5 @@ def apply_suggestion(root, subtree_map, suggestion):
if parent_node: if parent_node:
parent_node.substitute(suggestion.root, subtree) parent_node.substitute(suggestion.root, subtree)
return root return root
return subtree return subtree
from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG, OP_SIN, OP_COS, \
from .poly import match_combine_polynomes OP_TAN
from .groups import match_combine_groups from .groups import match_combine_groups
from .factors import match_expand from .factors import match_expand
from .powers import match_add_exponents, match_subtract_exponents, \ from .powers import match_add_exponents, match_subtract_exponents, \
match_multiply_exponents, match_duplicate_exponent, \ match_multiply_exponents, match_duplicate_exponent, \
match_remove_negative_exponent, match_exponent_to_root, \ match_raised_fraction, match_remove_negative_exponent, \
match_extend_exponent match_exponent_to_root, match_extend_exponent, match_constant_exponent
from .numerics import match_divide_numerics, match_multiply_numerics, \ from .numerics import match_add_numerics, match_divide_numerics, \
match_multiply_zero match_multiply_numerics, match_multiply_zero, match_multiply_one, \
match_raise_numerics
from .fractions import match_constant_division, match_add_constant_fractions, \ from .fractions import match_constant_division, match_add_constant_fractions, \
match_expand_and_add_fractions match_expand_and_add_fractions, match_multiply_fractions, \
from .negation import match_negate_group, match_negated_division match_divide_fractions, match_equal_fraction_parts
from .negation import match_negated_factor, match_negate_polynome, \
match_negated_division
from .sort import match_sort_multiplicants
from .goniometry import match_add_quadrants, match_negated_parameter, \
match_half_pi_subtraction, match_standard_radian
RULES = { RULES = {
OP_ADD: [match_add_constant_fractions, match_combine_polynomes, \ OP_ADD: [match_add_numerics, match_add_constant_fractions,
match_combine_groups], match_combine_groups, match_add_quadrants],
OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents, \ OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
match_expand_and_add_fractions, match_multiply_zero], match_expand_and_add_fractions, match_multiply_zero,
OP_DIV: [match_subtract_exponents, match_divide_numerics, \ match_negated_factor, match_multiply_one,
match_constant_division, match_negated_division], match_sort_multiplicants, match_multiply_fractions],
OP_POW: [match_multiply_exponents, match_duplicate_exponent, \ OP_DIV: [match_subtract_exponents, match_divide_numerics,
match_remove_negative_exponent, match_exponent_to_root, \ match_constant_division, match_divide_fractions, \
match_extend_exponent], match_negated_division, match_equal_fraction_parts],
OP_NEG: [match_negate_group], OP_POW: [match_multiply_exponents, match_duplicate_exponent,
match_raised_fraction, match_remove_negative_exponent,
match_exponent_to_root, match_extend_exponent,
match_constant_exponent, match_raise_numerics],
OP_NEG: [match_negate_polynome],
OP_SIN: [match_negated_parameter, match_half_pi_subtraction,
match_standard_radian],
OP_COS: [match_negated_parameter, match_half_pi_subtraction,
match_standard_radian],
OP_TAN: [match_standard_radian],
} }
from itertools import product, combinations from itertools import product, combinations
from ..node import Scope, OP_ADD, OP_MUL, OP_NEG from ..node import Scope, OP_ADD, OP_MUL
from ..possibilities import Possibility as P, MESSAGES from ..possibilities import Possibility as P, MESSAGES
from ..translate import _ from ..translate import _
...@@ -18,9 +18,13 @@ def match_expand(node): ...@@ -18,9 +18,13 @@ def match_expand(node):
additions = [] additions = []
for n in Scope(node): for n in Scope(node):
if n.is_leaf or n.is_op(OP_NEG) and n[0].is_leaf: if n.is_leaf:
leaves.append(n) leaves.append(n)
elif n.op == OP_ADD: elif n.op == OP_ADD:
# If the addition only contains numerics, do not expand
if not filter(lambda n: not n.is_numeric(), Scope(n)):
continue
additions.append(n) additions.append(n)
for args in product(leaves, additions): for args in product(leaves, additions):
...@@ -45,7 +49,7 @@ def expand_single(root, args): ...@@ -45,7 +49,7 @@ def expand_single(root, args):
scope = Scope(root) scope = Scope(root)
# Replace 'a' with the new expression # Replace 'a' with the new expression
scope.remove(a, a * b + a * c) scope.replace(a, a * b + a * c)
# Remove the addition # Remove the addition
scope.remove(bc) scope.remove(bc)
...@@ -66,7 +70,7 @@ def expand_double(root, args): ...@@ -66,7 +70,7 @@ def expand_double(root, args):
scope = Scope(root) scope = Scope(root)
# Replace 'a + b' with the new expression # Replace 'a + b' with the new expression
scope.remove(ab, a * c + a * d + b * c + b * d) scope.replace(ab, a * c + a * d + b * c + b * d)
# Remove the right addition # Remove the right addition
scope.remove(cd) scope.remove(cd)
......
This diff is collapsed.
from .utils import is_fraction
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, OP_ADD, \
OP_POW, OP_MUL, OP_DIV, OP_SIN, OP_COS, OP_TAN, PI, TYPE_OPERATOR
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def sin(*args):
return N('sin', *args)
def cos(*args):
return N('cos', *args)
def tan(*args):
return N('tan', *args)
def match_add_quadrants(node):
"""
sin(t) ^ 2 + cos(t) ^ 2 -> 1
"""
assert node.is_op(OP_ADD)
p = []
sin_q, cos_q = node
if sin_q.is_power(2) and cos_q.is_power(2):
sin, cos = sin_q[0], cos_q[0]
if sin.is_op(OP_SIN) and cos.is_op(OP_COS):
p.append(P(node, add_quadrants, ()))
return p
def add_quadrants(root, args):
"""
sin(t) ^ 2 + cos(t) ^ 2 -> 1
"""
return L(1)
MESSAGES[add_quadrants] = _('Add the sinus and cosinus quadrants to 1.')
def match_negated_parameter(node):
"""
sin(-t) -> -sin(t)
cos(-t) -> cos(t)
"""
assert node.is_op(OP_SIN) or node.is_op(OP_COS)
t = node[0]
if t.negated:
if node.op == OP_SIN:
return [P(node, negated_sinus_parameter, (t,))]
return [P(node, negated_cosinus_parameter, (t,))]
return []
def negated_sinus_parameter(root, args):
"""
sin(-t) -> -sin(t)
"""
return -sin(+args[0])
MESSAGES[negated_sinus_parameter] = \
_('Bring the negation from the sinus parameter {1} to the outside.')
def negated_cosinus_parameter(root, args):
"""
cos(-t) -> cos(t)
"""
return cos(+args[0])
MESSAGES[negated_cosinus_parameter] = \
_('Remove the negation from the cosinus parameter {1}.')
def match_half_pi_subtraction(node):
"""
sin(pi / 2 - t) -> cos(t)
cos(pi / 2 - t) -> sin(t)
"""
assert node.is_op(OP_SIN) or node.is_op(OP_COS)
if node[0].is_op(OP_ADD):
half_pi, t = node[0]
if half_pi == L(PI) / 2:
if node.op == OP_SIN:
return [P(node, half_pi_subtraction_sinus, (t,))]
return []
def is_pi_frac(node, denominator):
"""
Check if a node is a fraction of 1 multiplied with PI.
Example:
>>> print is_pi_frac(L(1) / 2 * L(PI), 2)
True
"""
if not node.is_op(OP_MUL):
return False
frac, pi = node
if not frac.is_op(OP_DIV) or not pi.is_leaf or pi.value != PI:
return False
n, d = frac
return n == 1 and d == denominator
def sqrt(value):
return N('sqrt', L(value))
l0, l1, sq2, sq3 = L(0), L(1), sqrt(2), sqrt(3)
half = l1 / 2
CONSTANTS = {
OP_SIN: [l0, half, half * sq2, half * sq3, l1],
OP_COS: [l1, half * sq3, half * sq2, half, l0],
OP_TAN: [l0, l1 / 3 * sq3, l1, sq3]
}
def match_standard_radian(node):
"""
Apply a direct constant calculation from the constants table.
| 0 | pi / 6 | pi / 4 | pi / 3 | pi / 2
----+---+-----------+-----------+-----------+-------
sin | 0 | 1/2 | sqrt(2)/2 | sqrt(3)/2 | 1
cos | 1 | sqrt(3)/2 | sqrt(2)/2 | 1/2 | 0
tan | 0 | sqrt(3)/3 | 1 | sqrt(3) | -
"""
assert node.type == TYPE_OPERATOR and node.op in (OP_SIN, OP_COS, OP_TAN)
t = node[0]
if t == 0:
return [P(node, standard_radian, (node.op, 0))]
denoms = [6, 4, 3]
if node.op != OP_TAN:
denoms.append(2)
for i, denominator in enumerate(denoms):
if is_pi_frac(t, denominator):
return [P(node, standard_radian, (node.op, i + 1))]
return []
def standard_radian(root, args):
op, column = args
return CONSTANTS[op][column].clone()
MESSAGES[standard_radian] = _('Replace standard radian {0}.')
from itertools import combinations from itertools import combinations
from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, Scope, \ from ..node import ExpressionLeaf as Leaf, Scope, OP_ADD, OP_MUL, nary_node, \
OP_ADD, OP_MUL, OP_NEG negate
from ..possibilities import Possibility as P, MESSAGES from ..possibilities import Possibility as P, MESSAGES
from ..translate import _ from ..translate import _
...@@ -18,50 +18,53 @@ def match_combine_groups(node): ...@@ -18,50 +18,53 @@ def match_combine_groups(node):
ab + 2ab -> 3ab ab + 2ab -> 3ab
ab + ba -> 2ab ab + ba -> 2ab
""" """
# TODO: handle OP_NEG nodes
assert node.is_op(OP_ADD) assert node.is_op(OP_ADD)
p = [] p = []
groups = [] groups = []
scope = Scope(node)
for n in Scope(node): for n in scope:
groups.append((1, n, n)) if not n.is_numeric():
groups.append((Leaf(1), n, n))
# Each number multiplication yields a group, multiple occurences of # Each number multiplication yields a group, multiple occurences of
# the same group can be replaced by a single one # the same group can be replaced by a single one
if n.is_op(OP_MUL): if n.is_op(OP_MUL):
scope = Scope(n) n_scope = Scope(n)
l = len(scope) l = len(n_scope)
for i, sub_node in enumerate(scope): for i, sub_node in enumerate(n_scope):
if sub_node.is_numeric() or (sub_node.is_op(OP_NEG) if sub_node.is_numeric():
and sub_node[0].is_numeric()): others = [n_scope[j] for j in range(i) + range(i + 1, l)]
others = [scope[j] for j in range(i) + range(i + 1, l)]
if len(others) == 1: if len(others) == 1:
g = others[0] g = others[0]
else: else:
g = Node('*', *others) g = nary_node('*', others)
groups.append((sub_node, g, n)) groups.append((sub_node, g, n))
for g0, g1 in combinations(groups, 2): for (c0, g0, n0), (c1, g1, n1) in combinations(groups, 2):
if g0[1].equals(g1[1]): if g0.equals(g1):
p.append(P(node, combine_groups, g0 + g1)) p.append(P(node, combine_groups, (scope, c0, g0, n0, c1, g1, n1)))
elif g0.equals(g1, ignore_negation=True):
# Move negations to constants
c0 = c0.negate(g0.negated)
c1 = c1.negate(g1.negated)
g0 = negate(g0, 0)
g1 = negate(g1, 0)
p.append(P(node, combine_groups, (scope, c0, g0, n0, c1, g1, n1)))
return p return p
def combine_groups(root, args): def combine_groups(root, args):
c0, g0, n0, c1, g1, n1 = args scope, c0, g0, n0, c1, g1, n1 = args
scope = Scope(root)
if not isinstance(c0, Leaf) and not isinstance(c0, Node):
c0 = Leaf(c0)
# Replace the left node with the new expression # Replace the left node with the new expression
scope.remove(n0, (c0 + c1) * g0) scope.replace(n0, (c0 + c1) * g0)
# Remove the right node # Remove the right node
scope.remove(n1) scope.remove(n1)
...@@ -70,4 +73,4 @@ def combine_groups(root, args): ...@@ -70,4 +73,4 @@ def combine_groups(root, args):
MESSAGES[combine_groups] = \ MESSAGES[combine_groups] = \
_('Group "{2}" is multiplied by {1} and {4}, combine them.') _('Group "{3}" is multiplied by {2} and {5}, combine them.')
from ..node import get_scope, nary_node, OP_NEG, OP_ADD, OP_MUL, OP_DIV from ..node import Scope, OP_ADD, OP_MUL, OP_DIV
from ..possibilities import Possibility as P, MESSAGES from ..possibilities import Possibility as P, MESSAGES
from ..translate import _ from ..translate import _
def match_negate_group(node): def match_negated_factor(node):
""" """
--a -> a This rule assures that negations in the scope of a multiplication are
--ab -> ab brought to the most left node in the multiplication's scope.
-(-ab + c) -> --ab - c
-(a + b + ... + z) -> -a + -b + ... + -z Example:
a * -b -> -ab
""" """
assert node.is_op(OP_NEG) assert node.is_op(OP_MUL)
val = node[0] p = []
scope = Scope(node)
if val.is_op(OP_NEG): # FIXME: The negation that is brought outside is assigned to the first
# --a # element in the scope during the next parsing step:
return [P(node, double_negation, (node,))] # -ab -> -(ab), but -(ab) is printed as -ab
for factor in scope[1:]:
if factor.negated:
p.append(P(node, negated_factor, (scope, factor)))
if not val.is_leaf: return p
scope = get_scope(val)
if not any(map(lambda n: n.is_op(OP_NEG), scope)):
return []
if val.is_op(OP_MUL): def negated_factor(root, args):
# --ab """
return [P(node, negate_polynome, (node, scope))] a * -b -> -ab
"""
scope, factor = args
scope[0] = -scope[0]
scope.replace(factor, +factor)
elif val.is_op(OP_ADD): return scope.as_nary_node()
# -(ab + c) -> -ab - c
# -(-ab + c) -> ab - c
return [P(node, negate_group, (node, scope))]
return []
MESSAGES[negated_factor] = \
_('Bring negation of {2} to the outside of the multiplication.')
def negate_polynome(root, args):
def match_negate_polynome(node):
""" """
# -a * -3c -> a * 3c --a -> a
--a * 3c -> a * 3c -(a + b) -> -a - b
--ab -> ab
--abc -> abc
""" """
node, scope = args #print 'match_negate_polynome:', node, node.negated
assert node.negated, str(node.negated) + '; ' + str(node)
for i, n in enumerate(scope): p = []
# XXX: validate this property!
if n.is_op(OP_NEG):
scope[i] = n[0]
return nary_node('*', scope)
raise RuntimeError('No negation node found in scope.') if node.negated == 2:
# --a
p.append(P(node, double_negation, ()))
if node.is_op(OP_ADD):
# -(a + b) -> -a - b
p.append(P(node, negate_polynome, ()))
MESSAGES[negate_polynome] = _('Apply negation to the polynome {1[0]}.') return p
def negate_group(root, args): def double_negation(root, args):
""" """
-(-ab + ... + c) -> --ab + ... + -c --a -> a
""" """
node, scope = args return root.reduce_negation(2)
# Negate each group
for i, n in enumerate(scope):
scope[i] = -n
return nary_node('+', scope) MESSAGES[double_negation] = _('Remove double negation in {0}.')
MESSAGES[negate_group] = _('Apply negation to the subexpression {1[0]}.') def negate_polynome(root, args):
def double_negation(root, args):
""" """
--a -> a -(a + b) -> -a - b
""" """
node = args[0] scope = Scope(root)
# Negate each group
for i, n in enumerate(scope):
scope[i] = -n
return +scope.as_nary_node()
return node[0][0]
MESSAGES[negate_polynome] = _('Apply negation to the polynome {0}.')
MESSAGES[double_negation] = _('Remove double negation in {1}.')
#def negate_group(root, args):
# """
# -(a * -3c) -> a * 3c
# -(a * ... * -b) -> ab
# """
# node, scope = args
#
# for i, n in enumerate(scope):
# if n.negated:
# scope[i] = n.reduce_negation()
#
# return nary_node('*', scope).reduce_negation()
#
#
#MESSAGES[negate_polynome] = _('Apply negation to the subexpression {1[0]}.')
def match_negated_division(node): def match_negated_division(node):
...@@ -92,33 +112,28 @@ def match_negated_division(node): ...@@ -92,33 +112,28 @@ def match_negated_division(node):
assert node.is_op(OP_DIV) assert node.is_op(OP_DIV)
a, b = node a, b = node
a_neg = a.is_op(OP_NEG)
b_neg = b.is_op(OP_NEG)
if a_neg and b_neg: if a.negated and b.negated:
return [P(node, double_negated_division, (node,))] return [P(node, double_negated_division, ())]
elif a_neg: elif b.negated:
return [P(node, single_negated_division, (a[0], b))] return [P(node, single_negated_division, (a, +b))]
elif b_neg:
return [P(node, single_negated_division, (a, b[0]))]
return [] return []
def single_negated_division(root, args): def single_negated_division(root, args):
""" """
-a / b -> -(a / b) a / -b -> -a / b
a / -b -> -(a / b)
""" """
a, b = args a, b = args
# FIXME: "-a/b" results in "-(a/b)", which will cause a loop. # FIXME: "-a/b" results in "-(a/b)", which will cause a loop.
return -(a / b) return -a / b
MESSAGES[single_negated_division] = \ MESSAGES[single_negated_division] = \
_('Bring negation outside of the division: -({1} / {2}).') _('Bring negation outside of the division: -{1} / {2}.')
def double_negated_division(root, args): def double_negated_division(root, args):
...@@ -127,8 +142,11 @@ def double_negated_division(root, args): ...@@ -127,8 +142,11 @@ def double_negated_division(root, args):
""" """
a, b = root a, b = root
return a[0] / b[0] return +a / +b
MESSAGES[double_negated_division] = \ MESSAGES[double_negated_division] = \
_('Eliminate top and bottom negation in {1}.') _('Eliminate top and bottom negation in {0}.')
# TODO: negated multiplication: -a * -b = ab
from itertools import combinations from itertools import combinations
from ..node import ExpressionLeaf as Leaf, Scope, OP_DIV, OP_MUL, OP_NEG from .utils import greatest_common_divisor
from ..node import ExpressionLeaf as Leaf, Scope, negate, OP_ADD, OP_DIV, \
OP_MUL, OP_POW
from ..possibilities import Possibility as P, MESSAGES from ..possibilities import Possibility as P, MESSAGES
from ..translate import _ from ..translate import _
def add_numerics(root, args): def match_add_numerics(node):
""" """
Combine two constants to a single constant in an n-ary addition. Combine two constants to a single constant in an n-ary addition.
...@@ -14,31 +16,57 @@ def add_numerics(root, args): ...@@ -14,31 +16,57 @@ def add_numerics(root, args):
2 + -3 -> -1 2 + -3 -> -1
-2 + 3 -> 1 -2 + 3 -> 1
-2 + -3 -> -5 -2 + -3 -> -5
0 + 3 -> 3
0 + -3 -> -3
"""
assert node.is_op(OP_ADD)
p = []
scope = Scope(node)
numerics = []
for n in scope:
if n == 0:
p.append(P(node, remove_zero, (scope, n)))
elif n.is_numeric():
numerics.append(n)
for c0, c1 in combinations(numerics, 2):
p.append(P(node, add_numerics, (scope, c0, c1)))
return p
def remove_zero(root, args):
"""
0 + a -> a
""" """
n0, n1, c0, c1 = args scope, n = args
scope.remove(n)
if c0.is_op(OP_NEG): return scope.as_nary_node()
c0 = -c0[0].value
else:
c0 = c0.value
if c1.is_op(OP_NEG):
c1 = (-c1[0].value)
else:
c1 = c1.value
scope = Scope(root) def add_numerics(root, args):
"""
2 + 3 -> 5
2 + -3 -> -1
-2 + 3 -> 1
-2 + -3 -> -5
"""
scope, c0, c1 = args
value = c0.actual_value() + c1.actual_value()
# Replace the left node with the new expression # Replace the left node with the new expression
scope.remove(n0, Leaf(c0 + c1)) scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
# Remove the right node # Remove the right node
scope.remove(n1) scope.remove(c1)
return scope.as_nary_node() return scope.as_nary_node()
MESSAGES[add_numerics] = _('Combine the constants {1} and {2}.') MESSAGES[add_numerics] = _('Add the constants {2} and {3}.')
#def match_subtract_numerics(node): #def match_subtract_numerics(node):
...@@ -59,23 +87,41 @@ def match_divide_numerics(node): ...@@ -59,23 +87,41 @@ def match_divide_numerics(node):
Example: Example:
6 / 2 -> 3 6 / 2 -> 3
3 / 2 -> 3 / 2 # 1.5 would mean a decrease in precision 3 / 2 -> 3 / 2 # 1.5 would mean a decrease in precision
3.0 / 2 -> 1.5 3.0 / 2 -> 1.5
3 / 2.0 -> 1.5 3 / 2.0 -> 1.5
3.0 / 2.0 -> 1.5 3.0 / 2.0 -> 1.5
3 / 1.0 -> 3 # Exceptional case: division of integer by 1.0 keeps 3 / 1.0 -> 3 # Exceptional case: division of integer by 1.0
# integer precision # keeps integer precision
2 / 4 -> 1 / 2 # 1 < greatest common divisor <= nominator
4 / 3 -> 1 + 1 / 3 # nominator > denominator
""" """
assert node.is_op(OP_DIV) assert node.is_op(OP_DIV)
n, d = node n, d = node
divide = False divide = False
dv = d.value nv, dv = n.value, d.value
if n.is_int() and d.is_int(): if n.is_int() and d.is_int():
# 6 / 2 -> 3 mod = nv % dv
# 3 / 2 -> 3 / 2
divide = not divmod(n.value, dv)[1] if not mod:
# 6 / 2 -> 3
# 3 / 2 -> 3 / 2
return [P(node, divide_numerics, (nv, dv, n.negated + d.negated))]
gcd = greatest_common_divisor(nv, dv)
if 1 < gcd <= nv:
# 2 / 4 -> 1 / 2
# TODO: Test with negations!
return [P(node, reduce_fraction_constants, (gcd,))]
if nv > dv:
# 4 / 3 -> 1 + 1 / 3
# TODO: Test with negations!
return [P(node, fraction_to_int_fraction,
((nv - mod) / dv, mod, dv))]
elif n.is_numeric() and d.is_numeric(): elif n.is_numeric() and d.is_numeric():
if d == 1.0: if d == 1.0:
# 3 / 1.0 -> 3 # 3 / 1.0 -> 3
...@@ -84,14 +130,14 @@ def match_divide_numerics(node): ...@@ -84,14 +130,14 @@ def match_divide_numerics(node):
# 3.0 / 2 -> 1.5 # 3.0 / 2 -> 1.5
# 3 / 2.0 -> 1.5 # 3 / 2.0 -> 1.5
# 3.0 / 2.0 -> 1.5 # 3.0 / 2.0 -> 1.5
divide = True return [P(node, divide_numerics, (nv, dv, n.negated + d.negated))]
return [P(node, divide_numerics, (n.value, dv))] if divide else [] return []
def divide_numerics(root, args): def divide_numerics(root, args):
""" """
Combine two constants to a single constant in a division. Combine two divided constants into a single constant.
Examples: Examples:
6 / 2 -> 3 6 / 2 -> 3
...@@ -100,14 +146,48 @@ def divide_numerics(root, args): ...@@ -100,14 +146,48 @@ def divide_numerics(root, args):
3.0 / 2.0 -> 1.5 3.0 / 2.0 -> 1.5
3 / 1.0 -> 3 3 / 1.0 -> 3
""" """
n, d = args n, d, negated = args
return Leaf(n / d) return Leaf(n / d).negate(negated)
MESSAGES[divide_numerics] = _('Divide constant {1} by constant {2}.') MESSAGES[divide_numerics] = _('Divide constant {1} by constant {2}.')
def reduce_fraction_constants(root, args):
"""
Reduce the nominator and denominator of a fraction with a given greatest
common divisor.
Example:
2 / 4 -> 1 / 2
"""
gcd = args[0]
a, b = root
return Leaf(a.value / gcd).negate(a.negated) \
/ Leaf(b.value / gcd).negate(b.negated)
MESSAGES[reduce_fraction_constants] = _('Simplify fraction {0}.')
def fraction_to_int_fraction(root, args):
"""
Combine two divided integer into an integer with a fraction.
Examples:
4 / 3 -> 1 + 1 / 3
"""
integer, nominator, denominator = map(Leaf, args)
return integer + nominator / denominator
MESSAGES[fraction_to_int_fraction] = _('Expand fraction with nominator greater'
' than denominator {0} to an integer plus a fraction.')
def match_multiply_zero(node): def match_multiply_zero(node):
""" """
a * 0 -> 0 a * 0 -> 0
...@@ -119,20 +199,12 @@ def match_multiply_zero(node): ...@@ -119,20 +199,12 @@ def match_multiply_zero(node):
assert node.is_op(OP_MUL) assert node.is_op(OP_MUL)
left, right = node left, right = node
is_zero = lambda n: n.is_leaf and n.value == 0
if is_zero(left): if (left.is_leaf and left.value == 0) \
negated = right.is_op(OP_NEG) or (right.is_leaf and right.value == 0):
elif is_zero(right): return [P(node, multiply_zero, (left.negated + right.negated,))]
negated = left.is_op(OP_NEG)
elif left.is_op(OP_NEG) and is_zero(left[0]):
negated = not right.is_op(OP_NEG)
elif right.is_op(OP_NEG) and is_zero(right[0]):
negated = not left.is_op(OP_NEG)
else:
return []
return [P(node, multiply_zero, (negated,))] return []
def multiply_zero(root, args): def multiply_zero(root, args):
...@@ -143,17 +215,48 @@ def multiply_zero(root, args): ...@@ -143,17 +215,48 @@ def multiply_zero(root, args):
0 * -a -> -0 0 * -a -> -0
-0 * -a -> 0 -0 * -a -> 0
""" """
negated = args[0] return negate(Leaf(0), args[0])
if negated:
return -Leaf(0)
else:
return Leaf(0)
MESSAGES[multiply_zero] = _('Multiplication with zero yields zero.') MESSAGES[multiply_zero] = _('Multiplication with zero yields zero.')
def match_multiply_one(node):
"""
a * 1 -> a
1 * a -> a
-1 * a -> -a
1 * -a -> -a
-1 * -a -> a
"""
assert node.is_op(OP_MUL)
left, right = node
if left.value == 1:
return [P(node, multiply_one, (right, left))]
if right.value == 1:
return [P(node, multiply_one, (left, right))]
return []
def multiply_one(root, args):
"""
a * 1 -> a
1 * a -> a
-1 * a -> -a
1 * -a -> -a
-1 * -a -> a
"""
a, one = args
return a.negate(one.negated + root.negated)
MESSAGES[multiply_one] = _('Multiplication with one yields the multiplicant.')
def match_multiply_numerics(node): def match_multiply_numerics(node):
""" """
3 * 2 -> 6 3 * 2 -> 6
...@@ -164,16 +267,11 @@ def match_multiply_numerics(node): ...@@ -164,16 +267,11 @@ def match_multiply_numerics(node):
assert node.is_op(OP_MUL) assert node.is_op(OP_MUL)
p = [] p = []
numerics = [] scope = Scope(node)
numerics = filter(lambda n: n.is_numeric(), scope)
for n in Scope(node):
if n.is_numeric():
numerics.append((n, n.value))
elif n.is_op(OP_NEG) and n[0].is_numeric():
numerics.append((n, -n[0].value))
for (n0, v0), (n1, v1) in combinations(numerics, 2): for c0, c1 in combinations(numerics, 2):
p.append(P(node, multiply_numerics, (n0, n1, v0, v1))) p.append(P(node, multiply_numerics, (scope, c0, c1)))
return p return p
...@@ -185,24 +283,46 @@ def multiply_numerics(root, args): ...@@ -185,24 +283,46 @@ def multiply_numerics(root, args):
Example: Example:
2 * 3 -> 6 2 * 3 -> 6
""" """
n0, n1, v0, v1 = args scope, c0, c1 = args
scope = []
value = v0 * v1
if value > 0:
substitution = Leaf(value)
else:
substitution = -Leaf(-value)
scope = Scope(root)
# Replace the left node with the new expression # Replace the left node with the new expression
scope.remove(n0, substitution) substitution = Leaf(c0.value * c1.value).negate(c0.negated + c1.negated)
scope.replace(c0, substitution)
# Remove the right node # Remove the right node
scope.remove(n1) scope.remove(c1)
return scope.as_nary_node() return scope.as_nary_node()
MESSAGES[multiply_numerics] = _('Multiply constant {1} with {2}.') MESSAGES[multiply_numerics] = _('Multiply constant {2} with {3}.')
def match_raise_numerics(node):
"""
2 ^ 3 -> 8
(-2) ^ 3 -> -8
(-2) ^ 2 -> 4
"""
assert node.is_op(OP_POW)
r, e = node
if r.is_numeric() and e.is_numeric() and not e.negated:
return [P(node, raise_numerics, (r, e))]
return []
def raise_numerics(root, args):
"""
2 ^ 3 -> 8
(-2) ^ 3 -> -8
(-2) ^ 2 -> 4
"""
r, e = args
return Leaf(r.value ** e.value).negate(r.negated * e.value)
MESSAGES[raise_numerics] = _('Raise constant {1} with {2}.')
from itertools import combinations
from ..node import Scope, OP_ADD, OP_NEG
from ..possibilities import Possibility as P, MESSAGES
from .numerics import add_numerics
def is_numeric_or_negated_numeric(n):
return n.is_numeric() or (n.is_op(OP_NEG) and n[0].is_numeric())
def match_combine_polynomes(node, verbose=False):
"""
n + exp + m -> exp + (n + m)
k0 * v ^ n + exp + k1 * v ^ n -> exp + (k0 + k1) * v ^ n
"""
assert node.is_op(OP_ADD)
p = []
# Collect all nodes that can be combined:
# a ^ e = 1 * a ^ e
# c * a = c * a ^ 1
# c * a ^ e
# a = 1 * a ^ 1
#
# Identifier nodes of all polynomes, tuple format is:
# (root, exponent, coefficient, literal_coefficient)
polys = []
if verbose: # pragma: nocover
print 'match combine factors:', node
for n in Scope(node):
polynome = n.extract_polynome_properties()
if verbose: # pragma: nocover
print 'n:', n, 'polynome:', polynome
if polynome:
polys.append((n, polynome))
# Each combination of powers of the same value and polynome can be added
if len(polys) >= 2:
for left, right in combinations(polys, 2):
n0, p0 = left
n1, p1 = right
c0, r0, e0 = p0
c1, r1, e1 = p1
# Both numeric root and same exponent -> combine coefficients and
# roots, or: same root and exponent -> combine coefficients.
# TODO: Addition with zero, e.g. a + 0 -> a
if c0 == 1 and c1 == 1 and e0 == 1 and e1 == 1 \
and all(map(is_numeric_or_negated_numeric, [r0, r1])):
# 2 + 3 -> 5
# 2 + -3 -> -1
# -2 + 3 -> 1
# -2 + -3 -> -5
p.append(P(node, add_numerics, (n0, n1, r0, r1)))
elif c0.is_numeric() and c1.is_numeric() and r0 == r1 and e0 == e1:
# 2a + 2a -> 4a
# a + 2a -> 3a
# 2a + a -> 3a
# a + a -> 2a
p.append(P(node, combine_polynomes, (n0, n1, c0, c1, r0, e0)))
return p
def combine_polynomes(root, args):
"""
Combine two multiplications of any polynome in an n-ary plus.
Synopsis:
c0 * a ^ b + c1 * a ^ b -> (c0 + c1) * a ^ b
"""
n0, n1, c0, c1, r, e = args
# a ^ 1 -> a
if e == 1:
power = r
else:
power = r ** e
scope = Scope(root)
# Replace the left node with the new expression:
# (c0 + c1) * a ^ b
# a, b and c are from 'left', d is from 'right'.
scope.remove(n0, (c0 + c1) * power)
# Remove the right node
scope.remove(n1)
return scope.as_nary_node()
from itertools import combinations from itertools import combinations
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \ from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
OP_NEG, OP_MUL, OP_DIV, OP_POW, OP_ADD OP_MUL, OP_DIV, OP_POW, OP_ADD, negate
from ..possibilities import Possibility as P, MESSAGES from ..possibilities import Possibility as P, MESSAGES
from ..translate import _ from ..translate import _
...@@ -12,21 +12,23 @@ def match_add_exponents(node): ...@@ -12,21 +12,23 @@ def match_add_exponents(node):
a * a^q -> a^(1 + q) a * a^q -> a^(1 + q)
a^p * a -> a^(p + 1) a^p * a -> a^(p + 1)
a * a -> a^(1 + 1) a * a -> a^(1 + 1)
-a * a^q -> -a^(1 + q)
""" """
assert node.is_op(OP_MUL) assert node.is_op(OP_MUL)
p = [] p = []
powers = {} powers = {}
scope = Scope(node)
for n in Scope(node): for n in scope:
# Order powers by their roots, e.g. a^p and a^q are put in the same
# list because of the mutual 'a'
if n.is_identifier(): if n.is_identifier():
s = n s = negate(n, 0)
exponent = L(1) exponent = L(1)
elif n.is_op(OP_POW): elif n.is_op(OP_POW):
# Order powers by their roots, e.g. a^p and a^q are put in the same
# list because of the mutual 'a'
s, exponent = n s, exponent = n
else: else: # pragma: nocover
continue continue
s_str = str(s) s_str = str(s)
...@@ -41,7 +43,7 @@ def match_add_exponents(node): ...@@ -41,7 +43,7 @@ def match_add_exponents(node):
# create a single power with that root # create a single power with that root
if len(occurrences) > 1: if len(occurrences) > 1:
for (n0, e1, a0), (n1, e2, a1) in combinations(occurrences, 2): for (n0, e1, a0), (n1, e2, a1) in combinations(occurrences, 2):
p.append(P(node, add_exponents, (n0, n1, a0, e1, e2))) p.append(P(node, add_exponents, (scope, n0, n1, a0, e1, e2)))
return p return p
...@@ -50,11 +52,12 @@ def add_exponents(root, args): ...@@ -50,11 +52,12 @@ def add_exponents(root, args):
""" """
a^p * a^q -> a^(p + q) a^p * a^q -> a^(p + q)
""" """
n0, n1, a, p, q = args scope, n0, n1, a, p, q = args
scope = Scope(root)
# TODO: combine exponent negations
# Replace the left node with the new expression # Replace the left node with the new expression
scope.remove(n0, a ** (p + q)) scope.replace(n0, (a ** (p + q)).negate(n0.negated + n1.negated))
# Remove the right node # Remove the right node
scope.remove(n1) scope.remove(n1)
...@@ -62,7 +65,7 @@ def add_exponents(root, args): ...@@ -62,7 +65,7 @@ def add_exponents(root, args):
return scope.as_nary_node() return scope.as_nary_node()
MESSAGES[add_exponents] = _('Add the exponents of {1} and {2}.') MESSAGES[add_exponents] = _('Add the exponents of {2} and {3}.')
def match_subtract_exponents(node): def match_subtract_exponents(node):
...@@ -91,6 +94,18 @@ def match_subtract_exponents(node): ...@@ -91,6 +94,18 @@ def match_subtract_exponents(node):
return [] return []
def subtract_exponents(root, args):
"""
a^p / a^q -> a^(p - q)
"""
a, p, q = args
return a ** (p - q)
MESSAGES[subtract_exponents] = _('Substract the exponents {2} and {3}.')
def match_multiply_exponents(node): def match_multiply_exponents(node):
""" """
(a^p)^q -> a^(pq) (a^p)^q -> a^(pq)
...@@ -105,34 +120,102 @@ def match_multiply_exponents(node): ...@@ -105,34 +120,102 @@ def match_multiply_exponents(node):
return [] return []
def multiply_exponents(root, args):
"""
(a^p)^q -> a^(pq)
"""
a, p, q = args
return a ** (p * q)
MESSAGES[multiply_exponents] = _('Multiply the exponents {2} and {3}.')
def match_duplicate_exponent(node): def match_duplicate_exponent(node):
""" """
(ab)^p -> a^p * b^p (ab)^p -> a^p * b^p
""" """
assert node.is_op(OP_POW) assert node.is_op(OP_POW)
left, right = node root, exponent = node
if root.is_op(OP_MUL):
return [P(node, duplicate_exponent, (list(Scope(root)), exponent))]
return []
def duplicate_exponent(root, args):
"""
(ab)^p -> a^p * b^p
(abc)^p -> a^p * b^p * c^p
"""
ab, p = args
result = ab[0] ** p
for b in ab[1:]:
result *= b ** p
return result
MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
if left.is_op(OP_MUL): def match_raised_fraction(node):
return [P(node, duplicate_exponent, (list(Scope(left)), right))] """
(a / b) ^ p -> a^p / b^p
"""
assert node.is_op(OP_POW)
root, exponent = node
if root.is_op(OP_DIV):
return [P(node, raised_fraction, (root, exponent))]
return [] return []
def raised_fraction(root, args):
"""
(a / b) ^ p -> a^p / b^p
"""
(a, b), p = args
return a ** p / b ** p
MESSAGES[raised_fraction] = _('Apply the exponent {2} to the nominator and'
' denominator of fraction {1}.')
def match_remove_negative_exponent(node): def match_remove_negative_exponent(node):
""" """
a^-p -> 1 / a^p a ^ -p -> 1 / a ^ p
""" """
assert node.is_op(OP_POW) assert node.is_op(OP_POW)
left, right = node a, p = node
if right.is_op(OP_NEG): if p.negated:
return [P(node, remove_negative_exponent, (left, right[0]))] return [P(node, remove_negative_exponent, (a, p))]
return [] return []
def remove_negative_exponent(root, args):
"""
a^-p -> 1 / a^p
"""
a, p = args
return L(1) / a ** p.reduce_negation()
MESSAGES[remove_negative_exponent] = _('Remove negative exponent {2}.')
def match_exponent_to_root(node): def match_exponent_to_root(node):
""" """
a^(1 / m) -> sqrt(a, m) a^(1 / m) -> sqrt(a, m)
...@@ -148,6 +231,16 @@ def match_exponent_to_root(node): ...@@ -148,6 +231,16 @@ def match_exponent_to_root(node):
return [] return []
def exponent_to_root(root, args):
"""
a^(1 / m) -> sqrt(a, m)
a^(n / m) -> sqrt(a^n, m)
"""
a, n, m = args
return N('sqrt', a if n == 1 else a ** n, m)
def match_extend_exponent(node): def match_extend_exponent(node):
""" """
(a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1) # n > 1 (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1) # n > 1
...@@ -176,64 +269,38 @@ def extend_exponent(root, args): ...@@ -176,64 +269,38 @@ def extend_exponent(root, args):
return left * left return left * left
def subtract_exponents(root, args): def match_constant_exponent(node):
""" """
a^p / a^q -> a^(p - q) (a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1) # n > 1
""" """
a, p, q = args assert node.is_op(OP_POW)
return a ** (p - q)
MESSAGES[subtract_exponents] = _('Substract the exponents {2} and {3}.')
def multiply_exponents(root, args): exponent = node[1]
"""
(a^p)^q -> a^(pq)
"""
a, p, q = args
return a ** (p * q) if exponent == 0:
return [P(node, remove_power_of_zero, ())]
if exponent == 1:
return [P(node, remove_power_of_one, ())]
MESSAGES[multiply_exponents] = _('Multiply the exponents {2} and {3}.') return []
def duplicate_exponent(root, args): def remove_power_of_zero(root, args):
""" """
(ab)^p -> a^p * b^p a ^ 0 -> 1
(abc)^p -> a^p * b^p * c^p
""" """
ab, p = args return L(1)
result = ab[0] ** p
for b in ab[1:]:
result *= b ** p
return result
MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.') MESSAGES[remove_power_of_zero] = _('Power of zero {0} rewrites to 1.')
def remove_negative_exponent(root, args): def remove_power_of_one(root, args):
""" """
a^-p -> 1 / a^p a ^ 1 -> a
""" """
a, p = args return root[0]
return L(1) / a ** p
MESSAGES[remove_negative_exponent] = _('Remove negative exponent {2}.')
def exponent_to_root(root, args): MESSAGES[remove_power_of_one] = _('Remove the power of one in {0}.')
"""
a^(1 / m) -> sqrt(a, m)
a^(n / m) -> sqrt(a^n, m)
"""
a, n, m = args
return N('sqrt', a if n == 1 else a ** n, m)
from itertools import product, combinations
from ..node import Scope, OP_ADD, OP_MUL
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def match_sort_multiplicants(node):
"""
Sort multiplicant factors by swapping
x * 2 -> 2x
"""
assert node.is_op(OP_MUL)
p = []
scope = Scope(node)
for i, n in enumerate(scope[1:]):
left_nb = scope[i]
if n.is_numeric() and not left_nb.is_numeric():
p.append(P(node, move_constant, (scope, n, left_nb)))
return p
def move_constant(root, args):
scope, constant, destination = args
scope.replace(destination, constant * destination)
scope.remove(constant)
return scope.as_nary_node()
MESSAGES[move_constant] = \
_('Move constant {2} to the left of the multiplication {0}.')
def gcd(a, b): from ..node import ExpressionLeaf as L, OP_MUL, OP_DIV
def greatest_common_divisor(a, b):
""" """
Return greatest common divisor using Euclid's Algorithm. Return greatest common divisor of a and b using Euclid's Algorithm.
""" """
while b: while b:
a, b = b, a % b a, b = b, a % b
...@@ -12,7 +15,7 @@ def lcm(a, b): ...@@ -12,7 +15,7 @@ def lcm(a, b):
""" """
Return least common multiple of a and b. Return least common multiple of a and b.
""" """
return a * b // gcd(a, b) return a * b // greatest_common_divisor(a, b)
def least_common_multiple(*args): def least_common_multiple(*args):
...@@ -20,3 +23,51 @@ def least_common_multiple(*args): ...@@ -20,3 +23,51 @@ def least_common_multiple(*args):
Return lcm of args. Return lcm of args.
""" """
return reduce(lcm, args) return reduce(lcm, args)
def is_fraction(node, nominator, denominator):
"""
Check if a node represents the fraction of a given nominator and
denominator.
>>> from ..node import ExpressionLeaf as L
>>> l1, l2, a = L('a'), L(1), L(2)
>>> is_fraction(a / l2, a, 2)
True
>>> is_fraction(l1 / l2 * a, a, 2)
True
>>> is_fraction(l2 / l1 * a, a, 2)
False
"""
if node.is_op(OP_DIV):
nom, denom = node
return nom == nominator and denom == denominator
if node.is_op(OP_MUL):
# 1 / denominator * nominator
# nominator * 1 / denominator
left, right = node
fraction = L(1) / denominator
return (left == nominator and right == fraction) \
or (right == nominator and left == fraction)
return False
def partition(callback, iterable):
"""
Partition an iterable into two parts using a callback that returns a
boolean.
Example:
>>> partition(lambda x: x & 1, range(6))
([1, 3, 5], [0, 2, 4])
"""
a, b = [], []
for item in iterable:
(a if callback(item) else b).append(item)
return a, b
from rules.sort import move_constant
from rules.numerics import reduce_fraction_constants, fraction_to_int_fraction
def pick_suggestion(possibilities):
if not possibilities:
return
# TODO: pick the best suggestion.
for suggestion, p in enumerate(possibilities + [None]):
if p and p.handler not in [move_constant, fraction_to_int_fraction,
reduce_fraction_constants]:
break
if not p:
return possibilities[0]
return possibilities[suggestion]
# vim: set fileencoding=utf-8 :
SQRT = '√'
CBRT = '∛'
FORT = '∜'
PI = 'π'
INFINITY = '∞'
SUP = {
'0': '⁰',
'1': '¹',
'2': '²',
'3': '³',
'3': '⁴',
'5': '⁵',
'6': '⁶',
'7': '⁷',
'8': '⁸',
'9': '⁹',
}
DOT = '⋅'
from src.parser import Parser
from tests.parser import ParserWrapper
from src.possibilities import apply_suggestion
def validate(exp, result):
"""
Validate that exp =>* result.
"""
parser = ParserWrapper(Parser)
result = parser.run([result])
return traverse_preorder(parser, exp, result)
def traverse_preorder(parser, exp, result):
"""
Traverse the possibility tree using pre-order traversal.
"""
root = parser.run([exp])
if root.equals(result):
return root
possibilities = parser.parser.possibilities
for p in possibilities:
child = apply_suggestion(root, p)
next_root = traverse_preorder(parser, str(child), result)
if next_root:
return next_root
...@@ -44,16 +44,26 @@ class RulesTestCase(unittest.TestCase): ...@@ -44,16 +44,26 @@ class RulesTestCase(unittest.TestCase):
try: try:
for i, exp in enumerate(rewrite_chain[:-1]): for i, exp in enumerate(rewrite_chain[:-1]):
self.assertMultiLineEqual(str(rewrite(exp)), self.assertMultiLineEqual(str(rewrite(exp)),
str(rewrite_chain[i+1])) str(rewrite_chain[i + 1]))
except AssertionError: # pragma: nocover except AssertionError as e: # pragma: nocover
print 'rewrite failed: "%s" -> "%s"' \ msg = e.args[0]
% (str(exp), str(rewrite_chain[i+1]))
print 'rewrite chain index: %d' % i
print 'rewrite chain: ---'
for i, c in enumerate(rewrite_chain): msg += '-' * 30 + '\n'
print '%2d %s' % (i, str(c))
print '-' * 30 msg += 'rewrite failed: "%s" -> "%s"\n' \
% (str(exp), str(rewrite_chain[i + 1]))
msg += 'rewrite chain: ---\n'
chain = []
for j, c in enumerate(rewrite_chain):
if i == j:
chain.append('%2d %s <-- error' % (j, str(c)))
else:
chain.append('%2d %s' % (j, str(c)))
e.message = msg + '\n'.join(chain)
e.args = (e.message,) + e.args[1:]
raise raise
...@@ -11,13 +11,13 @@ class TestB1Ch08(unittest.TestCase): ...@@ -11,13 +11,13 @@ class TestB1Ch08(unittest.TestCase):
run_expressions(Parser, [ run_expressions(Parser, [
('6*5^2', L(6) * L(5) ** 2), ('6*5^2', L(6) * L(5) ** 2),
('-5*(-3)^2', (-L(5)) * (-L(3)) ** 2), ('-5*(-3)^2', (-L(5)) * (-L(3)) ** 2),
('7p-3p', L(7) * 'p' + -(L(3) * 'p')), ('7p-3p', L(7) * 'p' + (-L(3) * 'p')),
('-5a*-6', (-L(5)) * 'a' * (-L(6))), ('-5a*-6', (-L(5)) * 'a' * (-L(6))),
('3a-8--5-2a', L(3) * 'a' + -L(8) + -(-L(5)) + -(L(2) * 'a')), ('3a-8--5-2a', L(3) * 'a' + -L(8) + (--L(5)) + (-L(2) * 'a')),
]) ])
def test_diagnostic_test_application(self): def test_diagnostic_test_application(self):
apply_expressions(Parser, [ apply_expressions(Parser, [
('7p+2p', 1, (L(7) + 2) * 'p'), ('7p+2p', 1, (L(7) + 2) * 'p'),
#('7p-3p', 1, (L(7) - 3) * 'p'), ('7p-3p', 1, (L(7) + -L(3)) * 'p'),
]) ])
...@@ -9,12 +9,12 @@ class TestB1Ch10(unittest.TestCase): ...@@ -9,12 +9,12 @@ class TestB1Ch10(unittest.TestCase):
def test_diagnostic_test(self): def test_diagnostic_test(self):
run_expressions(Parser, [ run_expressions(Parser, [
('5(a-2b)', L(5) * (L('a') + -(L(2) * 'b'))), ('5(a-2b)', L(5) * (L('a') + (-L(2) * 'b'))),
('-(3a+6b)', -(L(3) * L('a') + L(6) * 'b')), ('-(3a+6b)', -(L(3) * L('a') + L(6) * 'b')),
('18-(a-12)', L(18) + -(L('a') + -L(12))), ('18-(a-12)', L(18) + -(L('a') + -L(12))),
('-p-q+5(p-q)-3q-2(p-q)', ('-p-q+5(p-q)-3q-2(p-q)',
-L('p') + -L('q') + L(5) * (L('p') + -L('q')) + -(L(3) * 'q') \ -L('p') + -L('q') + L(5) * (L('p') + -L('q')) + (-L(3) * 'q') \
+ - (L(2) * (L('p') + -L('q'))) + (-L(2) * (L('p') + -L('q')))
), ),
('(2+3/7)^4', ('(2+3/7)^4',
N('^', N('+', L(2), N('/', L(3), L(7))), L(4)) N('^', N('+', L(2), N('/', L(3), L(7))), L(4))
......
...@@ -12,11 +12,11 @@ class TestCalc(unittest.TestCase): ...@@ -12,11 +12,11 @@ class TestCalc(unittest.TestCase):
== N('+', L(1), L(4)) == N('+', L(1), L(4))
def test_basic_on_exp(self): def test_basic_on_exp(self):
expressions = [('4', L(4)), expressions = [('4', L(4)),
('3+4', L(3) + L(4)), ('3+4', L(3) + L(4)),
('3-4', L(3) + -L(4)), ('3-4', L(3) + -L(4)),
('3/4', L(3) / L(4)), ('3/4', L(3) / L(4)),
('-4', -L(4)), ('-4', -L(4)),
('3^4', N('^', L(3), L(4))), ('3^4', N('^', L(3), L(4))),
('(2)', L(2))] ('(2)', L(2))]
......
...@@ -7,9 +7,4 @@ from tests.parser import ParserWrapper ...@@ -7,9 +7,4 @@ from tests.parser import ParserWrapper
class TestException(unittest.TestCase): class TestException(unittest.TestCase):
def test_raise(self): def test_raise(self):
try: self.assertRaises(RuntimeError, ParserWrapper(Parser).run, ['raise'])
ParserWrapper(Parser).run(['raise'])
except RuntimeError:
return
raise AssertionError('Expected raised RuntimeError!') # pragma: nocover
This diff is collapsed.
from tests.rulestestcase import RulesTestCase as TestCase
class TestLeidenOefenopgaveV12(TestCase):
def test_1_a(self):
self.assertRewrite(['-5(x2 - 3x + 6)',
'-5(x ^ 2 - 3x) - 5 * 6',
'-5x ^ 2 - 5 * -3x - 5 * 6',
'-5x ^ 2 - -15x - 5 * 6',
'-5x ^ 2 + 15x - 5 * 6',
'-5x ^ 2 + 15x - 30'])
def test_1_d(self):
self.assertRewrite(['(2x + x)x',
'(2 + 1)xx',
'3xx',
'3x ^ (1 + 1)',
'3x ^ 2'])
def test_1_e(self):
self.assertRewrite([
'-2(6x - 4) ^ 2x',
'-2(6x - 4)(6x - 4)x',
'(-2 * 6x - 2 * -4)(6x - 4)x',
'(-12x - 2 * -4)(6x - 4)x',
'(-12x - -8)(6x - 4)x',
'(-12x + 8)(6x - 4)x',
'(-12x * 6x - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72xx - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
'(-72x ^ 2 + 48x + 48x + 8 * -4)x',
'(-72x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
'(-72x ^ 2 + 2 * 48x + 8 * -4)x',
'(-72x ^ 2 + 96x + 8 * -4)x',
'(-72x ^ 2 + 96x - 32)x',
'x(-72x ^ 2 + 96x) + x * -32',
'x * -72x ^ 2 + x * 96x + x * -32',
'-x * 72x ^ 2 + x * 96x + x * -32',
'-x ^ (1 + 2) * 72 + x * 96x + x * -32',
'-x ^ 3 * 72 + x * 96x + x * -32',
'-x ^ 3 * 72 + x ^ (1 + 1) * 96 + x * -32',
'-x ^ 3 * 72 + x ^ 2 * 96 + x * -32',
'-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
'72 * -x ^ 3 + x ^ 2 * 96 - x * 32',
'-72x ^ 3 + x ^ 2 * 96 - x * 32',
'-72x ^ 3 + 96x ^ 2 - x * 32',
'-72x ^ 3 + 96x ^ 2 + 32 * -x',
'-72x ^ 3 + 96x ^ 2 - 32x',
])
def test_2_a(self):
self.assertRewrite([
'(a2b^-1)^3(ab2)',
'(a ^ 2 * (1 / b ^ 1)) ^ 3 * ab ^ 2',
'(a ^ 2 * (1 / b)) ^ 3 * ab ^ 2',
'(a ^ 2 * 1 / b) ^ 3 * ab ^ 2',
'(a ^ 2 / b) ^ 3 * ab ^ 2',
'(a ^ 2) ^ 3 / b ^ 3 * ab ^ 2',
'a ^ (2 * 3) / b ^ 3 * ab ^ 2',
'a ^ 6 / b ^ 3 * ab ^ 2',
'aa ^ 6 / b ^ 3 * b ^ 2',
'a ^ (1 + 6) / b ^ 3 * b ^ 2',
'a ^ 7 / b ^ 3 * b ^ 2',
'b ^ 2 * a ^ 7 / b ^ 3',
'b ^ 2 / b ^ 3 * a ^ 7 / 1',
'b ^ (2 - 3)a ^ 7 / 1',
'b ^ -1 * a ^ 7 / 1',
'1 / b ^ 1 * a ^ 7 / 1',
'1 / b * a ^ 7 / 1',
'a ^ 7 * 1 / b / 1',
'a ^ 7 / b / 1',
'a ^ 7 / b',
])
def test_2_b(self):
self.assertRewrite([
'a3b2a3',
'a ^ (3 + 3)b ^ 2',
'a ^ 6 * b ^ 2',
])
def test_2_c(self):
self.assertRewrite([
'a5+a3',
'a ^ 5 + a ^ 3',
])
def test_2_d(self):
self.assertRewrite([
'a2+a2',
'(1 + 1)a ^ 2',
'2a ^ 2',
])
def test_2_e(self):
self.assertRewrite([
'4b^-2',
'4(1 / b ^ 2)',
'4 * 1 / b ^ 2',
])
def test_2_f(self):
self.assertRewrite([
'(4b) ^ -2',
'4 ^ -2 * b ^ -2',
'1 / 4 ^ 2 * b ^ -2',
'1 / 16 * b ^ -2',
'1 / 16 * (1 / b ^ 2)',
'1 * 1 / (16b ^ 2)',
'1 / (16b ^ 2)',
])
...@@ -30,25 +30,17 @@ class TestNode(RulesTestCase): ...@@ -30,25 +30,17 @@ class TestNode(RulesTestCase):
self.assertTrue(N('+', *self.l[:2]).is_op(OP_ADD)) self.assertTrue(N('+', *self.l[:2]).is_op(OP_ADD))
self.assertFalse(N('-', *self.l[:2]).is_op(OP_ADD)) self.assertFalse(N('-', *self.l[:2]).is_op(OP_ADD))
def test_is_op_or_negated(self):
self.assertTrue(N('+', *self.l[:2]).is_op_or_negated(OP_ADD))
self.assertTrue(N('-', N('+', *self.l[:2])).is_op_or_negated(OP_ADD))
self.assertFalse(N('-', *self.l[:2]).is_op_or_negated(OP_ADD))
self.assertFalse(self.l[0].is_op_or_negated(OP_ADD))
def test_is_leaf(self): def test_is_leaf(self):
self.assertTrue(L(2).is_leaf) self.assertTrue(L(2).is_leaf)
self.assertFalse(N('+', *self.l[:2]).is_leaf) self.assertFalse(N('+', *self.l[:2]).is_leaf)
def test_is_leaf_or_negated(self):
self.assertTrue(L(2).is_leaf_or_negated())
self.assertTrue(N('-', L(2)).is_leaf_or_negated())
self.assertFalse(N('+', *self.l[:2]).is_leaf_or_negated())
self.assertFalse(N('-', N('+', *self.l[:2])).is_leaf_or_negated())
def test_is_power(self): def test_is_power(self):
self.assertTrue(N('^', *self.l[:2]).is_power()) self.assertTrue(N('^', *self.l[2:]).is_power())
self.assertFalse(N('+', *self.l[:2]).is_power()) self.assertFalse(N('+', *self.l[2:]).is_power())
def test_is_power_exponent(self):
self.assertTrue(N('^', *self.l[2:]).is_power(5))
self.assertFalse(N('^', *self.l[2:]).is_power(2))
def test_is_nary(self): def test_is_nary(self):
self.assertTrue(N('+', *self.l[:2]).is_nary()) self.assertTrue(N('+', *self.l[:2]).is_nary())
...@@ -173,6 +165,13 @@ class TestNode(RulesTestCase): ...@@ -173,6 +165,13 @@ class TestNode(RulesTestCase):
m0, m1 = tree('-5 * -3,-5 * 6') m0, m1 = tree('-5 * -3,-5 * 6')
self.assertFalse(m0.equals(m1)) self.assertFalse(m0.equals(m1))
def test_equals_ignore_negation(self):
p0, p1 = tree('-(a + b), a + b')
self.assertTrue(p0.equals(p1, ignore_negation=True))
a0, a1 = tree('-a,a')
self.assertTrue(a0.equals(a1, ignore_negation=True))
def test_scope___init__(self): def test_scope___init__(self):
self.assertEqual(self.scope.node, self.n) self.assertEqual(self.scope.node, self.n)
self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd]) self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd])
...@@ -185,14 +184,14 @@ class TestNode(RulesTestCase): ...@@ -185,14 +184,14 @@ class TestNode(RulesTestCase):
self.scope.remove(self.cd) self.scope.remove(self.cd)
self.assertEqual(self.scope.nodes, [self.a, self.b]) self.assertEqual(self.scope.nodes, [self.a, self.b])
def test_scope_remove_replace(self):
self.scope.remove(self.cd, self.f)
self.assertEqual(self.scope.nodes, [self.a, self.b, self.f])
def test_scope_remove_error(self): def test_scope_remove_error(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.scope.remove(self.f) self.scope.remove(self.f)
def test_scope_replace(self):
self.scope.replace(self.cd, self.f)
self.assertEqual(self.scope.nodes, [self.a, self.b, self.f])
def test_nary_node(self): def test_nary_node(self):
a, b, c, d = tree('a,b,c,d') a, b, c, d = tree('a,b,c,d')
...@@ -205,3 +204,8 @@ class TestNode(RulesTestCase): ...@@ -205,3 +204,8 @@ class TestNode(RulesTestCase):
def test_scope_as_nary_node(self): def test_scope_as_nary_node(self):
self.assertEqualNodes(self.scope.as_nary_node(), self.n) self.assertEqualNodes(self.scope.as_nary_node(), self.n)
def test_scope_as_nary_node_negated(self):
n = tree('-(a + b)')
self.assertEqualNodes(Scope(n).as_nary_node(), n)
self.assertEqualNodes(Scope(-n).as_nary_node(), -n)
...@@ -4,6 +4,8 @@ import unittest ...@@ -4,6 +4,8 @@ import unittest
from src.parser import Parser from src.parser import Parser
from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf from src.node import ExpressionNode as Node, ExpressionLeaf as Leaf
from tests.parser import ParserWrapper, run_expressions, line, graph from tests.parser import ParserWrapper, run_expressions, line, graph
from tests.rulestestcase import tree
from src.rules.goniometry import sin, cos
class TestParser(unittest.TestCase): class TestParser(unittest.TestCase):
...@@ -15,11 +17,42 @@ class TestParser(unittest.TestCase): ...@@ -15,11 +17,42 @@ class TestParser(unittest.TestCase):
run_expressions(Parser, [('a', Leaf('a'))]) run_expressions(Parser, [('a', Leaf('a'))])
def test_graph(self): def test_graph(self):
assert graph(Parser, '4a') == (""" self.assertEqual(graph(Parser, '4a'), ("""
* *
╭┴╮ ╭┴╮
4 a 4 a
""").replace('\n ', '\n')[1:-1] """).replace('\n ', '\n')[1:-1])
def test_line(self): def test_line(self):
self.assertEqual(line(Parser, '4-a'), '4 - a') self.assertEqual(line(Parser, '4-a'), '4 - a')
def test_reset_after_failure(self):
parser = ParserWrapper(Parser)
parser.run(['-(3a+6b)'])
possibilities1 = parser.parser.possibilities
self.assertNotEqual(possibilities1, [])
parser.run(['5+2*6'])
possibilities2 = parser.parser.possibilities
self.assertNotEqual(possibilities2, [])
self.assertNotEqual(possibilities1, possibilities2)
def test_moved_negation(self):
a, b = tree('a,b')
self.assertEqual(tree('-ab'), (-a) * b)
self.assertEqual(tree('-(ab)'), (-a) * b)
self.assertEqual(tree('-a / b'), (-a) / b)
self.assertEqual(tree('-(a / b)'), (-a) / b)
def test_functions(self):
root, x = tree('sin x, x')
self.assertEqual(root, sin(x))
self.assertEqual(tree('sin x ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin(x) ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin (x) ^ 2'), sin(x) ** 2)
self.assertEqual(tree('sin(x ^ 2)'), sin(x ** 2))
self.assertEqual(tree('sin cos x'), sin(cos(x)))
self.assertEqual(tree('sin cos x ^ 2'), sin(cos(x)) ** 2)
...@@ -45,7 +45,7 @@ class TestPossibilities(unittest.TestCase): ...@@ -45,7 +45,7 @@ class TestPossibilities(unittest.TestCase):
possibilities = parser.parser.possibilities possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]), self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="3 + 4" handler=add_numerics' \ '<Possibility root="3 + 4" handler=add_numerics' \
' args=(3, 4, 3, 4)>') ' args=(<Scope of "3 + 4">, 3, 4)>')
def test_multiple_runs(self): def test_multiple_runs(self):
parser = ParserWrapper(Parser) parser = ParserWrapper(Parser)
...@@ -53,21 +53,19 @@ class TestPossibilities(unittest.TestCase): ...@@ -53,21 +53,19 @@ class TestPossibilities(unittest.TestCase):
possibilities = parser.parser.possibilities possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]), self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="1 + 2" handler=add_numerics' \ '<Possibility root="1 + 2" handler=add_numerics' \
' args=(1, 2, 1, 2)>') ' args=(<Scope of "1 + 2">, 1, 2)>')
# Keep previous possibilities (skip whitespace lines) # Remove previous possibilities after second run() call.
parser.run(['', ' ']) parser.run(['', ' '])
possibilities = parser.parser.possibilities possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]), self.assertEqual(possibilities, [])
'<Possibility root="1 + 2" handler=add_numerics' \
' args=(1, 2, 1, 2)>')
# Overwrite previous possibilities with new ones # Overwrite previous possibilities with new ones
parser.run(['3+4']) parser.run(['3+4'])
possibilities = parser.parser.possibilities possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]), self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="3 + 4" handler=add_numerics' \ '<Possibility root="3 + 4" handler=add_numerics' \
' args=(3, 4, 3, 4)>') ' args=(<Scope of "3 + 4">, 3, 4)>')
def test_filter_duplicates(self): def test_filter_duplicates(self):
a, b = ab = tree('a + b') a, b = ab = tree('a + b')
......
from src.rules.fractions import match_constant_division, division_by_one, \ from src.rules.fractions import match_constant_division, division_by_one, \
division_of_zero, division_by_self, match_add_constant_fractions, \ division_of_zero, division_by_self, match_add_constant_fractions, \
equalize_denominators, add_nominators equalize_denominators, add_nominators, match_multiply_fractions, \
multiply_fractions, multiply_with_fraction, match_divide_fractions, \
divide_fraction, divide_by_fraction, match_equal_fraction_parts, \
divide_fraction_parts, extract_divided_roots
from src.node import Scope
from src.possibilities import Possibility as P from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree from tests.rulestestcase import RulesTestCase, tree
...@@ -51,12 +55,14 @@ class TestRulesFractions(RulesTestCase): ...@@ -51,12 +55,14 @@ class TestRulesFractions(RulesTestCase):
n0, n1 = root = l1 / l2 + l3 / l4 n0, n1 = root = l1 / l2 + l3 / l4
possibilities = match_add_constant_fractions(root) possibilities = match_add_constant_fractions(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, equalize_denominators, (n0, n1, 4))]) [P(root, equalize_denominators, (Scope(root), n0, n1, 4)),
P(root, equalize_denominators, (Scope(root), n0, n1, 8))])
(((n0, n1), n2), n3), n4 = root = a + l1 / l2 + b + l3 / l4 + c (((n0, n1), n2), n3), n4 = root = a + l1 / l2 + b + l3 / l4 + c
possibilities = match_add_constant_fractions(root) possibilities = match_add_constant_fractions(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, equalize_denominators, (n1, n3, 4))]) [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
n0, n1 = root = l2 / l4 + l3 / l4 n0, n1 = root = l2 / l4 + l3 / l4
possibilities = match_add_constant_fractions(root) possibilities = match_add_constant_fractions(root)
...@@ -74,7 +80,8 @@ class TestRulesFractions(RulesTestCase): ...@@ -74,7 +80,8 @@ class TestRulesFractions(RulesTestCase):
(((n0, n1), n2), n3), n4 = root = a + l2 / l2 + b + (-l3 / l4) + c (((n0, n1), n2), n3), n4 = root = a + l2 / l2 + b + (-l3 / l4) + c
possibilities = match_add_constant_fractions(root) possibilities = match_add_constant_fractions(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, equalize_denominators, (n1, n3, 4))]) [P(root, equalize_denominators, (Scope(root), n1, n3, 4)),
P(root, equalize_denominators, (Scope(root), n1, n3, 8))])
(((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + (-l3 / l4) + c (((n0, n1), n2), n3), n4 = root = a + l2 / l4 + b + (-l3 / l4) + c
possibilities = match_add_constant_fractions(root) possibilities = match_add_constant_fractions(root)
...@@ -85,22 +92,23 @@ class TestRulesFractions(RulesTestCase): ...@@ -85,22 +92,23 @@ class TestRulesFractions(RulesTestCase):
a, b, l1, l2, l3, l4 = tree('a,b,1,2,3,4') a, b, l1, l2, l3, l4 = tree('a,b,1,2,3,4')
n0, n1 = root = l1 / l2 + l3 / l4 n0, n1 = root = l1 / l2 + l3 / l4
self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)), self.assertEqualNodes(equalize_denominators(root,
l2 / l4 + l3 / l4) (Scope(root), n0, n1, 4)), l2 / l4 + l3 / l4)
n0, n1 = root = a / l2 + b / l4 n0, n1 = root = a / l2 + b / l4
self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)), self.assertEqualNodes(equalize_denominators(root,
(l2 * a) / l4 + b / l4) (Scope(root), n0, n1, 4)), (l2 * a) / l4 + b /
l4)
#2 / 2 - 3 / 4 -> 4 / 4 - 3 / 4 # Equalize denominators #2 / 2 - 3 / 4 -> 4 / 4 - 3 / 4 # Equalize denominators
n0, n1 = root = l1 / l2 + (-l3 / l4) n0, n1 = root = l1 / l2 + (-l3 / l4)
self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)), self.assertEqualNodes(equalize_denominators(root,
l2 / l4 + (-l3 / l4)) (Scope(root), n0, n1, 4)), l2 / l4 + (-l3 / l4))
#2 / 2 - 3 / 4 -> 4 / 4 - 3 / 4 # Equalize denominators #2 / 2 - 3 / 4 -> 4 / 4 - 3 / 4 # Equalize denominators
n0, n1 = root = a / l2 + (-b / l4) n0, n1 = root = a / l2 + (-b / l4)
self.assertEqualNodes(equalize_denominators(root, (n0, n1, 4)), self.assertEqualNodes(equalize_denominators(root,
(l2 * a) / l4 + (-b / l4)) (Scope(root), n0, n1, 4)), (l2 * a) / l4 + (-b / l4))
def test_add_nominators(self): def test_add_nominators(self):
a, b, c = tree('a,b,c') a, b, c = tree('a,b,c')
...@@ -118,3 +126,118 @@ class TestRulesFractions(RulesTestCase): ...@@ -118,3 +126,118 @@ class TestRulesFractions(RulesTestCase):
n0, n1 = root = a / -b + -c / -b n0, n1 = root = a / -b + -c / -b
self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / -b) self.assertEqualNodes(add_nominators(root, (n0, n1)), (a + -c) / -b)
def test_match_multiply_fractions(self):
(a, b), (c, d) = ab, cd = root = tree('a / b * (c / d)')
self.assertEqualPos(match_multiply_fractions(root),
[P(root, multiply_fractions, (Scope(root), ab, cd))])
(ab, e), cd = root = tree('a / b * e * (c / d)')
self.assertEqualPos(match_multiply_fractions(root),
[P(root, multiply_fractions, (Scope(root), ab, cd)),
P(root, multiply_with_fraction, (Scope(root), e, ab)),
P(root, multiply_with_fraction, (Scope(root), e, cd))])
def test_multiply_fractions(self):
(a, b), (c, d) = ab, cd = root = tree('a / b * (c / d)')
self.assertEqual(multiply_fractions(root, (Scope(root), ab, cd)),
a * c / (b * d))
(ab, e), cd = root = tree('a / b * e * (c / d)')
self.assertEqual(multiply_fractions(root, (Scope(root), ab, cd)),
a * c / (b * d) * e)
def test_match_divide_fractions(self):
(a, b), c = root = tree('a / b / c')
self.assertEqualPos(match_divide_fractions(root),
[P(root, divide_fraction, (a, b, c))])
root = tree('a / (b / c)')
self.assertEqualPos(match_divide_fractions(root),
[P(root, divide_by_fraction, (a, b, c))])
def test_divide_fraction(self):
(a, b), c = root = tree('a / b / c')
self.assertEqual(divide_fraction(root, (a, b, c)), a / (b * c))
def test_divide_by_fraction(self):
a, (b, c) = root = tree('a / (b / c)')
self.assertEqual(divide_by_fraction(root, (a, b, c)), a * c / b)
def test_match_equal_fraction_parts(self):
(a, b), (c, a) = root = tree('ab / (ca)')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, divide_fraction_parts, (a, [a, b], [c, a], 0, 1))])
(a, b), a = root = tree('ab / a')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, divide_fraction_parts, (a, [a, b], [a], 0, 0))])
a, (a, b) = root = tree('a / (ab)')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, divide_fraction_parts, (a, [a], [a, b], 0, 0))])
root = tree('abc / (cba)')
((a, b), c) = root[0]
s0, s1 = [a, b, c], [c, b, a]
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, divide_fraction_parts, (a, s0, s1, 0, 2)),
P(root, divide_fraction_parts, (b, s0, s1, 1, 1)),
P(root, divide_fraction_parts, (c, s0, s1, 2, 0))])
root = tree('-a / a')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, divide_fraction_parts, (a, [-a], [a], 0, 0))])
(ap, b), aq = root = tree('a ^ p * b / a ^ q')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, extract_divided_roots, (a, [ap, b], [aq], 0, 0))])
(a, b), aq = root = tree('a * b / a ^ q')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, extract_divided_roots, (a, [a, b], [aq], 0, 0))])
(ap, b), a = root = tree('a ^ p * b / a')
self.assertEqualPos(match_equal_fraction_parts(root),
[P(root, extract_divided_roots, (a, [ap, b], [a], 0, 0))])
def test_divide_fraction_parts(self):
(a, b), (c, a) = root = tree('ab / (ca)')
result = divide_fraction_parts(root, (a, [a, b], [c, a], 0, 1))
self.assertEqual(result, b / c)
(a, b), a = root = tree('ab / a')
result = divide_fraction_parts(root, (a, [a, b], [a], 0, 0))
self.assertEqual(result, b / 1)
root, l1 = tree('a / (ab), 1')
a, (a, b) = root
result = divide_fraction_parts(root, (a, [a], [a, b], 0, 0))
self.assertEqual(result, l1 / b)
root = tree('abc / (cba)')
((a, b), c) = root[0]
result = divide_fraction_parts(root, (a, [a, b, c], [c, b, a], 0, 2))
self.assertEqual(result, b * c / (c * b))
result = divide_fraction_parts(root, (b, [a, b, c], [c, b, a], 1, 1))
self.assertEqual(result, a * c / (c * a))
result = divide_fraction_parts(root, (c, [a, b, c], [c, b, a], 2, 0))
self.assertEqual(result, a * b / (b * a))
(a, b), a = root = tree('-ab / a')
result = divide_fraction_parts(root, (a, [-a, b], [a], 0, 0))
self.assertEqual(result, -b / 1)
def test_extract_divided_roots(self):
r, a = tree('a ^ p * b / a ^ q, a')
((a, p), b), (a, q) = (ap, b), aq = r
self.assertEqual(extract_divided_roots(r, (a, [ap, b], [aq], 0, 0)),
a ** p / a ** q * b / 1)
r = tree('a * b / a ^ q, a')
self.assertEqual(extract_divided_roots(r, (a, [a, b], [aq], 0, 0)),
a / a ** q * b / 1)
r = tree('a ^ p * b / a, a')
self.assertEqual(extract_divided_roots(r, (a, [ap, b], [a], 0, 0)),
a ** p / a * b / 1)
# vim: set fileencoding=utf-8 :
from src.rules.goniometry import sin, cos, tan, match_add_quadrants, \
add_quadrants, match_negated_parameter, negated_sinus_parameter, \
negated_cosinus_parameter, match_standard_radian, standard_radian, \
is_pi_frac
from src.node import PI, OP_SIN, OP_COS, OP_TAN
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
from src.rules import goniometry
import doctest
class TestRulesGoniometry(RulesTestCase):
def test_doctest(self):
self.assertEqual(doctest.testmod(m=goniometry)[0], 0)
def test_match_add_quadrants(self):
root = tree('sin t ^ 2 + cos t ^ 2')
possibilities = match_add_quadrants(root)
self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
def test_add_quadrants(self):
self.assertEqual(add_quadrants(None, ()), 1)
def test_match_negated_parameter(self):
s, c = tree('sin -t, cos -t')
t = s[0]
self.assertEqualPos(match_negated_parameter(s), \
[P(s, negated_sinus_parameter, (t,))])
self.assertEqualPos(match_negated_parameter(c), \
[P(c, negated_cosinus_parameter, (t,))])
def test_negated_sinus_parameter(self):
s = tree('sin -t')
t = s[0]
self.assertEqual(negated_sinus_parameter(s, (t,)), -sin(+t))
def test_negated_cosinus_parameter(self):
c = tree('cos -t')
t = c[0]
self.assertEqual(negated_cosinus_parameter(c, (t,)), cos(+t))
def test_is_pi_frac(self):
l1, pi = tree('1,' + PI)
self.assertTrue(is_pi_frac(l1 / 2 * pi, 2))
self.assertFalse(is_pi_frac(l1 / 2 * pi, 3))
self.assertFalse(is_pi_frac(l1 * pi, 3))
def test_match_standard_radian(self):
s, c, t = tree('sin(1 / 6 * pi), cos(1 / 2 * pi), tan(0)')
self.assertEqualPos(match_standard_radian(s), \
[P(s, standard_radian, (OP_SIN, 1))])
self.assertEqualPos(match_standard_radian(c), \
[P(c, standard_radian, (OP_COS, 4))])
self.assertEqualPos(match_standard_radian(t), \
[P(t, standard_radian, (OP_TAN, 0))])
def test_standard_radian(self):
l0, l1, sq3, pi6, pi4, pi2 = tree('0,1,sqrt(3),1/6*pi,1/4*pi,1/2*pi')
self.assertEqual(standard_radian(sin(pi6), (OP_SIN, 1)), l1 / 2)
self.assertEqual(standard_radian(sin(pi2), (OP_SIN, 4)), 1)
self.assertEqual(standard_radian(cos(l0), (OP_COS, 0)), 1)
self.assertEqual(standard_radian(tan(pi4), (OP_TAN, 3)), sq3)
from src.rules.groups import match_combine_groups, combine_groups from src.rules.groups import match_combine_groups, combine_groups
from src.node import Scope
from src.possibilities import Possibility as P from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree from tests.rulestestcase import RulesTestCase, tree
...@@ -6,64 +7,88 @@ from tests.rulestestcase import RulesTestCase, tree ...@@ -6,64 +7,88 @@ from tests.rulestestcase import RulesTestCase, tree
class TestRulesGroups(RulesTestCase): class TestRulesGroups(RulesTestCase):
def test_match_combine_groups_no_const(self): def test_match_combine_groups_no_const(self):
a0, a1 = root = tree('a + a') root, l1 = tree('a + a,1')
a0, a1 = root
possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities,
[P(root, combine_groups, (Scope(root), l1, a0, a0,
l1, a1, a1))])
def test_match_combine_groups_negation(self):
root, l1 = tree('-a + a,1')
a0, a1 = root
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (1, a0, a0, 1, a1, a1))]) [P(root, combine_groups, (Scope(root), -l1, +a0, a0,
l1, a1, a1))])
def test_match_combine_groups_single_const(self): def test_match_combine_groups_single_const(self):
a0, mul = root = tree('a + 2a') root, l1 = tree('a + 2a,1')
a0, mul = root
l2, a1 = mul l2, a1 = mul
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (1, a0, a0, l2, a1, mul))]) [P(root, combine_groups, (Scope(root), l1, a0, a0,
l2, a1, mul))])
def test_match_combine_groups_two_const(self): def test_match_combine_groups_two_const(self):
((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a') ((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a')
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (l2, a0, m0, l3, a1, m1))]) [P(root, combine_groups, (Scope(root), l2, a0, m0,
l3, a1, m1))])
def test_match_combine_groups_n_const(self): def test_match_combine_groups_n_const(self):
((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a') ((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a')
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (l2, a0, m0, l3, a1, m1)), [P(root, combine_groups, (Scope(root), l2, a0, m0,
P(root, combine_groups, (l2, a0, m0, l4, a2, m2)), l3, a1, m1)),
P(root, combine_groups, (l3, a1, m1, l4, a2, m2))]) P(root, combine_groups, (Scope(root), l2, a0, m0,
l4, a2, m2)),
P(root, combine_groups, (Scope(root), l3, a1, m1,
l4, a2, m2))])
def test_match_combine_groups_identifier_group_no_const(self): def test_match_combine_groups_identifier_group_no_const(self):
ab0, ab1 = root = tree('ab + ab') root, l1 = tree('ab + ab,1')
ab0, ab1 = root
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (1, ab0, ab0, 1, ab1, ab1))]) [P(root, combine_groups, (Scope(root), l1, ab0, ab0,
l1, ab1, ab1))])
def test_match_combine_groups_identifier_group_single_const(self): def test_match_combine_groups_identifier_group_single_const(self):
m0, m1 = root = tree('ab + 2ab') root, l1 = tree('ab + 2ab,1')
m0, m1 = root
(l2, a), b = m1 (l2, a), b = m1
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (1, m0, m0, l2, a * b, m1))]) [P(root, combine_groups, (Scope(root), l1, m0, m0,
l2, a * b, m1))])
def test_match_combine_groups_identifier_group_unordered(self): def test_match_combine_groups_identifier_group_unordered(self):
m0, m1 = root = tree('ab + ba') root, l1 = tree('ab + ba,1')
m0, m1 = root
b, a = m1 b, a = m1
possibilities = match_combine_groups(root) possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, combine_groups, (1, m0, m0, 1, b * a, m1))]) [P(root, combine_groups, (Scope(root), l1, m0, m0,
l1, b * a, m1))])
def test_combine_groups_simple(self): def test_combine_groups_simple(self):
root, l1 = tree('a + a,1') root, l1 = tree('a + a,1')
a0, a1 = root a0, a1 = root
self.assertEqualNodes(combine_groups(root, (1, a0, a0, 1, a1, a1)), self.assertEqualNodes(combine_groups(root,
(Scope(root), l1, a0, a0, l1, a1, a1)),
(l1 + 1) * a0) (l1 + 1) * a0)
def test_combine_groups_nary(self): def test_combine_groups_nary(self):
...@@ -71,5 +96,6 @@ class TestRulesGroups(RulesTestCase): ...@@ -71,5 +96,6 @@ class TestRulesGroups(RulesTestCase):
abb, ba = root abb, ba = root
ab, b = abb ab, b = abb
self.assertEqualNodes(combine_groups(root, (1, ab, ab, 1, ba, ba)), self.assertEqualNodes(combine_groups(root,
(Scope(root), l1, ab, ab, l1, ba, ba)),
(l1 + 1) * ab + b) (l1 + 1) * ab + b)
from src.rules.negation import match_negated_factor, negated_factor, \
from src.rules.negation import match_negated_division, \ match_negate_polynome, negate_polynome, double_negation, \
single_negated_division, double_negated_division match_negated_division, single_negated_division, \
double_negated_division
from src.node import Scope
from src.possibilities import Possibility as P from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree from tests.rulestestcase import RulesTestCase, tree
class TestRulesNegation(RulesTestCase): class TestRulesNegation(RulesTestCase):
def test_match_negated_factor(self):
a, b = root = tree('a * -b')
self.assertEqualPos(match_negated_factor(root),
[P(root, negated_factor, (Scope(root), b))])
(a, b), c = root = tree('a * -b * -c')
scope = Scope(root)
self.assertEqualPos(match_negated_factor(root),
[P(root, negated_factor, (scope, b)),
P(root, negated_factor, (scope, c))])
def test_negated_factor(self):
a, b = root = tree('a * -b')
self.assertEqualNodes(negated_factor(root, (Scope(root), b)),
-a * +b)
(a, b), c = root = tree('a * -b * -c')
self.assertEqualNodes(negated_factor(root, (Scope(root), b)),
-a * +b * c)
self.assertEqualNodes(negated_factor(root, (Scope(root), c)),
-a * b * +c)
def test_match_negate_polynome(self):
root = tree('--a')
self.assertEqualPos(match_negate_polynome(root),
[P(root, double_negation, ())])
root = tree('-(a + b)')
self.assertEqualPos(match_negate_polynome(root),
[P(root, negate_polynome, ())])
def test_double_negation(self):
root = tree('--a')
self.assertEqualNodes(double_negation(root, ()), ++root)
def test_negate_polynome(self):
a, b = root = tree('-(a + b)')
self.assertEqualNodes(negate_polynome(root, ()), -a + -b)
a, b = root = tree('-(a - b)')
self.assertEqualNodes(negate_polynome(root, ()), -a + -b)
def test_match_negated_division_none(self): def test_match_negated_division_none(self):
self.assertEqual(match_negated_division(tree('1 / 2')), []) self.assertEqual(match_negated_division(tree('1 / 2')), [])
def test_match_negated_division_single(self): def test_match_negated_division_single(self):
l1, l2 = root = tree('-1 / 2') l1, l2 = root = tree('-1 / 2')
possibilities = match_negated_division(root) self.assertEqualPos(match_negated_division(root), [])
self.assertEqualPos(possibilities,
[P(root, single_negated_division, (l1[0], l2))])
l1, l2 = root = tree('1 / -2') l1, l2 = root = tree('1 / -2')
possibilities = match_negated_division(root) possibilities = match_negated_division(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, single_negated_division, (l1, l2[0]))]) [P(root, single_negated_division, (l1, +l2))])
def test_match_negated_division_double(self): def test_match_negated_division_double(self):
root = tree('-1 / -2') root = tree('-1 / -2')
possibilities = match_negated_division(root) possibilities = match_negated_division(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, double_negated_division, (root,))]) [P(root, double_negated_division, ())])
def test_single_negated_division(self): def test_single_negated_division(self):
l1, l2 = root = tree('-1 / 2')
self.assertEqualNodes(single_negated_division(root, (l1[0], l2)),
-(l1[0] / l2))
l1, l2 = root = tree('1 / -2') l1, l2 = root = tree('1 / -2')
self.assertEqualNodes(single_negated_division(root, (l1, l2[0])), self.assertEqualNodes(single_negated_division(root, (l1, +l2)),
-(l1 / l2[0])) -l1 / +l2)
def test_double_negated_division(self): def test_double_negated_division(self):
l1, l2 = root = tree('-1 / -2') l1, l2 = root = tree('-1 / -2')
self.assertEqualNodes(double_negated_division(root, (root,)), self.assertEqualNodes(double_negated_division(root, ()),
l1[0] / l2[0]) +l1 / +l2)
from src.rules.numerics import add_numerics, match_divide_numerics, \ from src.rules.numerics import match_add_numerics, add_numerics, \
divide_numerics, match_multiply_numerics, multiply_numerics match_divide_numerics, divide_numerics, reduce_fraction_constants, \
fraction_to_int_fraction, match_multiply_numerics, multiply_numerics, \
raise_numerics
from src.node import ExpressionLeaf as L, Scope
from src.possibilities import Possibility as P from src.possibilities import Possibility as P
from src.node import ExpressionLeaf as L
from tests.rulestestcase import RulesTestCase, tree from tests.rulestestcase import RulesTestCase, tree
class TestRulesNumerics(RulesTestCase): class TestRulesNumerics(RulesTestCase):
def test_match_add_numerics(self):
l1, l2 = root = tree('1 + 2')
possibilities = match_add_numerics(root)
self.assertEqualPos(possibilities,
[P(root, add_numerics, (Scope(root), l1, l2))])
(l1, b), l2 = root = tree('1 + b + 2')
possibilities = match_add_numerics(root)
self.assertEqualPos(possibilities,
[P(root, add_numerics, (Scope(root), l1, l2))])
def test_add_numerics(self): def test_add_numerics(self):
l0, a, l1 = tree('1,a,2') l0, a, l1 = tree('1,a,2')
self.assertEqual(add_numerics(l0 + l1, (l0, l1, L(1), L(2))), 3) root = l0 + l1
self.assertEqual(add_numerics(l0 + a + l1, (l0, l1, L(1), L(2))), self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), 3)
L(3) + a) root = l0 + a + l1
self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), L(3) + a)
def test_add_numerics_negations(self): def test_add_numerics_negations(self):
l0, a, l1 = tree('1,a,2') l1, a, l2 = tree('1,a,2')
ml1, ml2 = -l1, -l2
self.assertEqual(add_numerics(-l0 + l1, (-l0, l1, -L(1), L(2))), 1) r = ml1 + l2
self.assertEqual(add_numerics(l0 + -l1, (l0, -l1, L(1), -L(2))), -1) self.assertEqual(add_numerics(r, (Scope(r), ml1, l2)), 1)
self.assertEqual(add_numerics(l0 + a + -l1, (l0, -l1, L(1), -L(2))), r = l1 + ml2
L(-1) + a) self.assertEqual(add_numerics(r, (Scope(r), l1, ml2)), -1)
def test_match_divide_numerics(self): def test_match_divide_numerics(self):
a, b, i2, i3, i6, f1, f2, f3 = tree('a,b,2,3,6,1.0,2.0,3.0') a, b, i2, i3, i4, i6, f1, f2, f3 = tree('a,b,2,3,4,6,1.0,2.0,3.0')
root = i6 / i2 root = i6 / i2
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, divide_numerics, (6, 2))]) [P(root, divide_numerics, (6, 2, 0))])
root = -i6 / i2
possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities,
[P(root, divide_numerics, (6, 2, 1))])
root = i3 / i2 root = i3 / i2
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, []) self.assertEqualPos(possibilities,
[P(root, fraction_to_int_fraction, (1, 1, 2))])
root = i2 / i4
possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities,
[P(root, reduce_fraction_constants, (2,))])
root = f3 / i2 root = f3 / i2
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, divide_numerics, (3.0, 2))]) [P(root, divide_numerics, (3.0, 2, 0))])
root = i3 / f2 root = i3 / f2
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, divide_numerics, (3, 2.0))]) [P(root, divide_numerics, (3, 2.0, 0))])
root = f3 / f2 root = f3 / f2
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, divide_numerics, (3.0, 2.0))]) [P(root, divide_numerics, (3.0, 2.0, 0))])
root = i3 / f1 root = i3 / f1
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, divide_numerics, (3, 1))]) [P(root, divide_numerics, (3, 1, 0))])
root = a / b root = a / b
possibilities = match_divide_numerics(root) possibilities = match_divide_numerics(root)
...@@ -61,55 +87,81 @@ class TestRulesNumerics(RulesTestCase): ...@@ -61,55 +87,81 @@ class TestRulesNumerics(RulesTestCase):
def test_divide_numerics(self): def test_divide_numerics(self):
i2, i3, i6, f2, f3 = tree('2,3,6,2.0,3.0') i2, i3, i6, f2, f3 = tree('2,3,6,2.0,3.0')
self.assertEqual(divide_numerics(i6 / i2, (6, 2)), 3) self.assertEqual(divide_numerics(i6 / i2, (6, 2, 0)), 3)
self.assertEqual(divide_numerics(f3 / i2, (3.0, 2)), 1.5) self.assertEqual(divide_numerics(f3 / i2, (3.0, 2, 0)), 1.5)
self.assertEqual(divide_numerics(i3 / f2, (3, 2.0)), 1.5) self.assertEqual(divide_numerics(i3 / f2, (3, 2.0, 0)), 1.5)
self.assertEqual(divide_numerics(f3 / f2, (3.0, 2.0)), 1.5) self.assertEqual(divide_numerics(f3 / f2, (3.0, 2.0, 0)), 1.5)
self.assertEqual(divide_numerics(i6 / i2, (6, 2, 1)), -3)
self.assertEqual(divide_numerics(i6 / i2, (6, 2, 2)), --i3)
def test_reduce_fraction_constants(self):
l1, l2 = tree('1,2')
self.assertEqual(reduce_fraction_constants(l2 / 4, (2,)), l1 / l2)
def test_fraction_to_int_fraction(self):
l1, l4 = tree('1,4')
self.assertEqual(fraction_to_int_fraction(l4 / 3, (1, 1, 3)),
l1 + l1 / 3)
def test_match_multiply_numerics(self): def test_match_multiply_numerics(self):
i2, i3, i6, f2, f3, f6 = tree('2,3,6,2.0,3.0,6.0') i2, i3, i6, f2, f3, f6 = tree('2,3,6,2.0,3.0,6.0')
root = i3 * i2 root = i3 * i2
self.assertEqual(match_multiply_numerics(root), self.assertEqual(match_multiply_numerics(root),
[P(root, multiply_numerics, (i3, i2, 3, 2))]) [P(root, multiply_numerics, (Scope(root), i3, i2))])
root = f3 * i2 root = f3 * i2
self.assertEqual(match_multiply_numerics(root), self.assertEqual(match_multiply_numerics(root),
[P(root, multiply_numerics, (f3, i2, 3.0, 2))]) [P(root, multiply_numerics, (Scope(root), f3, i2))])
root = i3 * f2 root = i3 * f2
self.assertEqual(match_multiply_numerics(root), self.assertEqual(match_multiply_numerics(root),
[P(root, multiply_numerics, (i3, f2, 3, 2.0))]) [P(root, multiply_numerics, (Scope(root), i3, f2))])
root = f3 * f2 root = f3 * f2
self.assertEqual(match_multiply_numerics(root), self.assertEqual(match_multiply_numerics(root),
[P(root, multiply_numerics, (f3, f2, 3.0, 2.0))]) [P(root, multiply_numerics, (Scope(root), f3, f2))])
def test_multiply_numerics(self): def test_multiply_numerics(self):
a, b, i2, i3, i6, f2, f3, f6 = tree('a,b,2,3,6,2.0,3.0,6.0') a, b, i2, i3, i6, f2, f3, f6 = tree('a,b,2,3,6,2.0,3.0,6.0')
self.assertEqual(multiply_numerics(i3 * i2, (i3, i2, 3, 2)), 6) root = i3 * i2
self.assertEqual(multiply_numerics(f3 * i2, (f3, i2, 3.0, 2)), 6.0) self.assertEqual(multiply_numerics(root, (Scope(root), i3, i2)), 6)
self.assertEqual(multiply_numerics(i3 * f2, (i3, f2, 3, 2.0)), 6.0) root = f3 * i2
self.assertEqual(multiply_numerics(f3 * f2, (f3, f2, 3.0, 2.0)), 6.0) self.assertEqual(multiply_numerics(root, (Scope(root), f3, i2)), 6.0)
root = i3 * f2
self.assertEqual(multiply_numerics(root, (Scope(root), i3, f2)), 6.0)
root = f3 * f2
self.assertEqual(multiply_numerics(root, (Scope(root), f3, f2)), 6.0)
self.assertEqualNodes(multiply_numerics(a * i3 * i2 * b, (i3, i2, 3, 2)), root = a * i3 * i2 * b
a * 6 * b) self.assertEqualNodes(multiply_numerics(root,
(Scope(root), i3, i2)), a * 6 * b)
def test_multiply_numerics_negation(self): def test_multiply_numerics_negation(self):
l1_neg, l2 = root = tree('-1 * 2') l1_neg, l2 = root = tree('-1 * 2')
self.assertEqualNodes(multiply_numerics(root, (l1_neg, l2, -1, 2)), -l2) self.assertEqualNodes(multiply_numerics(root, (Scope(root), l1_neg,
l2)), -l2)
root, l6 = tree('1 - 2 * 3,6')
l1, neg = root
l2, l3 = mul = neg[0]
self.assertEqualNodes(multiply_numerics(mul, (l2, l3, 2, 3)), l6)
l1, mul = root = tree('1 + -2 * 3') root, l6 = tree('1 + -2 * 3,6')
l1, mul = root
l2_neg, l3 = mul l2_neg, l3 = mul
self.assertEqualNodes(multiply_numerics(mul, (l2_neg, l3, -2, 3)), -l6) self.assertEqualNodes(multiply_numerics(mul, (Scope(mul),
l2_neg, l3)), -l6)
root, l30 = tree('-5 * x ^ 2 - -15x - 5 * 6,30') root, l30 = tree('-5 * x ^ 2 - -15x - 5 * 6,30')
rest, mul_neg = root rest, mul = root
l5_neg, l6 = mul = mul_neg[0] l5_neg, l6 = mul
self.assertEqualNodes(multiply_numerics(mul, (l5_neg, l6, 5, 6)), l30) self.assertEqualNodes(multiply_numerics(mul, (Scope(mul),
l5_neg, l6)), -l30)
def test_raise_numerics(self):
l1, l2 = root = tree('2 ^ 3')
self.assertEqualNodes(raise_numerics(root, (l1, l2)), L(8))
l1_neg, l2 = root = tree('(-2) ^ 2')
self.assertEqualNodes(raise_numerics(root, (l1_neg, l2)), --L(4))
l1_neg, l2 = root = tree('(-2) ^ 3')
self.assertEqualNodes(raise_numerics(root, (l1_neg, l2)), ---L(8))
from src.rules.poly import match_combine_polynomes, combine_polynomes
from src.rules.numerics import add_numerics
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
class TestRulesPoly(RulesTestCase):
def test_identifiers_basic(self):
a1, a2 = root = tree('a+a')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 1, 1, 'a', 1))])
def test_identifiers_normal(self):
a1, a2 = root = tree('a+2a')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 1, 2, 'a', 1))])
def test_identifiers_reverse(self):
a1, a2 = root = tree('a+a*2')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 1, 2, a1, 1))])
def test_identifiers_exponent(self):
a1, a2 = root = tree('a2+a2')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 1, 1, 'a', 2))])
def test_identifiers_coeff_exponent_left(self):
a1, a2 = root = tree('2a3+a3')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 2, 1, 'a', 3))])
def test_identifiers_coeff_exponent_both(self):
a1, a2 = root = tree('2a3+2a3')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (a1, a2, 2, 2, 'a', 3))])
def test_basic_subexpressions(self):
a_b, c, d = tree('a+b,c,d')
left, right = root = tree('(a+b)^d + (a+b)^d')
self.assertEqual(left, right)
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (left, right, 1, 1, a_b, d))])
left, right = root = tree('5(a+b)^d + 7(a+b)^d')
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, combine_polynomes, (left, right, 5, 7, a_b, d))])
# TODO: Move to other strategy
#left, right = root = tree('c(a+b)^d + c(a+b)^d')
#self.assertEqual(left, right)
#possibilities = match_combine_polynomes(root)
#self.assertEqualPos(possibilities,
# [P(root, combine_polynomes, (left, right, c, c, a_b, d))])
def test_match_add_numerics(self):
l0, l1, l2 = tree('0,1,2')
root = l0 + l1 + l2
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, add_numerics, (l0, l1, l0, l1)),
P(root, add_numerics, (l0, l2, l0, l2)),
P(root, add_numerics, (l1, l2, l1, l2))])
def test_match_add_numerics_explicit_powers(self):
l0, l1, l2 = tree('0^1,1*1,1*2^1')
root = l0 + l1 + l2
possibilities = match_combine_polynomes(root)
self.assertEqualPos(possibilities,
[P(root, add_numerics, (l0, l1, l0[0], l1[1])),
P(root, add_numerics, (l0, l2, l0[0], l2[1][0])),
P(root, add_numerics, (l1, l2, l1[1], l2[1][0]))])
def test_combine_polynomes(self):
# 2a + 3a -> (2 + 3) * a
l0, a, l1, l2 = tree('2,a,3,1')
root = l0 * a + l1 * a
left, right = root
replacement = combine_polynomes(root, (left, right, l0, l1, a, 1))
self.assertEqualNodes(replacement, (l0 + l1) * a)
# a + 3a -> (1 + 3) * a
root = a + l1 * a
left, right = root
replacement = combine_polynomes(root, (left, right, l2, l1, a, 1))
self.assertEqualNodes(replacement, (l2 + l1) * a)
# 2a + a -> (2 + 1) * a
root = l0 * a + a
left, right = root
replacement = combine_polynomes(root, (left, right, l0, l2, a, 1))
self.assertEqualNodes(replacement, (l0 + 1) * a)
# a + a -> (1 + 1) * a
root = a + a
left, right = root
replacement = combine_polynomes(root, (left, right, l2, l2, a, 1))
self.assertEqualNodes(replacement, (l2 + 1) * a)
...@@ -2,10 +2,12 @@ from src.rules.powers import match_add_exponents, add_exponents, \ ...@@ -2,10 +2,12 @@ from src.rules.powers import match_add_exponents, add_exponents, \
match_subtract_exponents, subtract_exponents, \ match_subtract_exponents, subtract_exponents, \
match_multiply_exponents, multiply_exponents, \ match_multiply_exponents, multiply_exponents, \
match_duplicate_exponent, duplicate_exponent, \ match_duplicate_exponent, duplicate_exponent, \
match_raised_fraction, raised_fraction, \
match_remove_negative_exponent, remove_negative_exponent, \ match_remove_negative_exponent, remove_negative_exponent, \
match_exponent_to_root, exponent_to_root match_exponent_to_root, exponent_to_root, \
match_constant_exponent, remove_power_of_zero, remove_power_of_one
from src.node import Scope, ExpressionNode as N
from src.possibilities import Possibility as P from src.possibilities import Possibility as P
from src.node import ExpressionNode as N
from tests.rulestestcase import RulesTestCase, tree from tests.rulestestcase import RulesTestCase, tree
...@@ -17,7 +19,7 @@ class TestRulesPowers(RulesTestCase): ...@@ -17,7 +19,7 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root) possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, add_exponents, (n0, n1, a, p, q))]) [P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
def test_match_add_exponents_ternary(self): def test_match_add_exponents_ternary(self):
a, p, q, r = tree('a,p,q,r') a, p, q, r = tree('a,p,q,r')
...@@ -25,9 +27,9 @@ class TestRulesPowers(RulesTestCase): ...@@ -25,9 +27,9 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root) possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, add_exponents, (n0, n1, a, p, q)), [P(root, add_exponents, (Scope(root), n0, n1, a, p, q)),
P(root, add_exponents, (n0, n2, a, p, r)), P(root, add_exponents, (Scope(root), n0, n2, a, p, r)),
P(root, add_exponents, (n1, n2, a, q, r))]) P(root, add_exponents, (Scope(root), n1, n2, a, q, r))])
def test_match_add_exponents_multiple_identifiers(self): def test_match_add_exponents_multiple_identifiers(self):
a, b, p, q = tree('a,b,p,q') a, b, p, q = tree('a,b,p,q')
...@@ -35,8 +37,24 @@ class TestRulesPowers(RulesTestCase): ...@@ -35,8 +37,24 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root) possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, add_exponents, (a0, a1, a, p, q)), [P(root, add_exponents, (Scope(root), a0, a1, a, p, q)),
P(root, add_exponents, (b0, b1, b, p, q))]) P(root, add_exponents, (Scope(root), b0, b1, b, p, q))])
def test_match_add_exponents_nary_multiplication(self):
a, p, q = tree('a,p,q')
(n0, l1), n1 = root = a ** p * 2 * a ** q
possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities,
[P(root, add_exponents, (Scope(root), n0, n1, a, p, q))])
def test_match_add_exponents_negated(self):
a, q = tree('a,q')
n0, n1 = root = (-a) * a ** q
possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities,
[P(root, add_exponents, (Scope(root), n0, n1, a, 1, q))])
def test_match_subtract_exponents_powers(self): def test_match_subtract_exponents_powers(self):
a, p, q = tree('a,p,q') a, p, q = tree('a,p,q')
...@@ -78,13 +96,25 @@ class TestRulesPowers(RulesTestCase): ...@@ -78,13 +96,25 @@ class TestRulesPowers(RulesTestCase):
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, duplicate_exponent, ([a, b], p))]) [P(root, duplicate_exponent, ([a, b], p))])
def test_match_raised_fraction(self):
ab, p = root = tree('(a / b) ^ p')
self.assertEqualPos(match_raised_fraction(root),
[P(root, raised_fraction, (ab, p))])
def test_raised_fraction(self):
ab, p = root = tree('(a / b) ^ p')
a, b = ab
self.assertEqual(raised_fraction(root, (ab, p)), a ** p / b ** p)
def test_match_remove_negative_exponent(self): def test_match_remove_negative_exponent(self):
a, p = tree('a,p') a, p = tree('a,p')
root = a ** -p root = a ** -p
possibilities = match_remove_negative_exponent(root) possibilities = match_remove_negative_exponent(root)
self.assertEqualPos(possibilities, self.assertEqualPos(possibilities,
[P(root, remove_negative_exponent, (a, p))]) [P(root, remove_negative_exponent, (a, -p))])
def test_match_exponent_to_root(self): def test_match_exponent_to_root(self):
a, n, m, l1 = tree('a,n,m,1') a, n, m, l1 = tree('a,n,m,1')
...@@ -103,7 +133,8 @@ class TestRulesPowers(RulesTestCase): ...@@ -103,7 +133,8 @@ class TestRulesPowers(RulesTestCase):
a, p, q = tree('a,p,q') a, p, q = tree('a,p,q')
n0, n1 = root = a ** p * a ** q n0, n1 = root = a ** p * a ** q
self.assertEqualNodes(add_exponents(root, (n0, n1, a, p, q)), a ** (p + q)) self.assertEqualNodes(add_exponents(root,
(Scope(root), n0, n1, a, p, q)), a ** (p + q))
def test_subtract_exponents(self): def test_subtract_exponents(self):
a, p, q = tree('a,p,q') a, p, q = tree('a,p,q')
...@@ -131,11 +162,11 @@ class TestRulesPowers(RulesTestCase): ...@@ -131,11 +162,11 @@ class TestRulesPowers(RulesTestCase):
a ** p * b ** p * c ** p) a ** p * b ** p * c ** p)
def test_remove_negative_exponent(self): def test_remove_negative_exponent(self):
a, p, l1 = tree('a,p,1') a, p, l1 = tree('a,-p,1')
root = a ** -p root = a ** p
self.assertEqualNodes(remove_negative_exponent(root, (a, p)), self.assertEqualNodes(remove_negative_exponent(root, (a, p)),
l1 / a ** p) l1 / a ** +p)
def test_exponent_to_root(self): def test_exponent_to_root(self):
a, n, m, l1 = tree('a,n,m,1') a, n, m, l1 = tree('a,n,m,1')
...@@ -146,3 +177,21 @@ class TestRulesPowers(RulesTestCase): ...@@ -146,3 +177,21 @@ class TestRulesPowers(RulesTestCase):
self.assertEqualNodes(exponent_to_root(root, (a, l1, m)), self.assertEqualNodes(exponent_to_root(root, (a, l1, m)),
N('sqrt', a, m)) N('sqrt', a, m))
def test_match_constant_exponent(self):
a0, a1, a2 = tree('a0,a1,a2')
self.assertEqualPos(match_constant_exponent(a0),
[P(a0, remove_power_of_zero, ())])
self.assertEqualPos(match_constant_exponent(a1),
[P(a1, remove_power_of_one, ())])
self.assertEqualPos(match_constant_exponent(a2), [])
def test_remove_power_of_zero(self):
self.assertEqual(remove_power_of_zero(tree('a0'), ()), 1)
def test_remove_power_of_one(self):
a1 = tree('a1')
self.assertEqual(remove_power_of_one(a1, ()), a1[0])
from src.rules.sort import match_sort_multiplicants, move_constant
from src.node import Scope
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
class TestRulesSort(RulesTestCase):
def test_match_sort_multiplicants(self):
x, l2 = root = tree('x * 2')
possibilities = match_sort_multiplicants(root)
self.assertEqualPos(possibilities,
[P(root, move_constant, (Scope(root), l2, x))])
def test_move_constant(self):
x, l2 = root = tree('x * 2')
self.assertEqualNodes(move_constant(root, (Scope(root), l2, x)),
l2 * x)
import unittest import unittest
from src.rules.utils import least_common_multiple from src.rules.utils import least_common_multiple, is_fraction, partition
from tests.rulestestcase import tree
class TestRulesUtils(unittest.TestCase): class TestRulesUtils(unittest.TestCase):
...@@ -9,3 +10,15 @@ class TestRulesUtils(unittest.TestCase): ...@@ -9,3 +10,15 @@ class TestRulesUtils(unittest.TestCase):
self.assertEqual(least_common_multiple(5, 6), 30) self.assertEqual(least_common_multiple(5, 6), 30)
self.assertEqual(least_common_multiple(5, 6, 15), 30) self.assertEqual(least_common_multiple(5, 6, 15), 30)
self.assertEqual(least_common_multiple(2, 4), 4) self.assertEqual(least_common_multiple(2, 4), 4)
def test_is_fraction(self):
l1, a = tree('1, a')
self.assertTrue(is_fraction(a / 2, a, 2))
self.assertTrue(is_fraction(l1 / 2 * a, a, 2))
self.assertTrue(is_fraction(a * (l1 / 2), a, 2))
self.assertFalse(is_fraction(l1 / 3 * a, a, 2))
def test_partition(self):
self.assertEqual(partition(lambda x: x & 1, range(6)),
([1, 3, 5], [0, 2, 4]))
from unittest import TestCase
from src.validation import validate
class TestValidation(TestCase):
def test_simple_success(self):
self.assertTrue(validate('3a+a', '4a'))
def test_simple_failure(self):
self.assertFalse(validate('3a+a', '4a+1'))
def test_intermediate_success(self):
self.assertTrue(validate('3a+a+b+2b', '4a+3b'))
self.assertTrue(validate('a/b/(c/d)', 'ad/(bc)'))
def test_intermediate_failure(self):
self.assertFalse(validate('3a+a+b+2b', '4a+4b'))
#def test_advanced_failure(self):
# self.assertFalse(validate('(x-1)^3+(x-1)^3', '4a+4b'))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment