├── .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 |
--------------------------------------------------------------------------------