Skip to content

Commit 15ba47f

Browse files
committed
Added proper functionality for unary minus and plus
1 parent 4632933 commit 15ba47f

File tree

7 files changed

+49
-6
lines changed

7 files changed

+49
-6
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="shunting-yard",
8-
version="1.0.2",
8+
version="1.0.3",
99
author="Paul 'charon25' Kern",
1010
description="Compute any math expression",
1111
long_description=long_description,

shunting_yard/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from string import ascii_lowercase
22

33

4+
UNARY_OPERATORS = '+-'
45
BASE_OPERATORS = '+-*/^'
56
NUMBER_CHARS = '0123456789.'
67
FUNCTION_CHARS = ascii_lowercase + '_'

shunting_yard/rpn.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import math
2-
from operator import add, mul, sub, truediv
2+
from operator import add, mul, neg, pos, sub, truediv
33
from typing import Any, Callable, Optional, Union
44

55
from shunting_yard.constants import NUMBER_CHARS
@@ -14,7 +14,9 @@ class WrongExpressionError(Exception):
1414

1515
FUNCTIONS: FunctionDictionary = {
1616
'+': (2, add),
17+
'+u': (1, pos),
1718
'-': (2, sub),
19+
'-u': (1, neg),
1820
'*': (2, mul),
1921
'/': (2, truediv),
2022
'^': (2, pow),

shunting_yard/shunting_yard.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ class MismatchedBracketsError(Exception):
1515
'-': 10,
1616
'*': 20,
1717
'/': 20,
18-
'^': 30
18+
'^': 30,
19+
'-u': 40,
20+
'+u': 40,
1921
}
2022

2123
# Returns the precendence if the operator is one of +-*/^, or infinity if it is a function
@@ -33,6 +35,8 @@ class Associativity(Enum):
3335
'*': Associativity.LEFT,
3436
'/': Associativity.LEFT,
3537
'^': Associativity.RIGHT,
38+
'-u': Associativity.RIGHT,
39+
'+u': Associativity.LEFT,
3640
}
3741

3842

shunting_yard/tokenize.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Iterator
22

3-
from shunting_yard.constants import BASE_OPERATORS, NUMBER_CHARS, FUNCTION_CHARS
3+
from shunting_yard.constants import BASE_OPERATORS, FUNCTION_CHARS, NUMBER_CHARS, UNARY_OPERATORS
44

55

66
def tokenize(string: str) -> Iterator[str]:
@@ -9,11 +9,20 @@ def tokenize(string: str) -> Iterator[str]:
99

1010
string = string.lower()
1111
cursor = 0
12+
is_infix = False
13+
1214
while cursor < len(string):
1315
char = string[cursor]
14-
if char in BASE_OPERATORS or char in '()':
16+
17+
if not is_infix and char in UNARY_OPERATORS:
18+
yield f'{char}u'
19+
cursor += 1
20+
21+
elif char in BASE_OPERATORS or char in '()':
1522
yield char
1623
cursor += 1
24+
is_infix = (char == ')')
25+
1726
elif char in NUMBER_CHARS:
1827
# Go through until not a number anymore
1928
cursor_end = cursor + 1
@@ -22,6 +31,8 @@ def tokenize(string: str) -> Iterator[str]:
2231

2332
yield string[cursor:cursor_end]
2433
cursor += (cursor_end - cursor)
34+
is_infix = True
35+
2536
elif char in FUNCTION_CHARS:
2637
# Go through until not a number anymore
2738
cursor_end = cursor + 1
@@ -30,5 +41,7 @@ def tokenize(string: str) -> Iterator[str]:
3041

3142
yield string[cursor:cursor_end]
3243
cursor += (cursor_end - cursor)
44+
is_infix = True
45+
3346
else:
3447
cursor += 1

tests/test_rpn.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ def test_base_operators(self):
1313
self.assertAlmostEqual(compute_rpn('1 2 /'), 0.5)
1414
self.assertEqual(compute_rpn('1 2 ^'), 1)
1515

16+
def test_unary_minus(self):
17+
self.assertEqual(compute_rpn('1 -u'), -1)
18+
self.assertEqual(compute_rpn('1 2 + -u'), -3)
19+
self.assertEqual(compute_rpn('1 -u 2 *'), -2)
20+
21+
def test_unary_plus(self):
22+
self.assertEqual(compute_rpn('1 +u'), 1)
23+
self.assertEqual(compute_rpn('1 2 + +u'), 3)
24+
self.assertEqual(compute_rpn('1 +u 2 *'), 2)
25+
1626
def test_priority_brackets(self):
1727
self.assertEqual(compute_rpn('2 1 * 3 +'), 5)
1828
self.assertEqual(compute_rpn('2 1 3 + *'), 8)

tests/test_tokenize.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_function_one_argument(self):
3737
self.assertListEqual(list(tokenize('sin(1 + 4)')), ['sin', '(', '1', '+', '4', ')'])
3838

3939
def test_function_no_argument(self):
40-
self.assertListEqual(list(tokenize('-3/2*pi')), ['-', '3', '/', '2', '*', 'pi'])
40+
self.assertListEqual(list(tokenize('-3/2*pi')), ['-u', '3', '/', '2', '*', 'pi'])
4141

4242
def test_function_2_arguments(self):
4343
self.assertListEqual(list(tokenize('max(1, 4)')), ['max', '(', '1', '4', ')'])
@@ -48,6 +48,19 @@ def test_function_upper_case(self):
4848
def test_function_underscore(self):
4949
self.assertListEqual(list(tokenize('arc_cos(0)')), ['arc_cos', '(', '0', ')'])
5050

51+
def test_function_unary_minus(self):
52+
self.assertListEqual(list(tokenize('-1')), ['-u', '1'])
53+
self.assertListEqual(list(tokenize('-(1+2)')), ['-u', '(', '1', '+', '2', ')'])
54+
self.assertListEqual(list(tokenize('2*-1')), ['2', '*', '-u', '1'])
55+
self.assertListEqual(list(tokenize('-(-1)')), ['-u', '(', '-u', '1', ')'])
56+
57+
def test_function_unary_plus(self):
58+
self.assertListEqual(list(tokenize('+1')), ['+u', '1'])
59+
self.assertListEqual(list(tokenize('+(1+2)')), ['+u', '(', '1', '+', '2', ')'])
60+
self.assertListEqual(list(tokenize('2*+1')), ['2', '*', '+u', '1'])
61+
self.assertListEqual(list(tokenize('+(+1)')), ['+u', '(', '+u', '1', ')'])
62+
63+
5164

5265
if __name__ == '__main__':
5366
unittest.main()

0 commit comments

Comments
 (0)