├── .coveragerc ├── .github └── workflows │ ├── publish-documentation.yml │ └── python-package.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── _ext │ ├── canonical.py │ └── github.py ├── _static │ ├── favicon.ico │ ├── mathparse-origional.png │ ├── mathparse.png │ └── style.css ├── _templates │ └── layout.html ├── conf.py ├── index.rst ├── setup.rst └── utils.rst ├── mathparse ├── __init__.py ├── mathparse.py └── mathwords.py ├── pyproject.toml └── tests ├── __init__.py ├── test_binary_operations.py ├── test_replace_word_tokens.py ├── test_unary_operations.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | show_missing = True -------------------------------------------------------------------------------- /.github/workflows/publish-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Sphinx documentation to Pages 2 | 3 | on: 4 | push: 5 | branches: [master] # branch to trigger deployment 6 | 7 | jobs: 8 | pages: 9 | runs-on: ubuntu-20.04 10 | environment: 11 | name: github-pages 12 | url: ${{ steps.deployment.outputs.page_url }} 13 | permissions: 14 | pages: write 15 | id-token: write 16 | steps: 17 | - id: deployment 18 | uses: sphinx-notes/pages@v3 19 | with: 20 | pyproject_extras: 'test' 21 | sphinx_build_options: '-b dirhtml' 22 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Setup 28 | run: | 29 | python -m pip install .[test] 30 | - name: Lint 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --show-source --statistics 34 | - name: Test code 35 | run: | 36 | python -m unittest discover -s tests 37 | - name: Test documentation 38 | run: | 39 | sphinx-build -nW -b html ./docs/ ./build/ 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | html/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # IPython Notebook 60 | .ipynb_checkpoints 61 | 62 | # pyenv 63 | .python-version 64 | 65 | # dotenv 66 | .env 67 | 68 | # virtualenv 69 | venv/ 70 | ENV/ 71 | 72 | #IDEA settings 73 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Gunther Cox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mathparse 2 | 3 | The `mathparse` library is a Python module designed to evaluate mathematical equations contained in strings. 4 | 5 | Here are a few examples: 6 | 7 | ```python 8 | from mathparse import mathparse 9 | 10 | mathparse.parse('50 * (85 / 100)') 11 | >>> 42.5 12 | 13 | mathparse.parse('one hundred times fifty four', mathparse.codes.ENG) 14 | >>> 5400 15 | 16 | mathparse.parse('(seven * nine) + 8 - (45 plus two)', language='ENG') 17 | >>> 24 18 | ``` 19 | 20 | ## Installation 21 | 22 | ```bash 23 | pip install mathparse 24 | ``` 25 | 26 | ## Language support 27 | 28 | The language parameter must be set in order to evaluate an equation that uses word operators. 29 | The language code should be a valid [ISO 639-2](https://www.loc.gov/standards/iso639-2/php/code_list.php) language code. 30 | 31 | ## Documentation 32 | 33 | See the full documentation at https://mathparse.chatterbot.us 34 | 35 | ## Changelog 36 | 37 | See [release notes](https://github.com/gunthercox/ChatterBot/releases) for changes. 38 | -------------------------------------------------------------------------------- /docs/_ext/canonical.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add GitHub repository details to the Sphinx context. 3 | """ 4 | 5 | 6 | def setup_canonical_func(app, pagename, templatename, context, doctree): 7 | """ 8 | Return the url to the specified page on GitHub. 9 | 10 | (Sphinx 7.4 generates a canonical link with a .html extension even 11 | when run in dirhtml mode) 12 | """ 13 | 14 | conf = app.config 15 | 16 | def canonical_func(): 17 | # Special case for the root index page 18 | if pagename == 'index': 19 | return conf.html_baseurl 20 | 21 | dir_name = pagename.replace('/index', '/') 22 | return f'{conf.html_baseurl}{dir_name}' 23 | 24 | # Add it to the page's context 25 | context['canonical_url'] = canonical_func 26 | 27 | 28 | # Extension setup function 29 | def setup(app): 30 | app.connect('html-page-context', setup_canonical_func) 31 | -------------------------------------------------------------------------------- /docs/_ext/github.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add GitHub repository details to the Sphinx context. 3 | """ 4 | 5 | GITHUB_USER = 'gunthercox' 6 | GITHUB_REPO = 'mathparse' 7 | 8 | 9 | def setup_github_func(app, pagename, templatename, context, doctree): 10 | """ 11 | Return the url to the specified page on GitHub. 12 | """ 13 | 14 | github_version = 'master' 15 | docs_path = 'docs' 16 | 17 | def my_func(): 18 | return ( 19 | f'https://github.com/{GITHUB_USER}/{GITHUB_REPO}/blob/' 20 | f'{github_version}/{docs_path}/{pagename}.rst' 21 | ) 22 | 23 | # Add it to the page's context 24 | context['github_page_link'] = my_func 25 | 26 | 27 | # Extension setup function 28 | def setup(app): 29 | app.connect('html-page-context', setup_github_func) 30 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gunthercox/mathparse/93c77873cc043f3d88d2e6d4f92611700ba38f92/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/mathparse-origional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gunthercox/mathparse/93c77873cc043f3d88d2e6d4f92611700ba38f92/docs/_static/mathparse-origional.png -------------------------------------------------------------------------------- /docs/_static/mathparse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gunthercox/mathparse/93c77873cc043f3d88d2e6d4f92611700ba38f92/docs/_static/mathparse.png -------------------------------------------------------------------------------- /docs/_static/style.css: -------------------------------------------------------------------------------- 1 | table { 2 | width: 100%; 3 | } 4 | 5 | th.head p { 6 | text-align: center; 7 | } 8 | 9 | .table-justified td { 10 | width: 50%; 11 | } 12 | 13 | .caption-text { 14 | padding-top: 3px; 15 | padding-bottom: 3px; 16 | } 17 | 18 | .wy-side-nav-search { 19 | background-color: #300a24; 20 | } 21 | 22 | .toctree-l1 { 23 | padding-bottom: 3px; 24 | } 25 | 26 | div.sphinxsidebar { 27 | overflow: hidden; 28 | } 29 | 30 | table caption span.caption-text { 31 | color: #efefef; 32 | background-color: #1c4e63; 33 | width: 100%; 34 | display: block; 35 | } 36 | 37 | .banner { 38 | margin: 0px -20px; 39 | } 40 | 41 | #searchbox { 42 | margin-bottom: 15px; 43 | } 44 | 45 | div.sphinxsidebar input[name="q"] { 46 | border-top-left-radius: 5px; 47 | border-bottom-left-radius: 5px; 48 | } 49 | 50 | div.sphinxsidebar input[type="submit"] { 51 | border-top-right-radius: 5px; 52 | border-bottom-right-radius: 5px; 53 | } 54 | 55 | 56 | .help-footer { 57 | margin-top: 15px; 58 | border-top: 15px #320023 solid; 59 | } 60 | 61 | .help-footer h1, .help-footer h2 { 62 | color: gray!important; 63 | background-color: transparent!important; 64 | } 65 | 66 | .help-footer a { 67 | color: #2980B9; 68 | text-decoration: none; 69 | } 70 | 71 | 72 | .bluesky-social-icon { 73 | width: 30px; 74 | margin-right: 10px; 75 | } 76 | 77 | .inline-block { 78 | display: inline-block; 79 | } 80 | 81 | /* mathparse */ 82 | 83 | div.sphinxsidebarwrapper, p.logo { 84 | padding-top: 0px; 85 | margin-top: 0px; 86 | } -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends '!layout.html' %} 2 | {# https://github.com/sphinx-doc/sphinx/blob/master/sphinx/themes/basic/layout.html #} 3 | 4 | {% set pageurl = canonical_url() %} 5 | 6 | {%- block htmltitle %} 7 | 8 | 10 | 11 | {{ super() }} 12 | {%- endblock %} 13 | 14 | {%- block relbaritems %} 15 |
  • Edit on GitHub |
  • 16 | {% endblock %} 17 | 18 | {% block menu %} 19 | {{ super() }} 20 | {% endblock %} 21 | 22 | {%- block footer %} 23 | {{ super() }} 24 | 25 | {% endblock %} -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # mathparse documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Aug 8 07:11:36 2017. 6 | 7 | 8 | # If extensions (or modules to document with autodoc) are in another directory, 9 | # add these directories to sys.path here. If the directory is relative to the 10 | # documentation root, use os.path.abspath to make it absolute, like shown here. 11 | 12 | import os 13 | import sys 14 | from pathlib import Path 15 | from datetime import datetime 16 | 17 | current_directory = os.path.abspath('.') 18 | parent_directory = os.path.abspath(os.path.join(current_directory, os.pardir)) 19 | 20 | sys.path.insert(0, parent_directory) 21 | 22 | sys.path.append(str(Path('_ext').resolve())) 23 | 24 | from mathparse import __version__ as mathparse_version # noqa: E402 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.autosectionlabel', 32 | 'sphinx.ext.doctest', 33 | 'sphinx.ext.intersphinx', 34 | 'sphinx.ext.coverage', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx.ext.ifconfig', 37 | 'sphinx.ext.viewcode', 38 | 'sphinx.ext.githubpages', 39 | 'github', 40 | 'canonical', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix(es) of source filenames. 47 | source_suffix = ['.rst', '.md'] 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'mathparse' 54 | author = 'Gunther Cox' 55 | copyright = '{}, {}'.format( 56 | datetime.now().year, 57 | author 58 | ) 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | 64 | # The full version, including alpha/beta/rc tags 65 | release = mathparse_version 66 | 67 | # The short X.Y version 68 | version = mathparse_version.rsplit('.', 1)[0] 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = 'en' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = [] 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'sphinx' 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = False 87 | 88 | 89 | # -- Options for HTML output ---------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | 94 | html_theme = 'classic' 95 | 96 | html_logo = '_static/mathparse.png' 97 | 98 | html_favicon = '_static/favicon.ico' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation: 103 | # https://www.sphinx-doc.org/en/master/usage/theming.html 104 | html_theme_options = { 105 | 'externalrefs': True, 106 | 'sidebarbgcolor': '#300a24', 107 | 'relbarbgcolor': '#26001b', 108 | 'footerbgcolor': '#13000d', 109 | 'headbgcolor': '#503949', 110 | 'headtextcolor': '#e8ffca', 111 | 'headlinkcolor': '#e8ffca', 112 | 'sidebarwidth': '300px', 113 | # 'collapsiblesidebar': True, 114 | } 115 | 116 | html_show_sourcelink = True 117 | 118 | html_baseurl = 'https://mathparse.chatterbot.us/' 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | html_css_files = [ 126 | 'style.css' 127 | ] 128 | 129 | # Custom sidebar templates, must be a dictionary that maps document names 130 | # to template names. 131 | 132 | # This is required for the alabaster theme 133 | html_sidebars = { 134 | '**': ['searchbox.html', 'globaltoc.html'] 135 | } 136 | 137 | 138 | # -- Options for HTMLHelp output ------------------------------------------ 139 | 140 | # Output file base name for HTML help builder. 141 | htmlhelp_basename = 'mathparsedoc' 142 | 143 | html_context = { 144 | 'extra_css_files': [ 145 | '_static/style.css' 146 | ] 147 | } 148 | 149 | # -- Options for LaTeX output --------------------------------------------- 150 | 151 | latex_elements = { 152 | # The paper size ('letterpaper' or 'a4paper'). 153 | # 154 | # 'papersize': 'letterpaper', 155 | 156 | # The font size ('10pt', '11pt' or '12pt'). 157 | # 158 | # 'pointsize': '10pt', 159 | 160 | # Additional stuff for the LaTeX preamble. 161 | # 162 | # 'preamble': '', 163 | 164 | # Latex figure (float) alignment 165 | # 166 | # 'figure_align': 'htbp', 167 | } 168 | 169 | # Grouping the document tree into LaTeX files. List of tuples 170 | # (source start file, target name, title, 171 | # author, documentclass [howto, manual, or own class]). 172 | latex_documents = [ 173 | (master_doc, 'mathparse.tex', 'mathparse Documentation', 174 | 'Gunther Cox', 'manual'), 175 | ] 176 | 177 | 178 | # -- Options for manual page output --------------------------------------- 179 | 180 | # One entry per manual page. List of tuples 181 | # (source start file, name, description, authors, manual section). 182 | man_pages = [ 183 | (master_doc, 'mathparse', 'mathparse Documentation', 184 | [author], 1) 185 | ] 186 | 187 | 188 | # -- Options for Texinfo output ------------------------------------------- 189 | 190 | # Grouping the document tree into Texinfo files. List of tuples 191 | # (source start file, target name, title, author, 192 | # dir menu entry, description, category) 193 | texinfo_documents = [ 194 | (master_doc, 'mathparse', 'mathparse Documentation', 195 | author, 'mathparse', 'One line description of project.', 196 | 'Miscellaneous'), 197 | ] 198 | 199 | 200 | # Example configuration for intersphinx: refer to the Python standard library. 201 | intersphinx_mapping = { 202 | 'python': ('https://docs.python.org/3', None) 203 | } 204 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mathparse documentation index 2 | 3 | Mathparse Documentation 4 | ======================= 5 | 6 | .. note:: 7 | As of April 2025, the ``mathparse`` documentation has moved under the 8 | ChatterBot domain in an effort to group these projects closer together. 9 | While both projects are distinctly different, and serve vastly different 10 | purposes, they both benefit from being a part of an ecosystem of software 11 | libraries that are commonly used together. 12 | 13 | .. automodule:: mathparse 14 | :members: 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | 19 | setup 20 | utils 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | 29 | ---- 30 | 31 | A special thanks to `Griffin Cox`_ for the original design of the Mathparse logo. 32 | 33 | .. _`Griffin Cox`: https://github.com/griffincx 34 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | mathparse can be installed using pip: 5 | 6 | .. code-block:: shell 7 | 8 | pip install mathparse 9 | 10 | 11 | Usage 12 | ===== 13 | 14 | .. code-block:: python 15 | 16 | from mathparse import mathparse 17 | 18 | mathparse.parse('50 * (85 / 100)') 19 | >>> 42.5 20 | 21 | mathparse.parse('one hundred times fifty four', mathparse.codes.ENG) 22 | >>> 5400 23 | 24 | mathparse.parse('(seven * nine) + 8 - (45 plus two)', language='ENG') 25 | >>> 24 26 | 27 | 28 | .. note:: 29 | 30 | The language parameter must be set in order to evaluate an equation that uses word operators. 31 | The language code should be a valid `ISO 639-2`_ language code. 32 | 33 | 34 | .. _`ISO 639-2`: https://www.loc.gov/standards/iso639-2/php/code_list.php 35 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | Utility functions 2 | ================= 3 | 4 | .. automodule:: mathparse.mathwords 5 | :members: 6 | 7 | 8 | Parsing strings 9 | +++++++++++++++ 10 | 11 | .. automodule:: mathparse.mathparse 12 | :members: 13 | -------------------------------------------------------------------------------- /mathparse/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | mathparse is a library for solving mathematical equations contained in strings 3 | """ 4 | 5 | __version__ = '0.2.1' 6 | -------------------------------------------------------------------------------- /mathparse/mathparse.py: -------------------------------------------------------------------------------- 1 | """ 2 | Methods for evaluating mathematical equations in strings. 3 | """ 4 | from decimal import Decimal 5 | from . import mathwords 6 | import re 7 | 8 | 9 | class PostfixTokenEvaluationException(Exception): 10 | """ 11 | Exception to be raised when a language code is given that 12 | is not a part of the ISO 639-2 standard. 13 | """ 14 | pass 15 | 16 | 17 | def is_int(string): 18 | """ 19 | Return true if string is an integer. 20 | """ 21 | try: 22 | int(string) 23 | return True 24 | except ValueError: 25 | return False 26 | 27 | 28 | def is_float(string): 29 | """ 30 | Return true if the string is a float. 31 | """ 32 | try: 33 | float(string) 34 | return '.' in string 35 | except ValueError: 36 | return False 37 | 38 | 39 | def is_constant(string): 40 | """ 41 | Return true if the string is a mathematical constant. 42 | """ 43 | return mathwords.CONSTANTS.get(string, False) 44 | 45 | 46 | def is_unary(string): 47 | """ 48 | Return true if the string is a defined unary mathematical 49 | operator function. 50 | """ 51 | return string in mathwords.UNARY_FUNCTIONS 52 | 53 | 54 | def is_binary(string): 55 | """ 56 | Return true if the string is a defined binary operator. 57 | """ 58 | return string in mathwords.BINARY_OPERATORS 59 | 60 | 61 | def is_symbol(string): 62 | """ 63 | Return true if the string is a mathematical symbol. 64 | """ 65 | return ( 66 | is_int(string) or is_float(string) or 67 | is_constant(string) or is_unary(string) or 68 | is_binary(string) or 69 | (string == '(') or (string == ')') 70 | ) 71 | 72 | 73 | def is_word(word, language): 74 | """ 75 | Return true if the word is a math word for the specified language. 76 | """ 77 | words = mathwords.words_for_language(language) 78 | 79 | return word in words 80 | 81 | 82 | def find_word_groups(string, words): 83 | """ 84 | Find matches for words in the format "3 thousand 6 hundred 2". 85 | The words parameter should be the list of words to check for 86 | such as "hundred". 87 | """ 88 | scale_pattern = '|'.join(words) 89 | # For example: 90 | # (?:(?:\d+)\s+(?:hundred|thousand)*\s*)+(?:\d+|hundred|thousand)+ 91 | regex = re.compile( 92 | r'(?:(?:\d+)\s+(?:' + 93 | scale_pattern + 94 | r')*\s*)+(?:\d+|' + 95 | scale_pattern + r')+' 96 | ) 97 | result = regex.findall(string) 98 | return result 99 | 100 | 101 | def replace_word_tokens(string, language): 102 | """ 103 | Given a string and an ISO 639-2 language code, 104 | return the string with the words replaced with 105 | an operational equivalent. 106 | """ 107 | words = mathwords.word_groups_for_language(language) 108 | 109 | # Replace operator words with numeric operators 110 | operators = words['binary_operators'].copy() 111 | if 'unary_operators' in words: 112 | operators.update(words['unary_operators']) 113 | 114 | for operator in list(operators.keys()): 115 | if operator in string: 116 | string = string.replace(operator, operators[operator]) 117 | 118 | # Replace number words with numeric values 119 | numbers = words['numbers'] 120 | for number in list(numbers.keys()): 121 | if number in string: 122 | string = string.replace(number, str(numbers[number])) 123 | 124 | # Replace scaling multipliers with numeric values 125 | scales = words['scales'] 126 | end_index_characters = mathwords.BINARY_OPERATORS 127 | end_index_characters.add('(') 128 | 129 | word_matches = find_word_groups(string, list(scales.keys())) 130 | 131 | for match in word_matches: 132 | string = string.replace(match, '(' + match + ')') 133 | 134 | for scale in list(scales.keys()): 135 | for _ in range(0, string.count(scale)): 136 | start_index = string.find(scale) - 1 137 | end_index = len(string) 138 | 139 | while is_int(string[start_index - 1]) and start_index > 0: 140 | start_index -= 1 141 | 142 | end_index = string.find(' ', start_index) + 1 143 | end_index = string.find(' ', end_index) + 1 144 | 145 | add = ' + ' 146 | if string[end_index] in end_index_characters: 147 | add = '' 148 | 149 | string = string[:start_index] + '(' + string[start_index:] 150 | string = string.replace( 151 | scale, '* ' + str(scales[scale]) + ')' + add, 152 | 1 153 | ) 154 | 155 | string = string.replace(') (', ') + (') 156 | 157 | return string 158 | 159 | 160 | def to_postfix(tokens): 161 | """ 162 | Convert a list of evaluatable tokens to postfix format. 163 | """ 164 | precedence = { 165 | '/': 4, 166 | '*': 4, 167 | '+': 3, 168 | '-': 3, 169 | '^': 2, 170 | '(': 1 171 | } 172 | 173 | postfix = [] 174 | opstack = [] 175 | 176 | for token in tokens: 177 | if is_int(token): 178 | postfix.append(int(token)) 179 | elif is_float(token): 180 | postfix.append(float(token)) 181 | elif token in mathwords.CONSTANTS: 182 | postfix.append(mathwords.CONSTANTS[token]) 183 | elif is_unary(token): 184 | opstack.append(token) 185 | elif token == '(': 186 | opstack.append(token) 187 | elif token == ')': 188 | top_token = opstack.pop() 189 | while top_token != '(': 190 | postfix.append(top_token) 191 | top_token = opstack.pop() 192 | else: 193 | while (opstack != []) and ( 194 | precedence[opstack[-1]] >= precedence[token] 195 | ): 196 | postfix.append(opstack.pop()) 197 | opstack.append(token) 198 | 199 | while opstack != []: 200 | postfix.append(opstack.pop()) 201 | 202 | return postfix 203 | 204 | 205 | def evaluate_postfix(tokens): 206 | """ 207 | Given a list of evaluatable tokens in postfix format, 208 | calculate a solution. 209 | """ 210 | stack = [] 211 | 212 | for token in tokens: 213 | total = None 214 | 215 | if is_int(token) or is_float(token) or is_constant(token): 216 | stack.append(token) 217 | elif is_unary(token): 218 | a = stack.pop() 219 | total = mathwords.UNARY_FUNCTIONS[token](a) 220 | elif len(stack): 221 | b = stack.pop() 222 | a = stack.pop() 223 | if token == '+': 224 | total = a + b 225 | elif token == '-': 226 | total = a - b 227 | elif token == '*': 228 | total = a * b 229 | elif token == '^': 230 | total = a ** b 231 | elif token == '/': 232 | if Decimal(str(b)) == 0: 233 | total = 'undefined' 234 | else: 235 | total = Decimal(str(a)) / Decimal(str(b)) 236 | else: 237 | raise PostfixTokenEvaluationException( 238 | 'Unknown token {}'.format(token) 239 | ) 240 | 241 | if total is not None: 242 | stack.append(total) 243 | 244 | # If the stack is empty the tokens could not be evaluated 245 | if not stack: 246 | raise PostfixTokenEvaluationException( 247 | 'The postfix expression resulted in an empty stack' 248 | ) 249 | 250 | return stack.pop() 251 | 252 | 253 | def tokenize(string, language=None, escape='___'): 254 | """ 255 | Given a string, return a list of math symbol tokens 256 | """ 257 | # Set all words to lowercase 258 | string = string.lower() 259 | 260 | # Ignore punctuation 261 | if len(string) and not string[-1].isalnum(): 262 | character = string[-1] 263 | string = string[:-1] + ' ' + character 264 | 265 | # Parenthesis must have space around them to be tokenized properly 266 | string = string.replace('(', ' ( ') 267 | string = string.replace(')', ' ) ') 268 | 269 | if language: 270 | words = mathwords.words_for_language(language) 271 | 272 | for phrase in words: 273 | escaped_phrase = phrase.replace(' ', escape) 274 | string = string.replace(phrase, escaped_phrase) 275 | 276 | tokens = string.split() 277 | 278 | for index, token in enumerate(tokens): 279 | tokens[index] = token.replace(escape, ' ') 280 | 281 | return tokens 282 | 283 | 284 | def parse(string, language=None): 285 | """ 286 | Return a solution to the equation in the input string. 287 | """ 288 | if language: 289 | string = replace_word_tokens(string, language) 290 | 291 | tokens = tokenize(string) 292 | postfix = to_postfix(tokens) 293 | 294 | return evaluate_postfix(postfix) 295 | 296 | 297 | def extract_expression(dirty_string, language): 298 | """ 299 | Give a string such as: "What is 4 + 4?" 300 | Return the string "4 + 4" 301 | """ 302 | tokens = tokenize(dirty_string, language) 303 | 304 | start_index = 0 305 | end_index = len(tokens) 306 | 307 | for part in tokens: 308 | if is_symbol(part) or is_word(part, language): 309 | break 310 | else: 311 | start_index += 1 312 | 313 | for part in reversed(tokens): 314 | if is_symbol(part) or is_word(part, language): 315 | break 316 | else: 317 | end_index -= 1 318 | 319 | return ' '.join(tokens[start_index:end_index]) 320 | -------------------------------------------------------------------------------- /mathparse/mathwords.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import math 4 | 5 | """ 6 | Utility methods for getting math word terms. 7 | """ 8 | 9 | BINARY_OPERATORS = { 10 | '^', '*', '/', '+', '-' 11 | } 12 | 13 | MATH_WORDS = { 14 | 'ENG': { 15 | 'unary_operators': { 16 | 'squared': '^ 2', 17 | 'cubed': '^ 3', 18 | 'square root of': 'sqrt' 19 | }, 20 | 'binary_operators': { 21 | 'plus': '+', 22 | 'divided by': '/', 23 | 'minus': '-', 24 | 'times': '*', 25 | 'to the power of': '^' 26 | }, 27 | 'numbers': { 28 | 'zero': 0, 29 | 'one': 1, 30 | 'two': 2, 31 | 'three': 3, 32 | 'four': 4, 33 | 'five': 5, 34 | 'six': 6, 35 | 'seven': 7, 36 | 'eight': 8, 37 | 'nine': 9, 38 | 'ten': 10, 39 | 'eleven': 11, 40 | 'twelve': 12, 41 | 'thirteen': 13, 42 | 'fourteen': 14, 43 | 'fifteen': 15, 44 | 'sixteen': 16, 45 | 'seventeen': 17, 46 | 'eighteen': 18, 47 | 'nineteen': 19, 48 | 'twenty': 20, 49 | 'thirty': 30, 50 | 'forty': 40, 51 | 'fifty': 50, 52 | 'sixty': 60, 53 | 'seventy': 70, 54 | 'eighty': 80, 55 | 'ninety': 90 56 | }, 57 | 'scales': { 58 | 'hundred': 100, 59 | 'thousand': 1000, 60 | 'million': 1000000, 61 | 'billion': 1000000000, 62 | 'trillion': 1000000000000 63 | } 64 | }, 65 | 'FRE': { 66 | 'binary_operators': { 67 | 'plus': '+', 68 | 'divisé par': '/', 69 | 'moins': '-', 70 | 'fois': '*', 71 | 'équarri': '^ 2', 72 | 'en cubes': '^ 3', 73 | 'à la puissance': '^' 74 | }, 75 | 'numbers': { 76 | 'un': 1, 77 | 'deux': 2, 78 | 'trois': 3, 79 | 'quatre': 4, 80 | 'cinq': 5, 81 | 'six': 6, 82 | 'sept': 7, 83 | 'huit': 8, 84 | 'neuf': 9, 85 | 'dix': 10, 86 | 'onze': 11, 87 | 'douze': 12, 88 | 'treize': 13, 89 | 'quatorze': 14, 90 | 'quinze': 15, 91 | 'seize': 16, 92 | 'dix-sept': 17, 93 | 'dix-huit': 18, 94 | 'dix-neuf': 19, 95 | 'vingt': 20, 96 | 'trente': 30, 97 | 'quarante': 40, 98 | 'cinquante': 50, 99 | 'soixante': 60, 100 | 'soixante-dix': 70, 101 | 'septante': 70, 102 | 'quatre-vingts': 80, 103 | 'huitante': 80, 104 | 'quatre-vingt-dix': 90, 105 | 'nonante': 90 106 | }, 107 | 'scales': { 108 | 'cent': 100, 109 | 'mille': 1000, 110 | 'un million': 1000000, 111 | 'un milliard': 1000000000, 112 | 'billions de': 1000000000000 113 | } 114 | }, 115 | 'GER': { 116 | 'binary_operators': { 117 | 'plus': '+', 118 | 'geteilt durch': '/', 119 | 'geteilt': '/', 120 | 'minus': '-', 121 | 'mal': '*', 122 | 'multipliziert mit': '*', 123 | 'im Quadrat': '^ 2', 124 | 'hoch zwei': '^ 2', 125 | 'quadriert': '^ 2', 126 | 'cubed': '^ 3', 127 | 'hoch': '^' 128 | }, 129 | 'numbers': { 130 | 'eins': 1, 131 | 'zwei': 2, 132 | 'drei': 3, 133 | 'vier': 4, 134 | 'fünf': 5, 135 | 'sechs': 6, 136 | 'sieben': 7, 137 | 'acht': 8, 138 | 'neun': 9, 139 | 'zehn': 10, 140 | 'elf': 11, 141 | 'zwölf': 12, 142 | 'dreizehn': 13, 143 | 'vierzehn': 14, 144 | 'fünfzehn': 15, 145 | 'sechszehn': 16, 146 | 'siebzehn': 17, 147 | 'achtzehn': 18, 148 | 'neunzehn': 19, 149 | 'zwanzig': 20, 150 | 'dreißig': 30, 151 | 'vierzig': 40, 152 | 'fünfzig': 50, 153 | 'sechzig': 60, 154 | 'siebzig': 70, 155 | 'achtzig': 80, 156 | 'neunzig': 90 157 | }, 158 | 'scales': { 159 | 'hundert': 100, 160 | 'tausend': 1000, 161 | 'hunderttausend': 100000, 162 | 'million': 1000000, 163 | 'milliarde': 1000000000, 164 | 'billion': 1000000000000 165 | } 166 | }, 167 | 'GRE': { 168 | 'unary_operators': { 169 | 'στο τετράγωνο': '^ 2', 170 | 'στον κύβο': '^ 3', 171 | 'τετραγωνική ρίζα του': 'sqrt' 172 | }, 173 | 'binary_operators': { 174 | 'συν': '+', 'και': '+', 175 | 'διά': '/', 176 | 'πλην': '-', 177 | 'επί': '*', 178 | 'στην δύναμη του': '^', 179 | 'εις την': '^' 180 | }, 181 | 'numbers': { 182 | 'μηδέν': 0, 183 | 'ένα': 1, 184 | 'δύο': 2, 185 | 'τρία': 3, 186 | 'τέσσερα': 4, 187 | 'πέντε': 5, 188 | 'έξι': 6, 189 | 'εφτά': 7, 190 | 'οκτώ': 8, 'οχτώ': 8, 191 | 'εννιά': 9, 'εννέα': 9, 192 | 'δέκα': 10, 193 | 'έντεκα': 11, 194 | 'δώδεκα': 12, 195 | 'δεκατρία': 13, 196 | 'δεκατέσσερα': 14, 197 | 'δεκαπέντε': 15, 198 | 'δεκαέξι': 16, 199 | 'δεκαεφτά': 17, 200 | 'δεκαοκτώ': 18, 'δεκαοχτώ': 18, 201 | 'δεκαεννιά': 19, 'δεκαεννέα': 19, 202 | 'είκοσι': 20, 203 | 'τριάντα': 30, 204 | 'σαράντα': 40, 205 | 'πενήντα': 50, 206 | 'εξήντα': 60, 207 | 'εβδομήντα': 70, 208 | 'ογδόντα': 80, 209 | 'ενενήντα': 90 210 | }, 211 | 'scales': { 212 | 'εκατό': 100, 213 | 'χίλια': 1000, 214 | 'εκατομμύρια': 1000000, 'εκ.': 1000000, 215 | 'δισεκατομμύρια': 1000000000, 216 | 'δισ.': 1000000000, 'δις': 1000000000, 217 | 'τρισεκατομμύρια': 1000000000000, 218 | 'τρισ.': 1000000000000, 'τρις': 1000000000000 219 | } 220 | }, 221 | 'ITA': { 222 | 'binary_operators': { 223 | 'più': '+', 224 | 'diviso': '/', 225 | 'meno': '-', 226 | 'per': '*', 227 | 'al quadrato': '^ 2', 228 | 'cubetti': '^ 3', 229 | 'alla potenza di': '^' 230 | }, 231 | 'numbers': { 232 | 'uno': 1, 233 | 'due': 2, 234 | 'tre': 3, 235 | 'quattro': 4, 236 | 'cinque': 5, 237 | 'sei': 6, 238 | 'sette': 7, 239 | 'otto': 8, 240 | 'nove': 9, 241 | 'dieci': 10, 242 | 'undici': 11, 243 | 'dodici': 12, 244 | 'tredici': 13, 245 | 'quattordici': 14, 246 | 'quindici': 15, 247 | 'sedici': 16, 248 | 'diciassette': 17, 249 | 'diciotto': 18, 250 | 'diciannove': 19, 251 | 'venti': 20, 252 | 'trenta': 30, 253 | 'quaranta': 40, 254 | 'cinquanta': 50, 255 | 'sessanta': 60, 256 | 'settanta': 70, 257 | 'ottanta': 80, 258 | 'novanta': 90 259 | }, 260 | 'scales': { 261 | 'centinaia': 100, 262 | 'migliaia': 1000, 263 | 'milioni': 1000000, 264 | 'miliardi': 1000000000, 265 | 'bilioni': 1000000000000 266 | } 267 | }, 268 | 'MAR': { 269 | 'binary_operators': { 270 | 'बेरीज': '+', 271 | 'भागाकार': '/', 272 | 'वजाबाकी': '-', 273 | 'गुणाकार': '*', 274 | '(संख्या)वर्ग': '^ 2', 275 | 'छोटे': '^ 3', 276 | 'गुण्या करण्यासाठी': '^' 277 | }, 278 | 'numbers': { 279 | 'शून्य': '0', 280 | 'एक': '१', 281 | 'दोन': '२', 282 | 'तीन': '३', 283 | 'चार': '४', 284 | 'पाच': '५', 285 | 'सहा': '६', 286 | 'सात': '७', 287 | 'आठ': '८', 288 | 'नऊ': '९', 289 | 'दहा': '१०', 290 | 'अकरा': '११', 291 | 'बारा': '१२', 292 | 'तेरा': '१३', 293 | 'चौदा': '१४', 294 | 'पंधरा': '१५', 295 | 'सोळा': '१६', 296 | 'सतरा': '१७', 297 | 'अठरा': '१८', 298 | 'एकोणीस': '१९', 299 | 'वीस': '२०', 300 | 'तीस': '३०', 301 | 'चाळीस': '४०', 302 | 'पन्नास': '५०', 303 | 'साठ': '६०', 304 | 'सत्तर': '७०', 305 | 'ऐंशी': '८०', 306 | 'नव्वद': '९०', 307 | 'शंभर': '१००' 308 | }, 309 | 'scales': { 310 | 'शंभर': 100, 311 | 'हजार': 1000, 312 | 'दशलक्ष': 1000000, 313 | 'अब्ज': 1000000000, 314 | 'खर्व': 1000000000000 315 | } 316 | }, 317 | 'RUS': { 318 | 'binary_operators': { 319 | 'плюс': '+', 320 | 'разделить': '/', 321 | 'деленное на': '/', 322 | 'делить на': '/', 323 | 'минус': '-', 324 | 'вычесть': '-', 325 | 'отнять': '-', 326 | 'умножить': '*', 327 | 'умноженное на': '*', 328 | 'умножить на': '*', 329 | 'квадрат': '^ 2', 330 | 'в квадрате': '^ 2', 331 | 'возведенный в куб': '^ 3', 332 | 'степень': '^' 333 | }, 334 | 'numbers': { 335 | 'один': 1, 336 | 'два': 2, 337 | 'три': 3, 338 | 'четыре': 4, 339 | 'пять': 5, 340 | 'шесть': 6, 341 | 'семь': 7, 342 | 'восемь': 8, 343 | 'девять': 9, 344 | 'десять': 10, 345 | 'одинадцать': 11, 346 | 'двенадцать': 12, 347 | 'тринадцать': 13, 348 | 'четырнадцать': 14, 349 | 'пятнадцать': 15, 350 | 'шестнадцать': 16, 351 | 'семнадцать': 17, 352 | 'восемнадцать': 18, 353 | 'девятнадцать': 19, 354 | 'двадцать': 20, 355 | 'тридцать': 30, 356 | 'сорок': 40, 357 | 'пятьдесят': 50, 358 | 'шестьдесят': 60, 359 | 'семьдесят': 70, 360 | 'восемьдесят': 80, 361 | 'девяносто': 90 362 | }, 363 | 'scales': { 364 | 'сто': 100, 365 | 'тысяч': 1000, 366 | 'миллион': 1000000, 367 | 'миллиард': 1000000000, 368 | 'триллион': 1000000000000 369 | } 370 | }, 371 | 'POR': { 372 | 'unary_operators': { 373 | 'ao quadrado': '^ 2', 374 | 'ao cubo': '^ 3', 375 | 'raiz quadrada de': 'sqrt' 376 | }, 377 | 'binary_operators': { 378 | 'mais': '+', 379 | 'dividido por': '/', 380 | 'menos': '-', 381 | 'vezes': '*', 382 | 'elevado à potência de': '^' 383 | }, 384 | 'numbers': { 385 | 'zero': 0, 386 | 'um': 1, 387 | 'dois': 2, 388 | 'três': 3, 389 | 'quatro': 4, 390 | 'cinco': 5, 391 | 'seis': 6, 392 | 'sete': 7, 393 | 'oito': 8, 394 | 'nove': 9, 395 | 'dez': 10, 396 | 'onze': 11, 397 | 'doze': 12, 398 | 'treze': 13, 399 | 'quatorze': 14, 400 | 'catorze': 14, 401 | 'quinze': 15, 402 | 'dezesseis': 16, 403 | 'dezessete': 17, 404 | 'dezoito': 18, 405 | 'dezenove': 19, 406 | 'vinte': 20, 407 | 'trinta': 30, 408 | 'quarenta': 40, 409 | 'cinquenta': 50, 410 | 'sessenta': 60, 411 | 'setenta': 70, 412 | 'oitenta': 80, 413 | 'noventa': 90 414 | }, 415 | 'scales': { 416 | 'cem': 100, 417 | 'mil': 1000, 418 | 'milhão': 1000000, 419 | 'bilhão': 1000000000, 420 | 'trilhão': 1000000000000 421 | } 422 | } 423 | } 424 | 425 | 426 | LANGUAGE_CODES = list(MATH_WORDS.keys()) 427 | 428 | 429 | CONSTANTS = { 430 | 'pi': 3.141693, 431 | 'e': 2.718281 432 | } 433 | 434 | 435 | UNARY_FUNCTIONS = { 436 | 'sqrt': math.sqrt, 437 | 438 | # Most people assume a log of base 10 when a base is not specified 439 | 'log': math.log10 440 | } 441 | 442 | 443 | class InvalidLanguageCodeException(Exception): 444 | """ 445 | Exception to be raised when a language code is given that 446 | is not a part of the ISO 639-2 standard. 447 | """ 448 | pass 449 | 450 | 451 | def word_groups_for_language(language_code): 452 | """ 453 | Return the math word groups for a language code. 454 | The language_code should be an ISO 639-2 language code. 455 | https://www.loc.gov/standards/iso639-2/php/code_list.php 456 | """ 457 | 458 | if language_code not in LANGUAGE_CODES: 459 | message = '{} is not an available language code'.format(language_code) 460 | raise InvalidLanguageCodeException(message) 461 | 462 | return MATH_WORDS[language_code] 463 | 464 | 465 | def words_for_language(language_code): 466 | """ 467 | Return the math words for a language code. 468 | The language_code should be an ISO 639-2 language code. 469 | https://www.loc.gov/standards/iso639-2/php/code_list.php 470 | """ 471 | word_groups = word_groups_for_language(language_code) 472 | words = [] 473 | 474 | for group in word_groups: 475 | words.extend(word_groups[group].keys()) 476 | 477 | return words 478 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | packages=[ 7 | "mathparse", 8 | ] 9 | 10 | [tool.setuptools.dynamic] 11 | version = {attr = "mathparse.__version__"} 12 | 13 | [project] 14 | name = "mathparse" 15 | requires-python = ">=3.9,<3.14" 16 | urls = { Documentation = "https://mathparse.chatterbot.us", Repository = "https://github.com/gunthercox/mathparse", Changelog = "https://github.com/gunthercox/mathparse/releases" } 17 | authors = [ 18 | {name = "Gunther Cox"}, 19 | ] 20 | license = "MIT" 21 | description = "mathparse is a library for solving mathematical equations contained in strings" 22 | readme = "README.md" 23 | dynamic = ["version"] 24 | keywords = [ 25 | "mathparse", 26 | "mathematics", 27 | "math", 28 | "nlp", 29 | ] 30 | classifiers = [ 31 | "Development Status :: 4 - Beta", 32 | "Intended Audience :: Developers", 33 | "Operating System :: OS Independent", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "Topic :: Scientific/Engineering :: Mathematics", 36 | "Topic :: Text Processing :: General", 37 | "Topic :: Text Processing :: Linguistic", 38 | "Programming Language :: Python", 39 | ] 40 | 41 | [project.optional-dependencies] 42 | test = [ 43 | "flake8", 44 | "sphinx>=7.4,<9.0" 45 | ] 46 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gunthercox/mathparse/93c77873cc043f3d88d2e6d4f92611700ba38f92/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_binary_operations.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mathparse import mathparse 3 | 4 | 5 | class PositiveIntegerTestCase(TestCase): 6 | 7 | def test_addition(self): 8 | result = mathparse.parse('4 + 4') 9 | 10 | self.assertEqual(result, 8) 11 | 12 | def test_addition_words(self): 13 | result = mathparse.parse('four plus four', language='ENG') 14 | 15 | self.assertEqual(result, 8) 16 | 17 | def test_addition_words_large(self): 18 | result = mathparse.parse( 19 | 'four thousand two hundred one plus five hundred', language='ENG' 20 | ) 21 | 22 | self.assertEqual(result, 4701) 23 | 24 | def test_subtraction(self): 25 | result = mathparse.parse('30 - 20') 26 | 27 | self.assertEqual(result, 10) 28 | 29 | def test_subtraction_words(self): 30 | result = mathparse.parse('thirty minus twenty', language='ENG') 31 | 32 | self.assertEqual(result, 10) 33 | 34 | def test_multiplication(self): 35 | result = mathparse.parse('9 * 9') 36 | 37 | self.assertEqual(result, 81) 38 | 39 | def test_multiplication_words(self): 40 | result = mathparse.parse('nine times nine', language='ENG') 41 | 42 | self.assertEqual(result, 81) 43 | 44 | def test_division(self): 45 | result = mathparse.parse('15 / 5') 46 | 47 | self.assertEqual(result, 3) 48 | 49 | def test_division_words(self): 50 | result = mathparse.parse('fifteen divided by five', language='ENG') 51 | 52 | self.assertEqual(result, 3) 53 | 54 | def test_double_digit_multiplier_for_scale_addition(self): 55 | result = mathparse.parse('fifty thousand plus one', language='ENG') 56 | 57 | self.assertEqual(result, 50001) 58 | 59 | def test_division_by_zero(self): 60 | result = mathparse.parse('42 / 0', language='ENG') 61 | 62 | self.assertEqual(result, 'undefined') 63 | 64 | def test_division_by_zero_words(self): 65 | result = mathparse.parse('six divided by zero', language='ENG') 66 | 67 | self.assertEqual(result, 'undefined') 68 | 69 | def test_division_words_large(self): 70 | result = mathparse.parse( 71 | 'one thousand two hundred four divided by one hundred', 72 | language='ENG' 73 | ) 74 | 75 | self.assertEqual(str(result), '12.04') 76 | 77 | 78 | class PositiveFloatTestCase(TestCase): 79 | 80 | def test_addition(self): 81 | result = mathparse.parse('0.6 + 0.5') 82 | 83 | self.assertEqual(result, 1.1) 84 | 85 | def test_subtraction(self): 86 | result = mathparse.parse('30.1 - 29.1') 87 | 88 | self.assertEqual(result, 1) 89 | 90 | def test_multiplication(self): 91 | result = mathparse.parse('0.9 * 0.9') 92 | 93 | self.assertEqual(result, 0.81) 94 | 95 | def test_division(self): 96 | result = mathparse.parse('0.6 / 0.2') 97 | 98 | self.assertEqual(result, 3) 99 | -------------------------------------------------------------------------------- /tests/test_replace_word_tokens.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mathparse import mathparse 3 | 4 | 5 | class EnglishWordTokenTestCase(TestCase): 6 | 7 | def test_addition(self): 8 | result = mathparse.replace_word_tokens('1 plus 1', language='ENG') 9 | 10 | self.assertEqual(result, '1 + 1') 11 | 12 | def test_thirty(self): 13 | result = mathparse.replace_word_tokens( 14 | 'thirty + thirty', language='ENG' 15 | ) 16 | 17 | self.assertEqual(result, '30 + 30') 18 | 19 | def test_thousand(self): 20 | result = mathparse.replace_word_tokens( 21 | 'five thousand + 30', language='ENG' 22 | ) 23 | 24 | # Note: this ends up with double parentheses because it is both a 25 | # scaled number ("thousand") and a word group ("five thousand") 26 | self.assertEqual(result, '((5 * 1000)) + 30') 27 | 28 | def test_double_digit_multiplier_for_scale(self): 29 | result = mathparse.replace_word_tokens( 30 | 'fifty thousand + 1', language='ENG' 31 | ) 32 | 33 | self.assertEqual(result, '((50 * 1000)) + 1') 34 | -------------------------------------------------------------------------------- /tests/test_unary_operations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from unittest import TestCase 3 | from mathparse import mathparse 4 | 5 | 6 | class UnaryOperatorTestCase(TestCase): 7 | 8 | def test_exponent(self): 9 | result = mathparse.parse('4 ^ 4') 10 | 11 | self.assertEqual(result, 256) 12 | 13 | def test_without_unary_operator_fre(self): 14 | result = mathparse.parse('50 * (85 / 100)', language='FRE') 15 | self.assertEqual(result, 42.5) 16 | 17 | def test_without_unary_operator_rus(self): 18 | result = mathparse.parse('четыре плюс четыре', 19 | language='RUS') 20 | self.assertEqual(result, 8) 21 | 22 | 23 | class UnaryWordOperatorTestCase(TestCase): 24 | 25 | def test_sqrt(self): 26 | result = mathparse.parse('sqrt 4') 27 | 28 | self.assertEqual(result, 2) 29 | 30 | def test_sqrt_parenthesis(self): 31 | result = mathparse.parse('sqrt(4)') 32 | 33 | self.assertEqual(result, 2) 34 | 35 | def test_square_root(self): 36 | result = mathparse.parse('square root of 4', language='ENG') 37 | 38 | self.assertEqual(result, 2) 39 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mathparse import mathparse 3 | 4 | 5 | class BooleanChecksTestCase(TestCase): 6 | 7 | def test_is_integer(self): 8 | self.assertTrue(mathparse.is_int('42')) 9 | 10 | def test_is_not_integer(self): 11 | self.assertFalse(mathparse.is_int('42.2')) 12 | 13 | def test_is_float(self): 14 | self.assertTrue(mathparse.is_float('0.5')) 15 | 16 | def test_is_not_float(self): 17 | self.assertFalse(mathparse.is_float('5')) 18 | 19 | def test_is_constant(self): 20 | self.assertTrue(mathparse.is_constant('pi')) 21 | 22 | def test_is_not_constant(self): 23 | self.assertFalse(mathparse.is_constant('+')) 24 | 25 | def test_is_unary(self): 26 | self.assertTrue(mathparse.is_unary('sqrt')) 27 | 28 | def test_is_not_unary(self): 29 | self.assertFalse(mathparse.is_unary('+')) 30 | 31 | def test_is_binary(self): 32 | self.assertTrue(mathparse.is_binary('-')) 33 | 34 | def test_is_not_binary(self): 35 | self.assertFalse(mathparse.is_binary('sqrt')) 36 | 37 | def test_is_word(self): 38 | self.assertTrue(mathparse.is_word('three', language='ENG')) 39 | 40 | def test_is_not_word(self): 41 | self.assertFalse(mathparse.is_word('3', language='ENG')) 42 | 43 | 44 | class TokenizationTestCase(TestCase): 45 | 46 | def test_lowercase_tokens(self): 47 | result = mathparse.tokenize('Three PLUS five', language='ENG') 48 | 49 | self.assertEqual(result, ['three', 'plus', 'five']) 50 | 51 | def test_load_english_words(self): 52 | from mathparse import mathwords 53 | 54 | words = mathwords.words_for_language('ENG') 55 | self.assertIn('three', words) 56 | 57 | def test_load_nonexistent_data(self): 58 | from mathparse import mathwords 59 | 60 | with self.assertRaises(mathwords.InvalidLanguageCodeException): 61 | mathwords.words_for_language('&&&') 62 | 63 | 64 | class ExtractExpressionTestCase(TestCase): 65 | 66 | def test_empty_string(self): 67 | result = mathparse.extract_expression('', language='ENG') 68 | 69 | self.assertEqual(result, '') 70 | 71 | def test_extract_expression(self): 72 | result = mathparse.extract_expression('3 + 3', language='ENG') 73 | 74 | self.assertEqual(result, '3 + 3') 75 | 76 | def test_ignore_punctuation(self): 77 | result = mathparse.extract_expression('3?', language='ENG') 78 | 79 | self.assertEqual(result, '3') 80 | 81 | def test_extract_expression_simple_additon(self): 82 | result = mathparse.extract_expression('What is 3 + 3?', language='ENG') 83 | 84 | self.assertEqual(result, '3 + 3') 85 | 86 | def test_extract_expression_simple_additon_words(self): 87 | result = mathparse.extract_expression( 88 | 'What is three plus three?', language='ENG' 89 | ) 90 | 91 | self.assertEqual(result, 'three plus three') 92 | --------------------------------------------------------------------------------