Commit 41047c7e authored by Taddeus Kroes's avatar Taddeus Kroes

Merge branch 'negated'

parents d419cb36 c050ce57
graph_drawing @ 821bdb8f
Subproject commit 84ad376b81ac72e163bacd7b538df16cac9be153
Subproject commit 821bdb8f8408fb36ee1d92ada162589794a8c5b3
......@@ -29,9 +29,21 @@ OP_MOD = 7
# N-ary (functions)
OP_INT = 8
OP_EXPAND = 9
OP_COMMA = 10
OP_SQRT = 11
OP_COMMA = 9
OP_SQRT = 10
# Goniometry
OP_SIN = 11
OP_COS = 12
OP_TAN = 13
OP_SOLVE = 14
OP_EQ = 15
OP_POSSIBILITIES = 16
OP_HINT = 17
OP_REWRITE_ALL = 18
OP_REWRITE = 19
TYPE_MAP = {
......@@ -41,19 +53,45 @@ TYPE_MAP = {
}
OP_MAP = {
',': OP_COMMA,
'+': OP_ADD,
# Either substraction or negation. Skip the operator sign in 'x' (= 2).
'-': OP_SUB,
'*': OP_MUL,
'/': OP_DIV,
'^': OP_POW,
'mod': OP_MOD,
'int': OP_INT,
'expand': OP_EXPAND,
'sin': OP_SIN,
'cos': OP_COS,
'tan': OP_TAN,
'sqrt': OP_SQRT,
',': OP_COMMA,
'int': OP_INT,
'solve': OP_SOLVE,
'=': OP_EQ,
'??': OP_POSSIBILITIES,
'?': OP_HINT,
'@@': OP_REWRITE_ALL,
'@': OP_REWRITE,
}
TOKEN_MAP = {
OP_COMMA: 'COMMA',
OP_ADD: 'PLUS',
OP_SUB: 'MINUS',
OP_MUL: 'TIMES',
OP_DIV: 'DIVIDE',
OP_POW: 'POW',
OP_SQRT: 'SQRT',
OP_SIN: 'SIN',
OP_COS: 'COS',
OP_TAN: 'TAN',
OP_INT: 'INT',
OP_SOLVE: 'SOLVE',
OP_EQ: 'EQ',
OP_POSSIBILITIES: 'POSSIBILITIES',
OP_HINT: 'HINT',
OP_REWRITE_ALL: 'REWRITE_ALL',
OP_REWRITE: 'REWRITE',
}
def to_expression(obj):
return obj if isinstance(obj, ExpressionBase) else ExpressionLeaf(obj)
......@@ -112,8 +150,11 @@ class ExpressionBase(object):
def is_op(self, op):
return not self.is_leaf and self.op == op
def is_power(self):
return not self.is_leaf and self.op == OP_POW
def is_power(self, exponent=None):
if self.is_leaf or self.op != OP_POW:
return False
return exponent == None or self[1] == exponent
def is_nary(self):
return not self.is_leaf and self.op in [OP_ADD, OP_SUB, OP_MUL]
......@@ -145,8 +186,13 @@ class ExpressionBase(object):
def __pow__(self, other):
return ExpressionNode('^', self, to_expression(other))
def __pos__(self):
return self.reduce_negation()
def reduce_negation(self, n=1):
"""Remove n negation flags from the node."""
assert self.negated
return self.negate(-n)
def negate(self, n=1):
......@@ -160,9 +206,6 @@ class ExpressionNode(Node, ExpressionBase):
self.type = TYPE_OPERATOR
self.op = OP_MAP[args[0]]
if hasattr(self.op, '__call__'):
self.op = self.op(args)
def __str__(self): # pragma: nocover
return generate_line(self)
......@@ -170,10 +213,8 @@ class ExpressionNode(Node, ExpressionBase):
"""
Check strict equivalence.
"""
if isinstance(other, ExpressionNode):
return self.op == other.op and self.nodes == other.nodes
return False
return isinstance(other, ExpressionNode) and self.op == other.op \
and self.negated == other.negated and self.nodes == other.nodes
def substitute(self, old_child, new_child):
self.nodes[self.nodes.index(old_child)] = new_child
......@@ -245,7 +286,7 @@ class ExpressionNode(Node, ExpressionBase):
return (self[0], self[1], ExpressionLeaf(1))
return (self[1], self[0], ExpressionLeaf(1))
def equals(self, other):
def equals(self, other, ignore_negation=False):
"""
Perform a non-strict equivalence check between two nodes:
- If the other node is a leaf, it cannot be equal to this node.
......@@ -256,18 +297,14 @@ class ExpressionNode(Node, ExpressionBase):
- If both nodes are divisions, the nominator and denominator have to be
non-strictly equal.
"""
if not other.is_op(self.op):
# FIXME: this is if-clause is a problem. To fix this problem
# permanently, normalize ("x * -1" -> "-1x") before comparing to
# the other node.
if not isinstance(other, ExpressionNode) or other.op != self.op:
return False
if self.op in (OP_ADD, OP_MUL):
s0 = Scope(self)
s1 = set(Scope(other))
# Scopes sould be of equal size
# Scopes should be of equal size
if len(s0) != len(s1):
return False
......@@ -291,8 +328,11 @@ class ExpressionNode(Node, ExpressionBase):
if not child.equals(other[i]):
return False
if ignore_negation:
return True
return self.negated == other.negated
class ExpressionLeaf(Leaf, ExpressionBase):
def __init__(self, *args, **kwargs):
......@@ -306,15 +346,32 @@ class ExpressionLeaf(Leaf, ExpressionBase):
other_type = type(other)
if other_type in TYPE_MAP:
return TYPE_MAP[other_type] == self.type and self.value == other
return TYPE_MAP[other_type] == self.type \
and self.actual_value() == other
return self.negated == other.negated and self.type == other.type \
and self.value == other.value
return other.type == self.type and self.value == other.value
def __repr__(self):
return '-' * self.negated + str(self.value)
def equals(self, other):
def equals(self, other, ignore_negation=False):
"""
Check non-strict equivalence.
Between leaves, this is the same as strict equivalence.
Between leaves, this is the same as strict equivalence, except when
negations must be ignored.
"""
if ignore_negation:
other_type = type(other)
if other_type in (int, float):
return TYPE_MAP[other_type] == self.type \
and self.value == abs(other)
elif other_type == str:
return self.type == TYPE_IDENTIFIER and self.value == other
return self.type == other.type and self.value == other.value
else:
return self == other
def extract_polynome_properties(self):
......@@ -350,7 +407,14 @@ class Scope(object):
def __iter__(self):
return iter(self.nodes)
def remove(self, node, replacement=None):
def __eq__(self, other):
return isinstance(other, Scope) and self.node == other.node \
and self.nodes == other.nodes
def __repr__(self):
return '<Scope of "%s">' % repr(self.node)
def remove(self, node, **kwargs):
if node.is_leaf:
node_cmp = hash(node)
else:
......@@ -363,8 +427,8 @@ class Scope(object):
n_cmp = n
if n_cmp == node_cmp:
if replacement != None:
self[i] = replacement
if 'replacement' in kwargs:
self[i] = kwargs['replacement']
else:
del self.nodes[i]
......@@ -373,8 +437,11 @@ class Scope(object):
raise ValueError('Node "%s" is not in the scope of "%s".'
% (node, self.node))
def replace(self, node, replacement):
self.remove(node, replacement=replacement)
def as_nary_node(self):
return nary_node(self.node.value, self.nodes)
return nary_node(self.node.value, self.nodes).negate(self.node.negated)
def nary_node(operator, scope):
......@@ -405,7 +472,9 @@ def get_scope(node):
def negate(node, n=1):
"""Negate the given node n times."""
node = node.clone()
node.negated = n
assert n >= 0
new_node = node.clone()
new_node.negated = n
return node
return new_node
......@@ -3,8 +3,6 @@ This parser will parse the given input and build an expression tree. Grammar
file for the supported mathematical expressions.
"""
from node import ExpressionNode as Node, ExpressionLeaf as Leaf
import os.path
PYBISON_BUILD = os.path.realpath('build/external/pybison')
EXTERNAL_MODS = os.path.realpath('external')
......@@ -16,7 +14,8 @@ sys.path.insert(1, EXTERNAL_MODS)
from pybison import BisonParser, BisonSyntaxError
from graph_drawing.graph import generate_graph
from node import TYPE_OPERATOR, OP_COMMA, OP_NEG
from node import ExpressionNode as Node, ExpressionLeaf as Leaf, OP_MAP, \
TOKEN_MAP, TYPE_OPERATOR, OP_COMMA, OP_NEG, OP_MUL, Scope
from rules import RULES
from possibilities import filter_duplicates, pick_suggestion, apply_suggestion
......@@ -52,10 +51,8 @@ class Parser(BisonParser):
# ----------------------------------------------------------------
# TODO: add a runtime check to verify that this token list match the list
# of tokens of the lex script.
tokens = ['NUMBER', 'IDENTIFIER', 'POSSIBILITIES',
'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'POW',
'LPAREN', 'RPAREN', 'COMMA', 'HINT', 'REWRITE',
'NEWLINE', 'QUIT', 'RAISE', 'GRAPH', 'SQRT']
tokens = ['NUMBER', 'IDENTIFIER', 'NEWLINE', 'QUIT', 'RAISE', 'GRAPH', \
'LPAREN', 'RPAREN'] + TOKEN_MAP.values()
# ------------------------------
# precedences
......@@ -74,13 +71,20 @@ class Parser(BisonParser):
BisonParser.__init__(self, **kwargs)
self.interactive = kwargs.get('interactive', 0)
self.timeout = kwargs.get('timeout', 0)
self.possibilities = self.last_possibilities = []
self.reset()
def reset(self):
self.read_buffer = ''
self.read_queue = Queue.Queue()
self.subtree_map = {}
#self.subtree_map = {}
self.root_node = None
self.possibilities = self.last_possibilities = []
def run(self, *args, **kwargs):
self.reset()
return super(Parser, self).run(*args, **kwargs)
# Override default read method with a version that prompts for input.
def read(self, nbytes):
......@@ -186,32 +190,16 @@ class Parser(BisonParser):
if not retval.negated and retval.type != TYPE_OPERATOR:
return retval
if self.subtree_map and retval.type == TYPE_OPERATOR:
# Update the subtree map to let the subtree point to its parent
# node.
parent_nodes = self.subtree_map.keys()
for child in retval:
if child in parent_nodes:
self.subtree_map[child] = retval
if retval.type == TYPE_OPERATOR and retval.op in RULES:
handlers = RULES[retval.op]
else:
handlers = []
if retval.negated:
handlers += RULES[OP_NEG]
handlers = RULES[OP_NEG]
for handler in handlers:
possibilities = handler(retval)
# Record the subtree root node in order to avoid tree traversal.
# At this moment, the node is the root node since the expression is
# parser using the left-innermost parsing strategy.
for p in possibilities:
self.subtree_map[p.root] = None
self.possibilities.extend(possibilities)
return retval
......@@ -231,8 +219,7 @@ class Parser(BisonParser):
if not suggestion:
return self.root_node
expression = apply_suggestion(self.root_node, self.subtree_map,
suggestion)
expression = apply_suggestion(self.root_node, suggestion)
if self.verbose:
print 'After application, expression=', expression
......@@ -350,9 +337,14 @@ class Parser(BisonParser):
"""
if option == 0: # rule: NEG exp
node = values[1]
node.negated += 1
return node
# Add negation to the left-most child
if values[1].is_leaf or values[1].op != OP_MUL:
values[1].negated += 1
else:
child = Scope(values[1])[0]
child.negated += 1
return values[1]
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
......@@ -371,8 +363,18 @@ class Parser(BisonParser):
if option == 4: # rule: exp MINUS exp
node = values[2]
# Add negation to the left-most child
if node.is_leaf or node.op != OP_MUL:
node.negated += 1
else:
node = Scope(node)[0]
node.negated += 1
return Node('+', values[0], node)
# Explicit call the hook handler on the created unary negation.
node = self.hook_handler('binary', 4, names, values, node)
return Node('+', values[0], values[2])
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
......@@ -388,6 +390,15 @@ class Parser(BisonParser):
raise BisonSyntaxError('Unsupported option %d in target "%s".'
% (option, target)) # pragma: nocover
# -----------------------------------------
# operator tokens
# -----------------------------------------
operators = ''
for op_str, op in OP_MAP.iteritems():
operators += '"%s"%s{ returntoken(%s); }\n' \
% (op_str, ' ' * (8 - len(op_str)), TOKEN_MAP[op])
# -----------------------------------------
# raw lex script, verbatim here
# -----------------------------------------
......@@ -415,8 +426,6 @@ class Parser(BisonParser):
yylloc.first_column = yycolumn; \
yylloc.last_column = yycolumn + yyleng; \
yycolumn += yyleng;
/*[a-zA-Z][0-9]+ { returntoken(CONCAT_POW); }*/
%}
%option yylineno
......@@ -427,19 +436,11 @@ class Parser(BisonParser):
[a-zA-Z] { returntoken(IDENTIFIER); }
"(" { returntoken(LPAREN); }
")" { returntoken(RPAREN); }
"+" { returntoken(PLUS); }
"-" { returntoken(MINUS); }
"*" { returntoken(TIMES); }
"^" { returntoken(POW); }
"/" { returntoken(DIVIDE); }
"," { returntoken(COMMA); }
"??" { returntoken(POSSIBILITIES); }
"?" { returntoken(HINT); }
"@" { returntoken(REWRITE); }
"quit" { yyterminate(); returntoken(QUIT); }
""" + operators + r"""
"raise" { returntoken(RAISE); }
"graph" { returntoken(GRAPH); }
"sqrt" { returntoken(SQRT); }
"quit" { yyterminate(); returntoken(QUIT); }
[ \t\v\f] { }
[\n] { yycolumn = 0; returntoken(NEWLINE); }
......
from node import TYPE_OPERATOR
# 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
# corresponding value is a string, which will be used to produce the hint
......@@ -60,7 +63,27 @@ def pick_suggestion(possibilities):
return possibilities[suggestion]
def apply_suggestion(root, subtree_map, suggestion):
def find_parent_node(root, child):
nodes = [root]
while nodes:
node = nodes.pop()
while node:
if node.type != TYPE_OPERATOR:
break
if child in node:
return node
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
# node, the subtree_map cannot be used since the hash() of each node in the
# deep copied root node has changed.
......@@ -68,10 +91,7 @@ def apply_suggestion(root, subtree_map, suggestion):
subtree = suggestion.handler(suggestion.root, suggestion.args)
if suggestion.root in subtree_map:
parent_node = subtree_map[suggestion.root]
else:
parent_node = None
parent_node = find_parent_node(root, suggestion.root)
# 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
......@@ -85,4 +105,5 @@ def apply_suggestion(root, subtree_map, suggestion):
if parent_node:
parent_node.substitute(suggestion.root, subtree)
return root
return subtree
from ..node import OP_ADD, OP_MUL, OP_DIV, OP_POW, OP_NEG
from .poly import match_combine_polynomes
from .groups import match_combine_groups
from .factors import match_expand
from .powers import match_add_exponents, match_subtract_exponents, \
match_multiply_exponents, match_duplicate_exponent, \
match_remove_negative_exponent, match_exponent_to_root, \
match_extend_exponent
from .numerics import match_divide_numerics, match_multiply_numerics, \
match_multiply_zero
match_extend_exponent, match_constant_exponent
from .numerics import match_add_numerics, match_divide_numerics, \
match_multiply_numerics, match_multiply_zero, match_multiply_one
from .fractions import match_constant_division, match_add_constant_fractions, \
match_expand_and_add_fractions
from .negation import match_negate_group, match_negated_division
from .negation import match_negated_factor, match_negate_polynome, \
match_negated_division
RULES = {
OP_ADD: [match_add_constant_fractions, match_combine_polynomes, \
OP_ADD: [match_add_numerics, match_add_constant_fractions,
match_combine_groups],
OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents, \
match_expand_and_add_fractions, match_multiply_zero],
OP_DIV: [match_subtract_exponents, match_divide_numerics, \
OP_MUL: [match_multiply_numerics, match_expand, match_add_exponents,
match_expand_and_add_fractions, match_multiply_zero,
match_negated_factor, match_multiply_one],
OP_DIV: [match_subtract_exponents, match_divide_numerics,
match_constant_division, match_negated_division],
OP_POW: [match_multiply_exponents, match_duplicate_exponent, \
match_remove_negative_exponent, match_exponent_to_root, \
match_extend_exponent],
OP_NEG: [match_negate_group],
OP_POW: [match_multiply_exponents, match_duplicate_exponent,
match_remove_negative_exponent, match_exponent_to_root,
match_extend_exponent, match_constant_exponent],
OP_NEG: [match_negate_polynome],
}
......@@ -21,6 +21,10 @@ def match_expand(node):
if n.is_leaf:
leaves.append(n)
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)
for args in product(leaves, additions):
......@@ -45,7 +49,7 @@ def expand_single(root, args):
scope = Scope(root)
# Replace 'a' with the new expression
scope.remove(a, a * b + a * c)
scope.replace(a, a * b + a * c)
# Remove the addition
scope.remove(bc)
......@@ -66,7 +70,7 @@ def expand_double(root, args):
scope = Scope(root)
# 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
scope.remove(cd)
......
from itertools import combinations
from .utils import least_common_multiple
from ..node import ExpressionLeaf as L, Scope, OP_DIV, OP_ADD, OP_MUL
from ..node import ExpressionLeaf as L, Scope, negate, OP_DIV, OP_ADD, OP_MUL
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
......@@ -112,9 +112,14 @@ def equalize_denominators(root, args):
mult = denom / d.value
if mult != 1:
n = L(n.value * mult) if n.is_numeric() else L(mult) * n
if n.is_numeric():
n = L(n.value * mult)
else:
n = L(mult) * n
scope.remove(fraction, negate(n / L(d.value * mult),
#n = L(n.value * mult) if n.is_numeric() else L(mult) * n
scope.replace(fraction, negate(n / L(d.value * mult),
fraction.negated))
return scope.as_nary_node()
......@@ -137,7 +142,7 @@ def add_nominators(root, args):
scope = Scope(root)
# Replace the left node with the new expression
scope.remove(ab, (a + negate(cb[0], cb.negated)) / b)
scope.replace(ab, (a + cb[0].negate(cb.negated)) / b)
# Remove the right node
scope.remove(cb)
......
from ..node import ExpressionNode as N, ExpressionLeaf as L, Scope, \
OP_ADD, OP_POW, OP_MUL, OP_SIN, OP_COS, OP_TAN
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def match_add_quadrants(node):
"""
sin(x) ^ 2 + cos(x) ^ 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(x) ^ 2 + cos(x) ^ 2 -> 1
"""
return L(1)
MESSAGES[add_quadrants] = _('Add the sinus and cosinus quadrants to 1.')
from itertools import combinations
from ..node import ExpressionNode as Node, ExpressionLeaf as Leaf, Scope, \
OP_ADD, OP_MUL
OP_ADD, OP_MUL, nary_node, negate
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
......@@ -22,44 +22,49 @@ def match_combine_groups(node):
p = []
groups = []
scope = Scope(node)
for n in Scope(node):
groups.append((1, n, n))
for n in scope:
if not n.is_numeric():
groups.append((Leaf(1), n, n))
# Each number multiplication yields a group, multiple occurences of
# the same group can be replaced by a single one
if n.is_op(OP_MUL):
scope = Scope(n)
l = len(scope)
n_scope = Scope(n)
l = len(n_scope)
for i, sub_node in enumerate(scope):
for i, sub_node in enumerate(n_scope):
if sub_node.is_numeric():
others = [scope[j] for j in range(i) + range(i + 1, l)]
others = [n_scope[j] for j in range(i) + range(i + 1, l)]
if len(others) == 1:
g = others[0]
else:
g = Node('*', *others)
g = nary_node('*', others)
groups.append((sub_node, g, n))
for g0, g1 in combinations(groups, 2):
if g0[1].equals(g1[1]):
p.append(P(node, combine_groups, g0 + g1))
for (c0, g0, n0), (c1, g1, n1) in combinations(groups, 2):
if g0.equals(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
def combine_groups(root, args):
c0, g0, n0, c1, g1, n1 = args
scope = Scope(root)
if not isinstance(c0, Leaf) and not isinstance(c0, Node):
c0 = Leaf(c0)
scope, c0, g0, n0, c1, g1, n1 = args
# Replace the left node with the new expression
scope.remove(n0, (c0 + c1) * g0)
scope.replace(n0, (c0 + c1) * g0)
# Remove the right node
scope.remove(n1)
......@@ -68,4 +73,4 @@ def combine_groups(root, args):
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_ADD, OP_MUL, OP_DIV
from ..node import Scope, OP_ADD, OP_MUL, OP_DIV
from ..possibilities import Possibility as P, MESSAGES
from ..translate import _
def match_negate_group(node):
def match_negated_factor(node):
"""
--a -> a
-(a * ... * -b) -> ab
-(a + b + ... + z) -> -a + -b + ... + -z
This rule assures that negations in the scope of a multiplication are
brought to the most left node in the multiplication's scope.
Example:
a * -b -> -(ab)
"""
assert node.negated
assert node.is_op(OP_MUL)
if node.negated == 2:
# --a
return [P(node, double_negation, (node,))]
p = []
scope = Scope(node)
if not node.is_leaf:
scope = get_scope(node)
# FIXME: The negation that is brought outside is assigned to the first
# element in the scope during the next parsing step:
# -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 node.is_op(OP_MUL) and any(map(lambda n: n.negated, scope)):
# -(-a)b
return [P(node, negate_group, (node, scope))]
return p
if node.is_op(OP_ADD):
# -(ab + c) -> -ab - c
# -(-ab + c) -> ab - c
return [P(node, negate_polynome, (node, scope))]
return []
def negated_factor(root, args):
"""
a * -b -> -ab
"""
scope, factor = args
scope[0] = -scope[0]
scope.replace(factor, +factor)
return scope.as_nary_node()
def negate_group(root, args):
MESSAGES[negated_factor] = \
_('Bring negation of {2} to the outside of the multiplication.')
def match_negate_polynome(node):
"""
-(a * -3c) -> a * 3c
-(a * ... * -b) -> ab
--a -> a
-(a + b) -> -a - b
"""
node, scope = args
#print 'match_negate_polynome:', node, node.negated
assert node.negated, str(node.negated) + '; ' + str(node)
for i, n in enumerate(scope):
if n.negated:
scope[i] = n.reduce_negation()
p = []
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, ()))
return p
return nary_node('*', scope).reduce_negation()
def double_negation(root, args):
"""
--a -> a
"""
return root.reduce_negation(2)
MESSAGES[negate_group] = _('Apply negation to the polynome {1[0]}.')
MESSAGES[double_negation] = _('Remove double negation in {0}.')
def negate_polynome(root, args):
"""
-(-ab + ... + c) -> --ab + ... + -c
-(a + b) -> -a - b
"""
node, scope = args
scope = Scope(root)
# Negate each group
for i, n in enumerate(scope):
scope[i] = -n
return nary_node('+', scope)
return +scope.as_nary_node()
MESSAGES[negate_polynome] = _('Apply negation to the subexpression {1[0]}.')
def double_negation(root, args):
"""
--a -> a
"""
return negate(args[0], args[0].negated - 2)
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):
......@@ -82,11 +114,11 @@ def match_negated_division(node):
a, b = node
if a.negated and b.negated:
return [P(node, double_negated_division, (node,))]
return [P(node, double_negated_division, ())]
elif a.negated:
return [P(node, single_negated_division, (a[0], b))]
return [P(node, single_negated_division, (+a, b))]
elif b.negated:
return [P(node, single_negated_division, (a, b[0]))]
return [P(node, single_negated_division, (a, +b))]
return []
......@@ -113,11 +145,11 @@ def double_negated_division(root, args):
"""
a, b = root
return a[0] / b[0]
return +a / +b
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 ..node import ExpressionLeaf as Leaf, Scope, negate, OP_DIV, OP_MUL
from ..node import ExpressionLeaf as Leaf, Scope, negate, OP_ADD, OP_DIV, \
OP_MUL
from ..possibilities import Possibility as P, MESSAGES
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.
......@@ -15,19 +16,43 @@ def add_numerics(root, args):
-2 + 3 -> 1
-2 + -3 -> -5
"""
n0, n1, c0, c1 = args
scope = Scope(root)
assert node.is_op(OP_ADD)
p = []
scope = Scope(node)
numerics = filter(lambda n: n.is_numeric(), scope)
for c0, c1 in combinations(numerics, 2):
p.append(P(node, add_numerics, (scope, c0, c1)))
return p
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()
if value < 0:
leaf = Leaf(-value).negate()
else:
leaf = Leaf(value)
# Replace the left node with the new expression
scope.remove(n0, Leaf(c0.actual_value() + c1.actual_value()))
scope.replace(c0, Leaf(abs(value)).negate(int(value < 0)))
# Remove the right node
scope.remove(n1)
scope.remove(c1)
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):
......@@ -130,6 +155,42 @@ def multiply_zero(root, args):
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):
"""
3 * 2 -> 6
......@@ -140,14 +201,11 @@ def match_multiply_numerics(node):
assert node.is_op(OP_MUL)
p = []
numerics = []
for n in Scope(node):
if n.is_numeric():
numerics.append((n, n.actual_value()))
scope = Scope(node)
numerics = filter(lambda n: n.is_numeric(), scope)
for (n0, v0), (n1, v1) in combinations(numerics, 2):
p.append(P(node, multiply_numerics, (n0, n1, v0, v1)))
for c0, c1 in combinations(numerics, 2):
p.append(P(node, multiply_numerics, (scope, c0, c1)))
return p
......@@ -159,24 +217,17 @@ def multiply_numerics(root, args):
Example:
2 * 3 -> 6
"""
n0, n1, v0, v1 = args
scope = []
value = v0 * v1
if value > 0:
substitution = Leaf(value)
else:
substitution = -Leaf(-value)
scope, c0, c1 = args
scope = Scope(root)
# 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
scope.remove(n1)
scope.remove(c1)
return scope.as_nary_node()
MESSAGES[multiply_numerics] = _('Multiply constant {1} with {2}.')
MESSAGES[multiply_numerics] = _('Multiply constant {2} with {3}.')
from itertools import combinations
from ..node import Scope, OP_ADD
from ..possibilities import Possibility as P, MESSAGES
from .numerics import add_numerics
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(lambda n: n.is_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()
......@@ -17,8 +17,9 @@ def match_add_exponents(node):
p = []
powers = {}
scope = Scope(node)
for n in Scope(node):
for n in scope:
if n.is_identifier():
s = n
exponent = L(1)
......@@ -26,7 +27,7 @@ def match_add_exponents(node):
# 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
else:
else: # pragma: nocover
continue
s_str = str(s)
......@@ -41,7 +42,7 @@ def match_add_exponents(node):
# create a single power with that root
if len(occurrences) > 1:
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
......@@ -50,11 +51,10 @@ def add_exponents(root, args):
"""
a^p * a^q -> a^(p + q)
"""
n0, n1, a, p, q = args
scope = Scope(root)
scope, n0, n1, a, p, q = args
# Replace the left node with the new expression
scope.remove(n0, a ** (p + q))
scope.replace(n0, a ** (p + q))
# Remove the right node
scope.remove(n1)
......@@ -62,7 +62,7 @@ def add_exponents(root, args):
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):
......@@ -162,7 +162,7 @@ MESSAGES[duplicate_exponent] = _('Duplicate the exponent {2}.')
def match_remove_negative_exponent(node):
"""
a^-p -> 1 / a^p
a ^ -p -> 1 / a ^ p
"""
assert node.is_op(OP_POW)
......@@ -237,3 +237,40 @@ def extend_exponent(root, args):
return left * left ** L(right.value - 1)
return left * left
def match_constant_exponent(node):
"""
(a + ... + z)^n -> (a + ... + z)(a + ... + z)^(n - 1) # n > 1
"""
assert node.is_op(OP_POW)
exponent = node[1]
if exponent == 0:
return [P(node, remove_power_of_zero, ())]
if exponent == 1:
return [P(node, remove_power_of_one, ())]
return []
def remove_power_of_zero(root, args):
"""
a ^ 0 -> 1
"""
return L(1)
MESSAGES[remove_power_of_zero] = _('Power of zero {0} rewrites to 1.')
def remove_power_of_one(root, args):
"""
a ^ 1 -> a
"""
return root[0]
MESSAGES[remove_power_of_one] = _('Remove the power of one in {0}.')
......@@ -44,16 +44,26 @@ class RulesTestCase(unittest.TestCase):
try:
for i, exp in enumerate(rewrite_chain[:-1]):
self.assertMultiLineEqual(str(rewrite(exp)),
str(rewrite_chain[i+1]))
except AssertionError: # pragma: nocover
print 'rewrite failed: "%s" -> "%s"' \
% (str(exp), str(rewrite_chain[i+1]))
print 'rewrite chain index: %d' % i
print 'rewrite chain: ---'
str(rewrite_chain[i + 1]))
except AssertionError as e: # pragma: nocover
msg = e.args[0]
for i, c in enumerate(rewrite_chain):
print '%2d %s' % (i, str(c))
msg += '-' * 30 + '\n'
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
......@@ -11,13 +11,13 @@ class TestB1Ch08(unittest.TestCase):
run_expressions(Parser, [
('6*5^2', L(6) * L(5) ** 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))),
('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):
apply_expressions(Parser, [
('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):
def test_diagnostic_test(self):
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')),
('18-(a-12)', L(18) + -(L('a') + -L(12))),
('-p-q+5(p-q)-3q-2(p-q)',
-L('p') + -L('q') + L(5) * (L('p') + -L('q')) + -(L(3) * 'q') \
+ - (L(2) * (L('p') + -L('q')))
-L('p') + -L('q') + L(5) * (L('p') + -L('q')) + (-L(3) * 'q') \
+ (-L(2) * (L('p') + -L('q')))
),
('(2+3/7)^4',
N('^', N('+', L(2), N('/', L(3), L(7))), L(4))
......
......@@ -7,9 +7,4 @@ from tests.parser import ParserWrapper
class TestException(unittest.TestCase):
def test_raise(self):
try:
ParserWrapper(Parser).run(['raise'])
except RuntimeError:
return
raise AssertionError('Expected raised RuntimeError!') # pragma: nocover
self.assertRaises(RuntimeError, ParserWrapper(Parser).run, ['raise'])
......@@ -30,6 +30,8 @@ class TestLeidenOefenopgave(TestCase):
'(xx + x * 1 + 1x + 1 * 1)(x + 1)',
'(x ^ (1 + 1) + x * 1 + 1x + 1 * 1)(x + 1)',
'(x ^ 2 + x * 1 + 1x + 1 * 1)(x + 1)',
'(x ^ 2 + x + 1x + 1 * 1)(x + 1)',
'(x ^ 2 + x + x + 1 * 1)(x + 1)',
'(x ^ 2 + (1 + 1)x + 1 * 1)(x + 1)',
'(x ^ 2 + 2x + 1 * 1)(x + 1)',
'(x ^ 2 + 2x + 1)(x + 1)',
......@@ -40,9 +42,11 @@ class TestLeidenOefenopgave(TestCase):
'x ^ 3 + x ^ (1 + 1) * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + (x ^ 2 + 2x) * 1 + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + 1 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + x ^ 2 * 2 + x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + (2 + 1) * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 1 * 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 2x + 1x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 2x + x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + (2 + 1)x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 3x + 1 * 1',
'x ^ 3 + 3 * x ^ 2 + 3x + 1',
......@@ -56,6 +60,8 @@ class TestLeidenOefenopgave(TestCase):
'xx + x * 1 + 1x + 1 * 1',
'x ^ (1 + 1) + x * 1 + 1x + 1 * 1',
'x ^ 2 + x * 1 + 1x + 1 * 1',
'x ^ 2 + x + 1x + 1 * 1',
'x ^ 2 + x + x + 1 * 1',
'x ^ 2 + (1 + 1)x + 1 * 1',
'x ^ 2 + 2x + 1 * 1',
'x ^ 2 + 2x + 1'],
......@@ -68,23 +74,44 @@ class TestLeidenOefenopgave(TestCase):
'xx + x * -1 - 1x - 1 * -1',
'x ^ (1 + 1) + x * -1 - 1x - 1 * -1',
'x ^ 2 + x * -1 - 1x - 1 * -1',
# FIXME: 'x ^ 2 + (-1 - 1)x - 1 * -1',
# FIXME: 'x ^ 2 - 2x - 1 * -1',
# FIXME: 'x ^ 2 - 2x - -1',
# FIXME: 'x ^ 2 - 2x + 1',
'x ^ 2 - x * 1 - 1x - 1 * -1',
'x ^ 2 - x - 1x - 1 * -1',
'x ^ 2 - x - x - 1 * -1',
'x ^ 2 + (1 + 1) * -x - 1 * -1',
'x ^ 2 + 2 * -x - 1 * -1',
'x ^ 2 - 2x - 1 * -1',
'x ^ 2 - 2x - -1',
'x ^ 2 - 2x + 1',
]]:
self.assertRewrite(chain)
def test_1_4_1(self):
self.assertRewrite(['x * -1 + 1x', '(-1 + 1)x', '0x',]) # FIXME: '0'])
self.assertRewrite(['x * -1 + 1x',
'-x * 1 + 1x',
'-x + 1x',
'-x + x',
'(-1 + 1)x',
'0x',
'0'])
def test_1_4_2(self):
# FIXME: self.assertRewrite(['x * -1 - 1x', '(-1 + -1)x', '-2x'])
pass
self.assertRewrite(['x * -1 - 1x',
'-x * 1 - 1x',
'-x - 1x',
'-x - x',
'(1 + 1) * -x',
'2 * -x',
'-2x'])
def test_1_4_3(self):
# FIXME: self.assertRewrite(['x * -1 + x * -1', '(-1 + -1)x', '-2x'])
pass
self.assertRewrite(['x * -1 + x * -1',
'-x * 1 + x * -1',
'-x + x * -1',
'-x - x * 1',
'-x - x',
'(1 + 1) * -x',
'2 * -x',
'-2x'])
def test_1_5(self):
self.assertRewrite(['(2x + x)x', '(2 + 1)xx', '3xx',
......@@ -113,18 +140,25 @@ class TestLeidenOefenopgave(TestCase):
def test_3(self):
pass
def test_4(self):
for exp, solution in [
('2/15 + 1/4', '8 / 60 + 15 / 60'),
('8/60 + 15/60', '(8 + 15) / 60'),
('(8 + 15) / 60', '23 / 60'),
('2/7 - 4/11', '22 / 77 - 28 / 77'),
('22/77 - 28/77', '(22 - 28) / 77'),
('(22 - 28)/77', '-6 / 77'),
# FIXME: ('(7/3) * (3/5)', '7 / 5'),
# FIXME: ('(3/4) / (5/6)', '9 / 10'),
# FIXME: ('1/4 * 1/x', '1 / (4x)'),
# FIXME: ('(3/x^2) / (x/7)', '21 / x^3'),
# FIXME: ('1/x + 2/(x+1)', '(3x + 1) / (x * (x + 1))'),
]:
self.assertEqual(str(rewrite(exp)), solution)
def test_4_1(self):
self.assertRewrite(['2/15 + 1/4', '8 / 60 + 15 / 60', '(8 + 15) / 60',
'23 / 60'])
def test_4_2(self):
self.assertRewrite(['2/7 - 4/11', '22 / 77 - 28 / 77',
'(22 - 28) / 77', '-6 / 77'])
#def test_4_3(self):
# self.assertRewrite(['(7/3) * (3/5)', '7 / 5'])
#def test_4_4(self):
# self.assertRewrite(['(3/4) / (5/6)', '9 / 10'])
#def test_4_5(self):
# self.assertRewrite(['1/4 * 1/x', '1 / (4x)'])
#def test_4_6(self):
# self.assertRewrite(['(3/x^2) / (x/7)', '21 / x^3'])
#def test_4_7(self):
# self.assertRewrite(['1/x + 2/(x+1)', '(3x + 1) / (x * (x + 1))'])
from tests.rulestestcase import RulesTestCase as TestCase, rewrite
class TestLeidenOefenopgaveV12(TestCase):
def test_1_e(self):
self.assertRewrite([
'-2(6x - 4) ^ 2 * x',
'-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',
'(-72 * x ^ (1 + 1) - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 - 12x * -4 + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 - -48x + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 + 48x + 8 * 6x + 8 * -4)x',
'(-72 * x ^ 2 + 48x + 48x + 8 * -4)x',
'(-72 * x ^ 2 + (1 + 1) * 48x + 8 * -4)x',
'(-72 * x ^ 2 + 2 * 48x + 8 * -4)x',
'(-72 * x ^ 2 + 96x + 8 * -4)x',
'(-72 * x ^ 2 + 96x - 32)x',
'x(-72 * x ^ 2 + 96x) + x * -32',
'x * -72 * x ^ 2 + x * 96x + x * -32',
'-x * 72 * x ^ 2 + x * 96x + x * -32',
'-x * 72 * x ^ 2 + x ^ (1 + 1) * 96 + x * -32',
'-x * 72 * x ^ 2 + x ^ 2 * 96 + x * -32',
'-x * 72 * x ^ 2 + x ^ 2 * 96 - x * 32'])
# FIXME: '-x ^ (1 + 2) * 72 + x ^ 2 * 96 - x * 32',
# FIXME: '-x ^ 3 * 72 + x ^ 2 * 96 - x * 32',
# FIXME: '-72x ^ 3 + x ^ 2 * 96 - x * 32',
# FIXME: '-72x ^ 3 + 96x ^ 2 - x * 32',
# FIXME: '-72x ^ 3 + 96x ^ 2 - 32x'])
......@@ -30,25 +30,17 @@ class TestNode(RulesTestCase):
self.assertTrue(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):
self.assertTrue(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):
self.assertTrue(N('^', *self.l[:2]).is_power())
self.assertFalse(N('+', *self.l[:2]).is_power())
self.assertTrue(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):
self.assertTrue(N('+', *self.l[:2]).is_nary())
......@@ -173,6 +165,13 @@ class TestNode(RulesTestCase):
m0, m1 = tree('-5 * -3,-5 * 6')
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):
self.assertEqual(self.scope.node, self.n)
self.assertEqual(self.scope.nodes, [self.a, self.b, self.cd])
......@@ -185,14 +184,14 @@ class TestNode(RulesTestCase):
self.scope.remove(self.cd)
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):
with self.assertRaises(ValueError):
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):
a, b, c, d = tree('a,b,c,d')
......@@ -205,3 +204,8 @@ class TestNode(RulesTestCase):
def test_scope_as_nary_node(self):
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)
......@@ -23,3 +23,15 @@ class TestParser(unittest.TestCase):
def test_line(self):
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)
......@@ -45,7 +45,7 @@ class TestPossibilities(unittest.TestCase):
possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="3 + 4" handler=add_numerics' \
' args=(3, 4, 3, 4)>')
' args=(<Scope of "3 + 4">, 3, 4)>')
def test_multiple_runs(self):
parser = ParserWrapper(Parser)
......@@ -53,21 +53,19 @@ class TestPossibilities(unittest.TestCase):
possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<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(['', ' '])
possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="1 + 2" handler=add_numerics' \
' args=(1, 2, 1, 2)>')
self.assertEqual(possibilities, [])
# Overwrite previous possibilities with new ones
parser.run(['3+4'])
possibilities = parser.parser.possibilities
self.assertEqual('\n'.join([repr(pos) for pos in possibilities]),
'<Possibility root="3 + 4" handler=add_numerics' \
' args=(3, 4, 3, 4)>')
' args=(<Scope of "3 + 4">, 3, 4)>')
def test_filter_duplicates(self):
a, b = ab = tree('a + b')
......
from src.rules.goniometry import match_add_quadrants, add_quadrants
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
class TestRulesGoniometry(RulesTestCase):
def test_match_add_quadrants(self):
return
root = tree('sin(x) ^ 2 + cos(x) ^ 2')
possibilities = match_add_quadrants(root)
self.assertEqualPos(possibilities, [P(root, add_quadrants, ())])
def test_add_quadrants(self):
return
root = tree('sin(x) ^ 2 + cos(x) ^ 2')
self.assertEqual(add_quadrants(root, ()), 1)
from src.rules.groups import match_combine_groups, combine_groups
from src.node import Scope
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
......@@ -6,64 +7,88 @@ from tests.rulestestcase import RulesTestCase, tree
class TestRulesGroups(RulesTestCase):
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)
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):
a0, mul = root = tree('a + 2a')
root, l1 = tree('a + 2a,1')
a0, mul = root
l2, a1 = mul
possibilities = match_combine_groups(root)
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):
((l2, a0), b), (l3, a1) = (m0, b), m1 = root = tree('2a + b + 3a')
possibilities = match_combine_groups(root)
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):
((l2, a0), (l3, a1)), (l4, a2) = (m0, m1), m2 = root = tree('2a+3a+4a')
possibilities = match_combine_groups(root)
self.assertEqualPos(possibilities,
[P(root, combine_groups, (l2, a0, m0, l3, a1, m1)),
P(root, combine_groups, (l2, a0, m0, l4, a2, m2)),
P(root, combine_groups, (l3, a1, m1, l4, a2, m2))])
[P(root, combine_groups, (Scope(root), l2, a0, m0,
l3, a1, m1)),
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):
ab0, ab1 = root = tree('ab + ab')
root, l1 = tree('ab + ab,1')
ab0, ab1 = root
possibilities = match_combine_groups(root)
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):
m0, m1 = root = tree('ab + 2ab')
root, l1 = tree('ab + 2ab,1')
m0, m1 = root
(l2, a), b = m1
possibilities = match_combine_groups(root)
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):
m0, m1 = root = tree('ab + ba')
root, l1 = tree('ab + ba,1')
m0, m1 = root
b, a = m1
possibilities = match_combine_groups(root)
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):
root, l1 = tree('a + a,1')
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)
def test_combine_groups_nary(self):
......@@ -71,5 +96,6 @@ class TestRulesGroups(RulesTestCase):
abb, ba = root
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)
from src.rules.negation import match_negated_division, \
single_negated_division, double_negated_division
from src.rules.negation import match_negated_factor, negated_factor, \
match_negate_polynome, negate_polynome, double_negation, \
match_negated_division, single_negated_division, \
double_negated_division
from src.node import Scope
from src.possibilities import Possibility as P
from tests.rulestestcase import RulesTestCase, tree
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):
self.assertEqual(match_negated_division(tree('1 / 2')), [])
......@@ -14,31 +58,31 @@ class TestRulesNegation(RulesTestCase):
l1, l2 = root = tree('-1 / 2')
possibilities = match_negated_division(root)
self.assertEqualPos(possibilities,
[P(root, single_negated_division, (l1[0], l2))])
[P(root, single_negated_division, (+l1, l2))])
l1, l2 = root = tree('1 / -2')
possibilities = match_negated_division(root)
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):
root = tree('-1 / -2')
possibilities = match_negated_division(root)
self.assertEqualPos(possibilities,
[P(root, double_negated_division, (root,))])
[P(root, double_negated_division, ())])
def test_single_negated_division(self):
l1, l2 = root = tree('-1 / 2')
self.assertEqualNodes(single_negated_division(root, (l1[0], l2)),
-(l1[0] / l2))
self.assertEqualNodes(single_negated_division(root, (+l1, l2)),
-(+l1 / l2))
l1, l2 = root = tree('1 / -2')
self.assertEqualNodes(single_negated_division(root, (l1, l2[0])),
-(l1 / l2[0]))
self.assertEqualNodes(single_negated_division(root, (l1, +l2)),
-(l1 / +l2))
def test_double_negated_division(self):
l1, l2 = root = tree('-1 / -2')
self.assertEqualNodes(double_negated_division(root, (root,)),
l1[0] / l2[0])
self.assertEqualNodes(double_negated_division(root, ()),
+l1 / +l2)
from src.rules.numerics import add_numerics, match_divide_numerics, \
divide_numerics, match_multiply_numerics, multiply_numerics
from src.rules.numerics import match_add_numerics, add_numerics, \
match_divide_numerics, divide_numerics, match_multiply_numerics, \
multiply_numerics
from src.node import ExpressionLeaf as L, Scope
from src.possibilities import Possibility as P
from src.node import ExpressionLeaf as L
from tests.rulestestcase import RulesTestCase, tree
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):
l0, a, l1 = tree('1,a,2')
self.assertEqual(add_numerics(l0 + l1, (l0, l1, L(1), L(2))), 3)
self.assertEqual(add_numerics(l0 + a + l1, (l0, l1, L(1), L(2))),
L(3) + a)
root = l0 + l1
self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), 3)
root = l0 + a + l1
self.assertEqual(add_numerics(root, (Scope(root), l0, l1)), L(3) + a)
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)
self.assertEqual(add_numerics(l0 + -l1, (l0, -l1, L(1), -L(2))), -1)
self.assertEqual(add_numerics(l0 + a + -l1, (l0, -l1, L(1), -L(2))),
L(-1) + a)
r = ml1 + l2
self.assertEqual(add_numerics(r, (Scope(r), ml1, l2)), 1)
r = l1 + ml2
self.assertEqual(add_numerics(r, (Scope(r), l1, ml2)), -1)
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')
......@@ -71,45 +85,49 @@ class TestRulesNumerics(RulesTestCase):
root = i3 * i2
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
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
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
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):
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)
self.assertEqual(multiply_numerics(f3 * i2, (f3, i2, 3.0, 2)), 6.0)
self.assertEqual(multiply_numerics(i3 * f2, (i3, f2, 3, 2.0)), 6.0)
self.assertEqual(multiply_numerics(f3 * f2, (f3, f2, 3.0, 2.0)), 6.0)
root = i3 * i2
self.assertEqual(multiply_numerics(root, (Scope(root), i3, i2)), 6)
root = f3 * i2
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)),
a * 6 * b)
root = a * i3 * i2 * b
self.assertEqualNodes(multiply_numerics(root,
(Scope(root), i3, i2)), a * 6 * b)
def test_multiply_numerics_negation(self):
l1_neg, l2 = root = tree('-1 * 2')
self.assertEqualNodes(multiply_numerics(root, (l1_neg, l2, -1, 2)), -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)
self.assertEqualNodes(multiply_numerics(root, (Scope(root), l1_neg,
l2)), -l2)
l1, mul = root = tree('1 + -2 * 3')
root, l6 = tree('1 + -2 * 3,6')
l1, mul = root
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')
rest, mul_neg = root
l5_neg, l6 = mul = mul_neg[0]
self.assertEqualNodes(multiply_numerics(mul, (l5_neg, l6, 5, 6)), l30)
rest, mul = root
l5_neg, l6 = mul
self.assertEqualNodes(multiply_numerics(mul, (Scope(mul),
l5_neg, l6)), -l30)
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)
......@@ -3,9 +3,10 @@ from src.rules.powers import match_add_exponents, add_exponents, \
match_multiply_exponents, multiply_exponents, \
match_duplicate_exponent, duplicate_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.node import ExpressionNode as N
from tests.rulestestcase import RulesTestCase, tree
......@@ -17,7 +18,7 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root)
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):
a, p, q, r = tree('a,p,q,r')
......@@ -25,9 +26,9 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities,
[P(root, add_exponents, (n0, n1, a, p, q)),
P(root, add_exponents, (n0, n2, a, p, r)),
P(root, add_exponents, (n1, n2, a, q, r))])
[P(root, add_exponents, (Scope(root), n0, n1, a, p, q)),
P(root, add_exponents, (Scope(root), n0, n2, a, p, r)),
P(root, add_exponents, (Scope(root), n1, n2, a, q, r))])
def test_match_add_exponents_multiple_identifiers(self):
a, b, p, q = tree('a,b,p,q')
......@@ -35,8 +36,16 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_add_exponents(root)
self.assertEqualPos(possibilities,
[P(root, add_exponents, (a0, a1, a, p, q)),
P(root, add_exponents, (b0, b1, b, p, q))])
[P(root, add_exponents, (Scope(root), a0, a1, a, 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_subtract_exponents_powers(self):
a, p, q = tree('a,p,q')
......@@ -84,7 +93,7 @@ class TestRulesPowers(RulesTestCase):
possibilities = match_remove_negative_exponent(root)
self.assertEqualPos(possibilities,
[P(root, remove_negative_exponent, (a, p))])
[P(root, remove_negative_exponent, (a, -p))])
def test_match_exponent_to_root(self):
a, n, m, l1 = tree('a,n,m,1')
......@@ -103,7 +112,8 @@ class TestRulesPowers(RulesTestCase):
a, p, q = tree('a,p,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):
a, p, q = tree('a,p,q')
......@@ -131,11 +141,11 @@ class TestRulesPowers(RulesTestCase):
a ** p * b ** p * c ** p)
def test_remove_negative_exponent(self):
a, p, l1 = tree('a,p,1')
root = a ** -p
a, p, l1 = tree('a,-p,1')
root = a ** p
self.assertEqualNodes(remove_negative_exponent(root, (a, p)),
l1 / a ** p)
l1 / a ** +p)
def test_exponent_to_root(self):
a, n, m, l1 = tree('a,n,m,1')
......@@ -146,3 +156,21 @@ class TestRulesPowers(RulesTestCase):
self.assertEqualNodes(exponent_to_root(root, (a, l1, 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])
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