Skip to content

Commit

Permalink
Added implicit multiplication support
Browse files Browse the repository at this point in the history
  • Loading branch information
charon25 committed Apr 17, 2023
1 parent 67e029c commit 03829e7
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 6 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ print(sy.compute("2^32 - sqrt(tan(42))"))
By default, the module can process the 5 basic operations (`+`, `-`, `*`, `/`, `^`) as well as those functions :
- sqrt
- sin, cos, tan
- min, max (with 2 arguments)
- min, max (with 2, 3 or 4 arguments)
- abs

As well as the constants `pi` and `e`.
Expand All @@ -80,6 +80,16 @@ The `sy.compute` (and `sy.shuting_yard`) also have extra parameters :

## Additional features

### Implicit multiplication

This program supports implicit multiplication (when the `*` symbol is omitted). They can have multiple forms, such as :
- `(1+2)(2+3)`
- `2sin(pi)`
- `(1+2)3`
- `sin(pi)10`

### Functions

Instead of just calling the `sy.compute` function, you can break it into its parts :
- `sy.shunting_yard` will return the RPN equivalent expression of the given mathematical expression ;
- `sy.compute_rpn` will use this expression and compute its value (use the `additional_functions` parameters here).
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="shunting-yard",
version="1.0.7",
version="1.0.8",
author="Paul 'charon25' Kern",
description="Compute any math expression",
long_description=long_description,
Expand All @@ -14,5 +14,5 @@
url="https://www.github.com/charon25/ShuntingYard",
license="MIT",
packages=['shunting_yard'],
download_url="https://github.com/charon25/ShuntingYard/archive/refs/tags/v1.0.7.tar.gz"
download_url="https://github.com/charon25/ShuntingYard/archive/refs/tags/v1.0.8.tar.gz"
)
2 changes: 1 addition & 1 deletion shunting_yard/rpn.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import math
from operator import add, mul, neg, pos, sub, truediv
from typing import Any, Callable, Optional, Union
import math

from shunting_yard.constants import NUMBER_CHARS

Expand Down
4 changes: 2 additions & 2 deletions shunting_yard/shunting_yard.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from string import ascii_lowercase
import math
from enum import Enum
from typing import Optional
import math
import re

from shunting_yard.tokenize import tokenize
from shunting_yard.constants import BASE_OPERATORS, NUMBER_CHARS, FUNCTION_CHARS, SEPARATORS, SEPARATORS_NO_CLOSING_BRACKET, UNARY_OPERATORS_SYMBOLS
Expand Down
22 changes: 22 additions & 0 deletions shunting_yard/tokenize.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
import re
from typing import Iterator

from shunting_yard.constants import BASE_OPERATORS, FUNCTION_CHARS, FUNCTION_FIRST_CHARS, NUMBER_CHARS, SEPARATORS, UNARY_OPERATORS



def _remove_implicit_multiplication(expression: str) -> str:
"""Add '*' to every implicit multiplication. These can be :
- between a number and a variable or function: 2x 3sin(x)
- between brackets: (x+1)(x-1), sin(x)(1+2)
- between a closing bracket and a number/variable/function: sin(x)2, (1+2)x, (1+2)sin(x)
However, a lot of cases do not require implicit multiplication, such as:
- 0.5
- min(1,2)
- min((1+2),2)
...
"""

# Insert '*' between a number and anything other than a digit, an operation, a closing bracket, a decimal dot, a function parameters separator
expression = re.sub(r'\b(\d+)([^)\d.,;+*\/^-])', r'\1*\2', expression)
# Insert '*' between a closing bracket and anything other than an operation, another closing bracket, a function parameters separator
expression = re.sub(r'(\))([^),;+*\/^-])', r'\1*\2', expression)
return expression


def tokenize(string: str) -> Iterator[str]:
if string == '':
return

# Remove all whitespaces are they do not change anything
string = ''.join(string.split())
string = _remove_implicit_multiplication(string)

cursor = 0
is_infix = False
Expand Down
49 changes: 49 additions & 0 deletions tests/test_tokenize.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest

from shunting_yard import tokenize
from shunting_yard.tokenize import _remove_implicit_multiplication


class TestTokenizer(unittest.TestCase):
Expand Down Expand Up @@ -73,6 +74,54 @@ def test_digits_in_function_name(self):
self.assertListEqual(list(tokenize('min3(1, 2)')), ['min3', '(', '1', ',', '2', ')'])


class TestRemoveImplicitMultiplication(unittest.TestCase):

def test_no_implicit_mult(self):
self.assertEqual(_remove_implicit_multiplication('1+1'), '1+1')
self.assertEqual(_remove_implicit_multiplication('1-1'), '1-1')
self.assertEqual(_remove_implicit_multiplication('1*1'), '1*1')
self.assertEqual(_remove_implicit_multiplication('1/1'), '1/1')
self.assertEqual(_remove_implicit_multiplication('1^1'), '1^1')
self.assertEqual(_remove_implicit_multiplication('(1)+1'), '(1)+1')
self.assertEqual(_remove_implicit_multiplication('(1)-1'), '(1)-1')
self.assertEqual(_remove_implicit_multiplication('(1)*1'), '(1)*1')
self.assertEqual(_remove_implicit_multiplication('(1)/1'), '(1)/1')
self.assertEqual(_remove_implicit_multiplication('(1)^1'), '(1)^1')
self.assertEqual(_remove_implicit_multiplication('((1))'), '((1))')

def test_no_implicit_mult2(self):
self.assertEqual(_remove_implicit_multiplication('0.5'), '0.5')
self.assertEqual(_remove_implicit_multiplication('max(1,2)'), 'max(1,2)')
self.assertEqual(_remove_implicit_multiplication('max(1;2)'), 'max(1;2)')
self.assertEqual(_remove_implicit_multiplication('max((1+2),2)'), 'max((1+2),2)')
self.assertEqual(_remove_implicit_multiplication('max((1+2);2)'), 'max((1+2);2)')

def test_digit_other(self):
self.assertEqual(_remove_implicit_multiplication('2x'), '2*x')
self.assertEqual(_remove_implicit_multiplication('3cos(5)'), '3*cos(5)')
self.assertEqual(_remove_implicit_multiplication('3_func(0)'), '3*_func(0)')
self.assertEqual(_remove_implicit_multiplication('1(2+3)'), '1*(2+3)')

def test_implicit_mult_double_brackets(self):
self.assertEqual(_remove_implicit_multiplication('(1+2)(3+4)'), '(1+2)*(3+4)')
self.assertEqual(_remove_implicit_multiplication('(1+(2))((3+5)+4)((6+7)(8+9))'), '(1+(2))*((3+5)+4)*((6+7)*(8+9))')
self.assertEqual(_remove_implicit_multiplication('sin(pi)(1+2)'), 'sin(pi)*(1+2)')

def test_implicit_mult_bracket_other(self):
self.assertEqual(_remove_implicit_multiplication('sin(1)2'), 'sin(1)*2')
self.assertEqual(_remove_implicit_multiplication('sin(1).5'), 'sin(1)*.5')
self.assertEqual(_remove_implicit_multiplication('(1+2)3'), '(1+2)*3')
self.assertEqual(_remove_implicit_multiplication('(1+2)x'), '(1+2)*x')
self.assertEqual(_remove_implicit_multiplication('(1+2)sin(1)'), '(1+2)*sin(1)')

def test_with_longer_numbers(self):
self.assertEqual(_remove_implicit_multiplication('200x'), '200*x')
self.assertEqual(_remove_implicit_multiplication('301cos(5)'), '301*cos(5)')
self.assertEqual(_remove_implicit_multiplication('345_func(0)'), '345*_func(0)')
self.assertEqual(_remove_implicit_multiplication('156(2+3)'), '156*(2+3)')
self.assertEqual(_remove_implicit_multiplication('1+200x'), '1+200*x')



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

0 comments on commit 03829e7

Please sign in to comment.