├── src └── pycel │ ├── lib │ ├── __init__.py │ ├── function_info.py │ ├── information.py │ ├── logical.py │ ├── function_helpers.py │ ├── engineering.py │ └── lookup.py │ ├── __init__.py │ ├── version.py │ ├── addin.py │ ├── excellib.py │ └── excelwrapper.py ├── binder ├── runtime.txt ├── postBuild └── requirements.txt ├── MANIFEST.in ├── example ├── example.xlsx └── example.py ├── tests ├── fixtures │ ├── npv.xlsx │ ├── pv.xlsx │ ├── basic.xlsx │ ├── lookup.xlsx │ ├── stats.xlsx │ ├── text.xlsx │ ├── circular.xlsx │ ├── logical.xlsx │ ├── yearfrac.xlsx │ ├── cond-format.xlsx │ ├── date-time.xlsx │ ├── information.xlsx │ ├── excelcompiler.xlsx │ └── fixture.xlsx.yml ├── lib │ ├── test_function_info.py │ ├── test_function_helpers.py │ ├── test_information.py │ ├── test_engineering.py │ ├── test_logical.py │ ├── test_text.py │ └── test_stats.py ├── test_package.py ├── conftest.py └── test_excelwrapper.py ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── test-requirements.txt ├── lib └── pyxll │ └── pyxll.cfg ├── .travis.yml ├── .gitattributes ├── docs ├── Makefile ├── source │ ├── index.rst │ └── conf.py └── make.bat ├── .coveragerc ├── .gitignore ├── tox.ini ├── setup.py ├── README.rst └── CHANGES.rst /src/pycel/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /binder/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | python3 setup.py install 2 | -------------------------------------------------------------------------------- /example/example.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/example/example.xlsx -------------------------------------------------------------------------------- /tests/fixtures/npv.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/npv.xlsx -------------------------------------------------------------------------------- /tests/fixtures/pv.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/pv.xlsx -------------------------------------------------------------------------------- /tests/fixtures/basic.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/basic.xlsx -------------------------------------------------------------------------------- /tests/fixtures/lookup.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/lookup.xlsx -------------------------------------------------------------------------------- /tests/fixtures/stats.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/stats.xlsx -------------------------------------------------------------------------------- /tests/fixtures/text.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/text.xlsx -------------------------------------------------------------------------------- /tests/fixtures/circular.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/circular.xlsx -------------------------------------------------------------------------------- /tests/fixtures/logical.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/logical.xlsx -------------------------------------------------------------------------------- /tests/fixtures/yearfrac.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/yearfrac.xlsx -------------------------------------------------------------------------------- /tests/fixtures/cond-format.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/cond-format.xlsx -------------------------------------------------------------------------------- /tests/fixtures/date-time.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/date-time.xlsx -------------------------------------------------------------------------------- /tests/fixtures/information.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/information.xlsx -------------------------------------------------------------------------------- /tests/fixtures/excelcompiler.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgorissen/pycel/HEAD/tests/fixtures/excelcompiler.xlsx -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] closes #xxxx 2 | - [ ] tests added / passed 3 | - [ ] passes ``tox`` 4 | 5 | -------------------------------------------------------------------------------- /binder/requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | networkx>=2.0,<2.7 3 | numpy 4 | openpyxl>=2.6.2 5 | python-dateutil 6 | pydot 7 | ruamel.yaml 8 | -------------------------------------------------------------------------------- /src/pycel/__init__.py: -------------------------------------------------------------------------------- 1 | from .excelcompiler import ExcelCompiler 2 | from .excelutil import AddressCell, AddressRange, PyCelException 3 | from .version import __version__ 4 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | tox # put this first so it can drive package versions 2 | codecov 3 | flake8-import-order 4 | pytest 5 | pytest-cov 6 | pytest-flake8 7 | restructuredtext-lint 8 | tox-travis 9 | -------------------------------------------------------------------------------- /lib/pyxll/pyxll.cfg: -------------------------------------------------------------------------------- 1 | [PYXLL] 2 | pythonpath = ../../src 3 | modules = pycel.addin 4 | developer_mode = 1 5 | 6 | [LOG] 7 | verbosity = info 8 | format = %(asctime)s - %(levelname)s : %(message)s 9 | path = ./logs 10 | file = pyxll.%(date)s.log 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: focal 3 | 4 | language: python 5 | 6 | python: ["3.6", "3.7", "3.8", "3.9"] 7 | 8 | sudo: false 9 | 10 | install: 11 | - pip install -r test-requirements.txt 12 | 13 | script: 14 | - tox 15 | 16 | after_success: 17 | - codecov 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py text eol=lf 2 | *.sql text eol=lf 3 | *.yml text eol=lf 4 | *.yaml text eol=lf 5 | *.rst text eol=lf 6 | *.sln text eol=lf 7 | *.json text eol=lf 8 | *.rdl text eol=lf 9 | *.mdb binary 10 | *.accdb binary 11 | *.zip binary 12 | *.xlsx binary 13 | 14 | notebooks/* linguist-documentation 15 | -------------------------------------------------------------------------------- /src/pycel/version.py: -------------------------------------------------------------------------------- 1 | # Store the version here so: 2 | # 1) we don't load dependencies by storing it in __init__.py 3 | # 2) we can import it in setup.py for the same reason 4 | # 3) we can import it into your module 5 | # See StackOverflow/458550 for more details 6 | 7 | # uses semantic versioning, http://semver.org 8 | __version__ = '1.0b30' 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Pycel documentation master file, created by 2 | sphinx-quickstart on Sat Dec 22 11:57:41 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PyCel's documentation! 7 | ================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | :caption: Contents: 12 | 13 | 14 | ExcelCompiler 15 | ===================== 16 | 17 | .. autoclass:: excelcompiler.ExcelCompiler 18 | :members: 19 | 20 | 21 | AddressRange 22 | ===================== 23 | 24 | .. autoclass:: excelutil.AddressRange 25 | :members: 26 | 27 | 28 | AddressCell 29 | ===================== 30 | 31 | .. autoclass:: excelutil.AddressCell 32 | :members: 33 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | 3 | [run] 4 | 5 | branch = True 6 | 7 | include = 8 | src/pycel/* 9 | 10 | omit = 11 | */pycel/addin.py 12 | 13 | [report] 14 | 15 | fail_under = 100 16 | skip_covered = True 17 | show_missing = True 18 | 19 | # Regexes for lines to exclude from consideration 20 | exclude_lines = 21 | # Have to re-enable the standard pragma 22 | pragma: no cover 23 | 24 | # Don't complain about missing debug-only code: 25 | def __repr__ 26 | if self\.debug 27 | 28 | # Don't complain if tests don't hit defensive assertion code: 29 | raise AssertionError 30 | 31 | # Don't complain if non-runnable code isn't run: 32 | if 0: 33 | if __name__ == .__main__.: 34 | @abc.abstract 35 | 36 | ignore_errors = True 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | 36 | # for this repository # 37 | *.pyc 38 | *.gexf 39 | *.pickle 40 | 41 | .coverage 42 | .idea/ 43 | .pytest_cache/ 44 | */__pycache__/ 45 | venv/ 46 | .tox/ 47 | *.egg-info/ 48 | 49 | docs/build/ 50 | build/ 51 | dist/ 52 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### What actually happened 4 | 5 | [Please include the full traceback if there was an exception] 6 | 7 | #### What was expected to happen 8 | 9 | 10 | #### Problem description 11 | 12 | [If needed, this should explain **why** the current behavior is a problem and why the expected output is a better solution] 13 | 14 | #### Code Sample 15 | 16 | ```python 17 | # Your code here 18 | 19 | ``` 20 | 21 | [If possible, include a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve) to help us identify the issue. This also helps check that the issue is not with your own code] 22 | 23 | #### Environment 24 | 25 | [Pycel Version, Python Version and OS used. Also any other environment details that you think might be relevant] 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py36, py37, py38, py39, py310 8 | 9 | [testenv] 10 | 11 | setenv = 12 | PYTHONPATH = {toxinidir} 13 | 14 | deps = 15 | pytest 16 | pytest-flake8 17 | pytest-cov 18 | codecov 19 | flake8-import-order 20 | restructuredtext-lint 21 | tox-travis 22 | 23 | commands = 24 | pytest --flake8 --cov {envsitepackagesdir}/pycel --cov-report term-missing 25 | 26 | [flake8] 27 | 28 | application-import-names = pycel 29 | 30 | max-line-length = 100 31 | 32 | import-order-style = smarkets 33 | 34 | select = C,D300,E,F,I,W 35 | 36 | [pytest] 37 | 38 | python_files = tests/*.py 39 | 40 | flake8-ignore = 41 | */pycel/__init__.py F401 42 | */pycel/* W504 43 | */pycel/lib/function_info_data.py E501 44 | -------------------------------------------------------------------------------- /tests/lib/test_function_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | from pycel.lib.function_info import ( 13 | all_excel_functions, 14 | func_status_msg, 15 | function_category, 16 | function_version, 17 | ) 18 | 19 | 20 | def test_function_info(): 21 | assert 'INDEX' in all_excel_functions 22 | assert function_version['INDEX'] == '' 23 | assert function_category['INDEX'] == 'Lookup and reference' 24 | 25 | 26 | @pytest.mark.parametrize( 27 | 'function, known, group, introduced', ( 28 | ('ACOS', True, 'Math and trigonometry', ''), 29 | ('ACOT', True, 'Math and trigonometry', 'Excel 2013'), 30 | ('ACOU', False, '', ''), 31 | ) 32 | ) 33 | def test_func_status_msg(function, known, group, introduced): 34 | is_known, msg = func_status_msg(function) 35 | assert known == is_known 36 | assert group in msg 37 | assert ('not a known' in msg) != (function in all_excel_functions) 38 | 39 | if introduced: 40 | assert introduced in msg 41 | else: 42 | assert 'introduced' not in msg 43 | -------------------------------------------------------------------------------- /src/pycel/addin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Simple Excel addin, requires www.pyxll.com 12 | """ 13 | import os 14 | import webbrowser 15 | 16 | import win32api 17 | import win32com.client 18 | from pyxll import ( 19 | get_active_object, 20 | get_config, 21 | xl_menu 22 | ) 23 | 24 | from pycel import AddressRange, ExcelCompiler 25 | 26 | 27 | @xl_menu("Open log file", menu="PyXLL") 28 | def on_open_logfile(): 29 | # the PyXLL config is accessed as a ConfigParser.ConfigParser object 30 | config = get_config() 31 | if config.has_option("LOG", "path") and config.has_option("LOG", "file"): 32 | path = os.path.join( 33 | config.get("LOG", "path"), config.get("LOG", "file")) 34 | webbrowser.open("file://%s" % path) 35 | 36 | 37 | def xl_app(): 38 | xl_window = get_active_object() 39 | xl_app = win32com.client.Dispatch(xl_window).Application 40 | return xl_app 41 | 42 | 43 | @xl_menu("Compile selection", menu="Pycel") 44 | def compile_selection_menu(): 45 | curfile = xl_app().ActiveWorkbook.FullName 46 | newfile = curfile + ".pickle" 47 | selection = xl_app().Selection 48 | seed = selection.Address 49 | 50 | if not selection or seed.find(',') > 0: 51 | win32api.MessageBox( 52 | 0, "You must select a cell or rectangular range of cells", "Pycel") 53 | return 54 | 55 | res = win32api.MessageBox( 56 | 0, "Going to compile %s to %s starting from %s" % ( 57 | curfile, newfile, seed), "Pycel", 1) 58 | if res == 2: 59 | return 60 | 61 | sp = do_compilation(curfile, seed) 62 | win32api.MessageBox( 63 | 0, "Compilation done, graph has %s nodes and %s edges" % ( 64 | len(sp.dep_graph.nodes()), len(sp.dep_graph.edges())), "Pycel") 65 | 66 | 67 | def do_compilation(fname, seed, sheet=None): 68 | sp = ExcelCompiler(filename=fname) 69 | sp.evaluate(AddressRange(seed, sheet=sheet)) 70 | sp.to_file() 71 | sp.export_to_gexf() 72 | return sp 73 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Simple example file showing how a spreadsheet can be translated to python 12 | and executed 13 | """ 14 | import logging 15 | import os 16 | import sys 17 | 18 | from pycel import ExcelCompiler 19 | 20 | 21 | def pycel_logging_to_console(enable=True): 22 | if enable: 23 | logger = logging.getLogger('pycel') 24 | logger.setLevel('INFO') 25 | 26 | console = logging.StreamHandler(sys.stdout) 27 | console.setLevel(logging.INFO) 28 | logger.addHandler(console) 29 | 30 | 31 | if __name__ == '__main__': 32 | pycel_logging_to_console() 33 | 34 | path = os.path.dirname(__file__) 35 | fname = os.path.join(path, "example.xlsx") 36 | 37 | print(f"Loading {fname}...") 38 | 39 | # load & compile the file to a graph 40 | excel = ExcelCompiler(filename=fname) 41 | 42 | # test evaluation 43 | print(f"D1 is {excel.evaluate('Sheet1!D1')}") 44 | 45 | print("Setting A1 to 200") 46 | excel.set_value('Sheet1!A1', 200) 47 | 48 | print(f"D1 is now {excel.evaluate('Sheet1!D1')} (the same should happen in Excel)") 49 | 50 | # show the graph using matplotlib if installed 51 | print("Plotting using matplotlib...") 52 | try: 53 | excel.plot_graph() 54 | except ImportError: 55 | pass 56 | 57 | # export the graph, can be loaded by a viewer like gephi 58 | print("Exporting to gexf...") 59 | excel.export_to_gexf(fname + ".gexf") 60 | 61 | # As an alternative to using evaluate to put cells in the graph and 62 | # as a way to trim down the size of the file to just that needed. 63 | excel.trim_graph(input_addrs=['Sheet1!A1'], output_addrs=['Sheet1!D1']) 64 | 65 | # As a sanity check, validate that the compiled code can produce 66 | # the current cell values. 67 | assert {} == excel.validate_calcs(output_addrs=['Sheet1!D1']) 68 | 69 | print("Serializing to disk...") 70 | excel.to_file(fname) 71 | 72 | # To reload the file later... 73 | 74 | print("Loading from compiled file...") 75 | excel = ExcelCompiler.from_file(fname) 76 | 77 | # test evaluation 78 | print(f"D1 is {excel.evaluate('Sheet1!D1')}") 79 | 80 | print("Setting A1 to 1") 81 | excel.set_value('Sheet1!A1', 1) 82 | 83 | print(f"D1 is now {excel.evaluate('Sheet1!D1')} (the same should happen in Excel)") 84 | 85 | print("Done") 86 | -------------------------------------------------------------------------------- /tests/test_package.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import os 11 | from distutils.version import LooseVersion 12 | from pathlib import Path 13 | from unittest import mock 14 | 15 | import pytest 16 | import restructuredtext_lint 17 | 18 | import pycel 19 | 20 | repo_root = Path(__file__).parents[1] 21 | 22 | 23 | @pytest.fixture(scope='session') 24 | def changes_rst(): 25 | with open(repo_root / 'CHANGES.rst', 'r') as f: 26 | return f.readlines() 27 | 28 | 29 | @pytest.fixture(scope='session') 30 | def setup_py(): 31 | with mock.patch('setuptools.setup'), mock.patch('setuptools.find_packages'): 32 | cwd = os.getcwd() 33 | os.chdir(repo_root) 34 | import importlib 35 | setup = importlib.import_module('setup') 36 | os.chdir(cwd) 37 | return setup 38 | 39 | 40 | def test_module_version(): 41 | assert pycel.version.__version__ == pycel.__version__ 42 | 43 | 44 | def test_module_version_components(): 45 | loose = LooseVersion(pycel.__version__).version 46 | for component in loose: 47 | assert isinstance(component, int) or component in ('a', 'b', 'rc') 48 | 49 | 50 | def test_docs_versions(changes_rst): 51 | doc_versions = [ 52 | l1.split()[0].strip() for l1, l2 in zip(changes_rst, changes_rst[1:]) 53 | if l2.startswith('===') 54 | ] 55 | 56 | for version in doc_versions: 57 | assert version[0] == '[' 58 | assert version[-1] == ']' 59 | 60 | assert doc_versions[0] == '[unreleased]' 61 | assert pycel.version.__version__ == doc_versions[1][1:-1] 62 | 63 | for v1, v2 in zip(doc_versions[1:], doc_versions[2:]): 64 | assert LooseVersion(v1[1:-1]) > LooseVersion(v2[1:-1]) 65 | 66 | 67 | def test_binder_requirements(setup_py): 68 | binder_reqs_file = '../binder/requirements.txt' 69 | if os.path.exists(binder_reqs_file): 70 | with open(binder_reqs_file, 'r') as f: 71 | binder_reqs = sorted(line.strip() for line in f.readlines()) 72 | 73 | setup_reqs = setup_py.setup.mock_calls[0][2]['install_requires'] 74 | 75 | # the binder requirements also include the optional graphing libs 76 | assert binder_reqs == sorted(setup_reqs + ['matplotlib', 'pydot']) 77 | 78 | 79 | def test_changes_rst(changes_rst, setup_py): 80 | def check_errors(to_check): 81 | return [err for err in to_check if err.level > 1] 82 | 83 | errors = restructuredtext_lint.lint('\n'.join(changes_rst)) 84 | assert not check_errors(errors) 85 | 86 | errors = restructuredtext_lint.lint(setup_py.long_description) 87 | assert not check_errors(errors) 88 | -------------------------------------------------------------------------------- /tests/fixtures/fixture.xlsx.yml: -------------------------------------------------------------------------------- 1 | excel_hash: null 2 | cell_map: 3 | Sheet1!A1: 1 4 | Sheet1!A2: 2 5 | Sheet1!A3: 3 6 | Sheet1!A4: 4 7 | Sheet1!A5: 5 8 | Sheet1!A6: 6 9 | Sheet1!A7: 7 10 | Sheet1!A8: 8 11 | Sheet1!A9: 9 12 | Sheet1!A10: 10 13 | Sheet1!A11: 11 14 | Sheet1!A12: 12 15 | Sheet1!A13: 13 16 | Sheet1!A14: 14 17 | Sheet1!A15: 15 18 | Sheet1!A16: 16 19 | Sheet1!A17: 17 20 | Sheet1!A18: 18 21 | Sheet1!A19: 22 | Sheet1!A20: 23 | Sheet1!B1: =sum_(_R_("Sheet1!A1:A3")) 24 | Sheet1!B2: =sum_(_R_("Sheet1!A2:A4")) 25 | Sheet1!B3: =sum_(_R_("Sheet1!A3:A5")) 26 | Sheet1!B4: =sum_(_R_("Sheet1!A4:A6")) 27 | Sheet1!B5: =sum_(_R_("Sheet1!A5:A7")) 28 | Sheet1!B6: =sum_(_R_("Sheet1!A6:A8")) 29 | Sheet1!B7: =sum_(_R_("Sheet1!A7:A9")) 30 | Sheet1!B8: =sum_(_R_("Sheet1!A8:A10")) 31 | Sheet1!B9: =sum_(_R_("Sheet1!A9:A11")) 32 | Sheet1!B10: =sum_(_R_("Sheet1!A10:A12")) 33 | Sheet1!B11: =sum_(_R_("Sheet1!A11:A13")) 34 | Sheet1!B12: =sum_(_R_("Sheet1!A12:A14")) 35 | Sheet1!B13: =sum_(_R_("Sheet1!A13:A15")) 36 | Sheet1!B14: =sum_(_R_("Sheet1!A14:A16")) 37 | Sheet1!B15: =sum_(_R_("Sheet1!A15:A17")) 38 | Sheet1!B16: =sum_(_R_("Sheet1!A16:A18")) 39 | Sheet1!B17: =sum_(_R_("Sheet1!A17:A19")) 40 | Sheet1!B18: =sum_(_R_("Sheet1!A18:A20")) 41 | Sheet1!C1: =sin(_C_("Sheet1!B1") * (_C_("Sheet1!A1") ** 2)) 42 | Sheet1!C2: =sin(_C_("Sheet1!B2") * (_C_("Sheet1!A2") ** 2)) 43 | Sheet1!C3: =sin(_C_("Sheet1!B3") * (_C_("Sheet1!A3") ** 2)) 44 | Sheet1!C4: =sin(_C_("Sheet1!B4") * (_C_("Sheet1!A4") ** 2)) 45 | Sheet1!C5: =sin(_C_("Sheet1!B5") * (_C_("Sheet1!A5") ** 2)) 46 | Sheet1!C6: =sin(_C_("Sheet1!B6") * (_C_("Sheet1!A6") ** 2)) 47 | Sheet1!C7: =sin(_C_("Sheet1!B7") * (_C_("Sheet1!A7") ** 2)) 48 | Sheet1!C8: =sin(_C_("Sheet1!B8") * (_C_("Sheet1!A8") ** 2)) 49 | Sheet1!C9: =sin(_C_("Sheet1!B9") * (_C_("Sheet1!A9") ** 2)) 50 | Sheet1!C10: =sin(_C_("Sheet1!B10") * (_C_("Sheet1!A10") ** 2)) 51 | Sheet1!C11: =sin(_C_("Sheet1!B11") * (_C_("Sheet1!A11") ** 2)) 52 | Sheet1!C12: =sin(_C_("Sheet1!B12") * (_C_("Sheet1!A12") ** 2)) 53 | Sheet1!C13: =sin(_C_("Sheet1!B13") * (_C_("Sheet1!A13") ** 2)) 54 | Sheet1!C14: =sin(_C_("Sheet1!B14") * (_C_("Sheet1!A14") ** 2)) 55 | Sheet1!C15: =sin(_C_("Sheet1!B15") * (_C_("Sheet1!A15") ** 2)) 56 | Sheet1!C16: =sin(_C_("Sheet1!B16") * (_C_("Sheet1!A16") ** 2)) 57 | Sheet1!C17: =sin(_C_("Sheet1!B17") * (_C_("Sheet1!A17") ** 2)) 58 | Sheet1!C18: =sin(_C_("Sheet1!B18") * (_C_("Sheet1!A18") ** 2)) 59 | Sheet1!D1: =linest(_R_("Sheet1!C1:C18"), _R_("Sheet1!B1:B18"), degree=1)[0] 60 | # Older mappings for excel functions that match Python built-in and keywords 61 | Sheet2!A1: =x_abs(-1) 62 | Sheet2!A2: =x_and(True) 63 | Sheet2!A3: =xatan2(1, 1) 64 | Sheet2!A4: =x_if(1, 2, 3) 65 | Sheet2!A5: =x_int(1.23) 66 | Sheet2!A6: =x_len("Plugh") 67 | Sheet2!A7: =xmax(2, 3) 68 | Sheet2!A8: =x_not(True) 69 | Sheet2!A9: =x_or(True, False) 70 | Sheet2!A10: =xmin(2, 3) 71 | Sheet2!A11: =x_round(2.34) 72 | Sheet2!A12: =xsum(1, 2) 73 | Sheet2!A13: =x_xor(True, True) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Setup script for packaging pycel. 4 | 5 | To release: 6 | Update /src/pycel/version.py, /CHANGES.rst 7 | 8 | Run tests with: 9 | tox 10 | 11 | To build a package for distribution: 12 | python setup.py sdist bdist_wheel 13 | 14 | and upload it to the PyPI with: 15 | twine upload --verbose dist/* 16 | 17 | to install a link for development work: 18 | pip install -e . 19 | 20 | """ 21 | 22 | from setuptools import find_packages, setup 23 | 24 | # see StackOverflow/458550 25 | exec(open('src/pycel/version.py').read()) 26 | 27 | 28 | # Create long description from README.rst and CHANGES.rst. 29 | # PYPI page will contain complete changelog. 30 | def changes(): 31 | """get changes.rst and remove the keep-a-changelog header""" 32 | import itertools as it 33 | import re 34 | 35 | lines = tuple(open('CHANGES.rst', 'r', encoding='utf-8').readlines()) 36 | first_change_re = re.compile(r'^\[\d') 37 | header = tuple(it.takewhile(lambda line: not first_change_re.match(line), lines)) 38 | return lines[len(header):] 39 | 40 | 41 | long_description = u'{}\n\nChange Log\n==========\n\n{}'.format( 42 | open('README.rst', 'r', encoding='utf-8').read(), ''.join(changes())) 43 | 44 | with open('test-requirements.txt') as f: 45 | tests_require = f.readlines() 46 | 47 | 48 | setup( 49 | name='pycel', 50 | version=__version__, # noqa: F821 51 | packages=find_packages('src'), 52 | package_dir={'': 'src'}, 53 | description='A library for compiling excel spreadsheets to python code ' 54 | '& visualizing them as a graph', 55 | keywords='excel compiler formula parser', 56 | url='https://github.com/stephenrauch/pycel', 57 | project_urls={ 58 | # 'Documentation': 'https://pycel.readthedocs.io/en/stable/', 59 | 'Tracker': 'https://github.com/stephenrauch/pycel/issues', 60 | }, 61 | tests_require=tests_require, 62 | test_suite='pytest', 63 | install_requires=[ 64 | 'networkx>=2.0,<2.7', 65 | 'numpy', 66 | 'openpyxl>=2.6.2', 67 | 'python-dateutil', 68 | 'ruamel.yaml', 69 | ], 70 | python_requires='>=3.6', 71 | author='Dirk Gorissen, Stephen Rauch', 72 | author_email='dgorissen@gmail.com', 73 | maintainer='Stephen Rauch', 74 | maintainer_email='stephen.rauch+pycel@gmail.com', 75 | long_description=long_description, 76 | classifiers=[ 77 | 'Development Status :: 4 - Beta', 78 | 'Intended Audience :: Developers', 79 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 80 | 'Operating System :: OS Independent', 81 | 'Programming Language :: Python', 82 | 'Programming Language :: Python :: 3', 83 | 'Programming Language :: Python :: 3.6', 84 | 'Programming Language :: Python :: 3.7', 85 | 'Programming Language :: Python :: 3.8', 86 | 'Programming Language :: Python :: 3.9', 87 | 'Programming Language :: Python :: 3.10', 88 | 'Topic :: Software Development :: Libraries :: Python Modules', 89 | ], 90 | ) 91 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing to Pycel 2 | 3 | Thank you for considering contributing to Pycel\! 4 | 5 | Whether you are a novice or experienced software developer, all contributions and suggestions are welcome. 6 | 7 | ### Couple of things we ask before opening a Github Issue or Pull Request 8 | 9 | #### Have a Support Question? 10 | 11 | * Github is not the best support forum. If you have a support question, [Stack Overflow](https://stackoverflow.com/questions/tagged/pycel?sort=linked) is likely preferable to using the issue tracker. 12 | * Please note that [Stack Overflow](https://stackoverflow.com/questions/tagged/pycel?sort=linked) expects questions about non-working code to include a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve). 13 | 14 | 15 | #### Reporting a bug? 16 | 17 | * Please tell us: 18 | * What you expected to happen. 19 | * What actually happened. Include the full traceback if there was an exception. 20 | * If possible, include a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve) to help us identify the issue. This also helps check that the issue is not with your own code. 21 | * Which version of Pycel you're using. 22 | * Which version of Python you're using. 23 | * Which OS you are using. 24 | * How to reproduce the bug. Bugs with a failing test in a [pull request](https://help.github.com/articles/using-pull-requests) get fixed much quicker. Some bugs may never be fixed. 25 | 26 | * Want to paste some code or output into the issue? Put \`\`\` on a line above and below your code/output. See [GFM](https://help.github.com/articles/github-flavored-markdown)'s *Fenced Code Blocks* for details. 27 | 28 | 29 | #### Suggesting a feature? 30 | 31 | * Do you have an idea for a new feature? 32 | - Fantastic! 33 | - We would love to hear about it, and may consider implementing it. 34 | - But please don't **expect** it to be implemented unless you or someone else sends a [pull request](https://help.github.com/articles/using-pull-requests). 35 | 36 | 37 | #### Submitting a Patch or Feature? 38 | 39 | * Let us know what you plan in the [GitHub Issue tracker](https://github.com/dgorissen/pycel/issues) so we can provide feedback. 40 | * We love [pull requests](https://help.github.com/articles/using-pull-requests). But if you don't have any tests to go with it, it is much less likely to be merged. 41 | * When fixing a bug, please explain clearly under which circumstances the bug happens. Also please provide a failing test case that your patch solves, and make sure the test fails without your patch. 42 | * The [pytest]( http://pytest.org/latest/) unit tests use [flake8](https://pypi.python.org/pypi/flake8) and will fail if certain aspects of [PEP8](https://www.python.org/dev/peps/pep-0008/) are not followed. 43 | * The [CI tests](https://travis-ci.com) use [tox](http://testrun.org/tox/latest/) and can fail if you have not tested in all supported versions. 44 | * Open a [GitHub Pull Request](https://github.com/dgorissen/pycel/pulls) with your patches and we will review your contribution and respond as quickly as possible. Please keep in mind that this is an open source project, and it may take us some time to get back to you. Your patience is very much appreciated. 45 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import os 11 | import shutil 12 | from unittest import mock 13 | 14 | import pytest 15 | from openpyxl.utils import column_index_from_string 16 | 17 | from pycel.excelcompiler import ExcelCompiler 18 | from pycel.excelutil import AddressCell 19 | from pycel.excelwrapper import ExcelOpxWrapper as ExcelWrapperImpl 20 | 21 | 22 | @pytest.fixture(scope='session') 23 | def ATestCell(): 24 | 25 | class ATestCell: 26 | 27 | def __init__(self, col, row, sheet='', excel=None, value=None): 28 | self.row = row 29 | self.col = col 30 | self.col_idx = column_index_from_string(col) 31 | self.sheet = sheet 32 | self.excel = excel 33 | self.address = AddressCell(f'{col}{row}', sheet=sheet) 34 | self.value = value 35 | 36 | return ATestCell 37 | 38 | 39 | @pytest.fixture(scope='session') 40 | def fixture_dir(): 41 | return os.path.join(os.path.dirname(__file__), 'fixtures') 42 | 43 | 44 | @pytest.fixture(scope='session') 45 | def tmpdir(tmpdir_factory): 46 | return tmpdir_factory.mktemp('fixtures') 47 | 48 | 49 | @pytest.fixture(scope='session') 50 | def serialization_override_path(tmpdir): 51 | return os.path.join(str(tmpdir), 'excelcompiler_serialized.yml') 52 | 53 | 54 | def copy_fixture_xls_path(fixture_dir, tmpdir, filename): 55 | src = os.path.join(fixture_dir, filename) 56 | dst = os.path.join(str(tmpdir), filename) 57 | shutil.copy(src, dst) 58 | return dst 59 | 60 | 61 | @pytest.fixture(scope='session') 62 | def fixture_xls_copy(fixture_dir, tmpdir): 63 | def wrapped(filename): 64 | return copy_fixture_xls_path(fixture_dir, tmpdir, filename) 65 | return wrapped 66 | 67 | 68 | @pytest.fixture(scope='session') 69 | def fixture_xls_path(fixture_xls_copy): 70 | return fixture_xls_copy('excelcompiler.xlsx') 71 | 72 | 73 | @pytest.fixture(scope='session') 74 | def fixture_xls_path_circular(fixture_xls_copy): 75 | return fixture_xls_copy('circular.xlsx') 76 | 77 | 78 | @pytest.fixture(scope='session') 79 | def unconnected_excel(fixture_xls_path): 80 | import openpyxl.worksheet._reader as orw 81 | old_warn = orw.warn 82 | 83 | def new_warn(msg, *args, **kwargs): 84 | if 'Unknown' not in msg: 85 | old_warn(msg, *args, **kwargs) 86 | 87 | # quiet the warnings about unknown extensions 88 | with mock.patch('openpyxl.worksheet._reader.warn', new_warn): 89 | yield ExcelWrapperImpl(fixture_xls_path) 90 | 91 | 92 | @pytest.fixture 93 | def excel(unconnected_excel): 94 | unconnected_excel.load() 95 | return unconnected_excel 96 | 97 | 98 | @pytest.fixture(scope='session') 99 | def basic_ws(fixture_xls_copy): 100 | return ExcelCompiler(fixture_xls_copy('basic.xlsx')) 101 | 102 | 103 | @pytest.fixture(scope='session') 104 | def cond_format_ws(fixture_xls_copy): 105 | return ExcelCompiler(fixture_xls_copy('cond-format.xlsx')) 106 | 107 | 108 | @pytest.fixture 109 | def circular_ws(fixture_xls_path_circular): 110 | return ExcelCompiler(fixture_xls_path_circular, cycles=True) 111 | 112 | 113 | @pytest.fixture 114 | def excel_compiler(excel): 115 | return ExcelCompiler(excel=excel) 116 | -------------------------------------------------------------------------------- /src/pycel/lib/function_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import os 11 | 12 | from pycel.lib.function_info_data import function_info 13 | 14 | # set of all function names 15 | all_excel_functions = frozenset(f.name for f in function_info) 16 | 17 | # function name to excel version and function category 18 | function_version = {f.name: f.version for f in function_info} 19 | function_category = {f.name: f.category for f in function_info} 20 | base_url = 'https://support.microsoft.com/en-us/office/' 21 | 22 | 23 | def func_status_msg(name): 24 | """Return a string with info about an excel function""" 25 | name = name.upper() 26 | known = name in all_excel_functions 27 | if known: 28 | msg = f'{name} is in the "{function_category[name]}" group' 29 | version = function_version[name] 30 | if version: 31 | msg += f', and was introduced in {version}' 32 | else: 33 | msg = f'{name} is not a known Excel function' 34 | return known, msg 35 | 36 | 37 | def scrape_function_list(): # pragma: no cover 38 | """Development Code to scrape web for list of excel functions 39 | builds: function_info_data.py 40 | """ 41 | import requests 42 | from bs4 import BeautifulSoup 43 | from urllib.parse import urlparse 44 | 45 | base_dir = os.path.dirname(__file__) 46 | tmp_data_name = 'tmp_function_list_page' 47 | 48 | from_web = True 49 | if from_web: 50 | url = base_url + 'Excel-functions-alphabetical-' \ 51 | 'b3944572-255d-4efb-bb96-c6d90033e188' 52 | 53 | page = requests.get(url) 54 | soup = BeautifulSoup(page.text, 'html.parser') 55 | 56 | # temporarily save page for further testing 57 | tmp_data_py = os.path.join(base_dir, tmp_data_name + '.py') 58 | with open(tmp_data_py, 'wb') as f: 59 | f.write(f'page_html = """{page.text}"""') 60 | 61 | else: 62 | import importlib 63 | web_data = importlib.import_module(f'pycel.lib.{tmp_data_name}') 64 | soup = BeautifulSoup(web_data.page_html, 'html.parser') 65 | 66 | base_url_path = urlparse(base_url).path 67 | table = max(((len(table.find_all('tr')), table) 68 | for table in soup.find_all('table')))[1] 69 | 70 | rows_data = [] 71 | for row in table.find_all('tr'): 72 | p = tuple(p.text for p in row.find_all('p')) 73 | if ':' not in p[1]: 74 | continue 75 | href = tuple(a.get('href') for a in row.find_all('a')) 76 | row_url = href[-1].replace(base_url_path, '') 77 | name = p[0].strip().replace(' function', '') 78 | category, description = (x.strip() for x in p[1].split(':', 1)) 79 | version = '' 80 | for img in row.find_all('img'): 81 | version = img['alt'].replace(' button', '') 82 | if version.startswith('20') and len(version) == 4: 83 | version = f'Excel {version}' 84 | rows_data.append((name, category, version, row_url, description)) 85 | 86 | with open(os.path.join(base_dir, 'function_info_data.py'), 'w') as f: 87 | f.write("import collections\n\n") 88 | f.write("FunctionInfo = collections.namedtuple(\n") 89 | f.write(" 'FunctionInfo', 'name category version uri')\n\n") 90 | f.write('function_info = (\n') 91 | for row_data in rows_data: 92 | for name in row_data[0].split(','): 93 | f.write(" FunctionInfo('{}', '{}', '{}', '{}'),\n".format( 94 | name.strip().rstrip('s'), *row_data[1:])) 95 | f.write(')\n') 96 | 97 | 98 | def print_function_header(): # pragma: no cover 99 | """Development Code to generate sample function header stubs""" 100 | from .function_info_data import function_info 101 | print() 102 | for row in function_info: 103 | if row[1].startswith('Math'): 104 | print(f"# def {row.name.lower()}(value):") 105 | print(f" # Excel reference: {base_url}") 106 | print(f" # {row.uri}") 107 | print() 108 | print() 109 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pycel 2 | ===== 3 | 4 | |build-state| |coverage| |requirements| 5 | 6 | |pypi| |pypi-pyversions| |repo-size| |code-size| 7 | 8 | Pycel is a small python library that can translate an Excel spreadsheet into 9 | executable python code which can be run independently of Excel. 10 | 11 | The python code is based on a graph and uses caching & lazy evaluation to 12 | ensure (relatively) fast execution. The graph can be exported and analyzed 13 | using tools like `Gephi `_. See the contained example 14 | for an illustration. 15 | 16 | Required python libraries: 17 | `dateutil `_, 18 | `networkx `_, 19 | `numpy `_, 20 | `openpyxl `_, 21 | `ruamel.yaml `_, and optionally: 22 | `matplotlib `_, 23 | `pydot `_ 24 | 25 | The full motivation behind pycel including some examples & screenshots is 26 | described in this `blog post `_. 27 | 28 | Usage 29 | ====== 30 | 31 | Download the library and run the example file. 32 | 33 | **Quick start:** 34 | You can use binder to see and explore the tool quickly and interactively in the 35 | browser: |notebook| 36 | 37 | **The good:** 38 | 39 | All the main mathematical functions (sin, cos, atan2, ...) and operators 40 | (+,/,^, ...) are supported as are ranges (A5:D7), and functions like 41 | MIN, MAX, INDEX, LOOKUP, and LINEST. 42 | 43 | The codebase is small, relatively fast and should be easy to understand 44 | and extend. 45 | 46 | I have tested it extensively on spreadsheets with 10 sheets & more than 47 | 10000 formulae. In that case calculation of the equations takes about 50ms 48 | and agrees with Excel up to 5 decimal places. 49 | 50 | **The bad:** 51 | 52 | My development is driven by the particular spreadsheets I need to handle so 53 | I have only added support for functions that I need. However, it is should be 54 | straightforward to add support for others. 55 | 56 | The code does currently support cell references so a function like OFFSET works, 57 | but suffers from the fact that if a cell is not already compiled in, then the 58 | function can fail. Also, for obvious reasons, any VBA code is not compiled 59 | but needs to be re-implemented manually on the python side. 60 | 61 | **The Ugly:** 62 | 63 | The resulting graph-based code is fast enough for my purposes but to make it 64 | truly fast you would probably replace the graph with a dependency tracker 65 | based on sparse matrices or something similar. 66 | 67 | Excel Addin 68 | =========== 69 | 70 | It's possible to run pycel as an excel addin using 71 | `PyXLL `_. Simply place pyxll.xll and pyxll.py in the 72 | lib directory and add the xll file to the Excel Addins list as explained in 73 | the pyxll documentation. 74 | 75 | Acknowledgements 76 | ================ 77 | 78 | This code was originally made possible thanks to the python port of 79 | Eric Bachtal's `Excel formula parsing code 80 | `_ 81 | by Robin Macharg. 82 | 83 | The code currently uses a tokenizer of similar origin from the 84 | `openpyxl library. 85 | `_ 86 | 87 | .. Image links 88 | 89 | .. |build-state| image:: https://travis-ci.com/dgorissen/pycel.svg?branch=master 90 | :target: https://travis-ci.com/dgorissen/pycel 91 | :alt: Build Status 92 | 93 | .. |coverage| image:: https://codecov.io/gh/dgorissen/pycel/branch/master/graph/badge.svg 94 | :target: https://codecov.io/gh/dgorissen/pycel/list/master 95 | :alt: Code Coverage 96 | 97 | .. |pypi| image:: https://img.shields.io/pypi/v/pycel.svg 98 | :target: https://pypi.org/project/pycel/ 99 | :alt: Latest Release 100 | 101 | .. |pypi-pyversions| image:: https://img.shields.io/pypi/pyversions/pycel.svg 102 | :target: https://pypi.python.org/pypi/pycel 103 | 104 | .. |requirements| image:: https://requires.io/github/stephenrauch/pycel/requirements.svg?branch=master 105 | :target: https://requires.io/github/stephenrauch/pycel/requirements/?branch=master 106 | :alt: Requirements Status 107 | 108 | .. |repo-size| image:: https://img.shields.io/github/repo-size/dgorissen/pycel.svg 109 | :target: https://github.com/dgorissen/pycel 110 | :alt: Repo Size 111 | 112 | .. |code-size| image:: https://img.shields.io/github/languages/code-size/dgorissen/pycel.svg 113 | :target: https://github.com/dgorissen/pycel 114 | :alt: Code Size 115 | 116 | .. |notebook| image:: https://mybinder.org/badge.svg 117 | :target: https://mybinder.org/v2/gh/dgorissen/pycel/master?filepath=notebooks%2Fexample.ipynb 118 | :alt: Open Notebook 119 | -------------------------------------------------------------------------------- /tests/lib/test_function_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import importlib 11 | import math 12 | 13 | import pytest 14 | 15 | from pycel.excelutil import ( 16 | AddressCell, 17 | AddressRange, 18 | DIV0, 19 | NUM_ERROR, 20 | VALUE_ERROR, 21 | ) 22 | from pycel.lib.function_helpers import ( 23 | apply_meta, 24 | cse_array_wrapper, 25 | error_string_wrapper, 26 | excel_helper, 27 | excel_math_func, 28 | load_functions, 29 | ) 30 | 31 | 32 | DATA = ( 33 | (1, 2), 34 | (3, 4), 35 | ) 36 | 37 | 38 | @pytest.mark.parametrize( 39 | 'arg_num, f_args, result', ( 40 | (0, (1,), 2), 41 | (1, (0, 1), 2), 42 | (0, (DATA, ), ((2, 3), (4, 5))), 43 | (1, (1, DATA), ((2, 3), (4, 5))), 44 | ) 45 | ) 46 | def test_cse_array_wrapper(arg_num, f_args, result): 47 | 48 | def f_test(*args): 49 | return args[arg_num] + 1 50 | 51 | assert cse_array_wrapper(f_test, arg_num)(*f_args) == result 52 | 53 | 54 | @pytest.mark.parametrize( 55 | 'arg_nums, f_args, result', ( 56 | (((0, 1, 2, 3)), ((0, NUM_ERROR), (DIV0, NUM_ERROR)), NUM_ERROR), 57 | ((0, 1), (DIV0, 1), DIV0), 58 | ((0, 1), (1, DIV0), DIV0), 59 | ((0, 1), (NUM_ERROR, DIV0), NUM_ERROR), 60 | ((0, 1), (DIV0, NUM_ERROR), DIV0), 61 | ((0,), (1, DIV0), "args: (1, '#DIV/0!')"), 62 | ((1,), (1, DIV0), DIV0), 63 | ) 64 | ) 65 | def test_error_string_wrapper(arg_nums, f_args, result): 66 | 67 | def f_test(*args): 68 | return f'args: {args}' 69 | 70 | assert error_string_wrapper(f_test, arg_nums)(*f_args) == result 71 | 72 | 73 | @pytest.mark.parametrize( 74 | 'value, result', ( 75 | (1, 1), 76 | (DIV0, DIV0), 77 | (None, 0), 78 | ('1.1', 1.1), 79 | ('xyzzy', VALUE_ERROR), 80 | ) 81 | ) 82 | def test_math_wrap(value, result): 83 | assert apply_meta( 84 | excel_math_func(lambda x: x), name_space={})[0](value) == result 85 | 86 | 87 | def test_math_wrap_domain_error(): 88 | func = apply_meta(excel_math_func(lambda x: math.log(x)), name_space={})[0] 89 | assert func(-1) == NUM_ERROR 90 | 91 | 92 | @pytest.mark.parametrize( 93 | 'value, result', ( 94 | ((1, 2, 3), (1, 2, 3)), 95 | ((AddressRange('A1:B1'),) * 3, 96 | ('R:A1:B1', AddressRange('A1:B1'), 'R:A1:B1')), 97 | ((AddressCell('A1'),) * 3, 98 | ('C:A1', AddressCell('A1'), 'C:A1')), 99 | ) 100 | ) 101 | def test_ref_wrap(value, result): 102 | def r_test(*args): 103 | return args 104 | 105 | name_space = locals() 106 | name_space['_R_'] = lambda a: f'R:{a}' 107 | name_space['_C_'] = lambda a: f'C:{a}' 108 | 109 | func = apply_meta( 110 | excel_helper(ref_params=1)(r_test), name_space=name_space)[0] 111 | assert func(*value) == result 112 | 113 | 114 | def test_apply_meta_nothing_active(): 115 | 116 | def a_test_func(x): 117 | return x 118 | 119 | func = apply_meta(excel_helper( 120 | err_str_params=None, ref_params=-1)(a_test_func), name_space={})[0] 121 | assert func == a_test_func 122 | 123 | 124 | def test_apply_meta_kwargs(): 125 | 126 | def a_test_func(**x): 127 | pass 128 | 129 | with pytest.raises(RuntimeError, 130 | match=r'Function a_test_func: \*\*kwargs not allowed in signature'): 131 | apply_meta(excel_helper()(a_test_func)) 132 | 133 | 134 | def test_load_functions(): 135 | 136 | modules = ( 137 | importlib.import_module('pycel.excellib'), 138 | importlib.import_module('pycel.lib.date_time'), 139 | importlib.import_module('pycel.lib.logical'), 140 | importlib.import_module('math'), 141 | ) 142 | 143 | namespace = locals() 144 | 145 | names = 'degrees if_ junk'.split() 146 | missing = load_functions(names, namespace, modules) 147 | assert missing == {'junk'} 148 | assert 'degrees' in namespace 149 | assert 'if_' in namespace 150 | 151 | names = 'radians if_ junk'.split() 152 | missing = load_functions(names, namespace, modules) 153 | assert missing == {'junk'} 154 | assert 'radians' in namespace 155 | 156 | assert namespace['radians'](180) == math.pi 157 | assert namespace['radians'](((180, 360),)) == ((math.pi, 2 * math.pi),) 158 | 159 | assert namespace['if_'](0, 'Y', 'N') == 'N' 160 | assert namespace['if_'](((0, 1),), 'Y', 'N') == (('N', 'Y'),) 161 | 162 | missing = load_functions(['log'], namespace, modules) 163 | assert not missing 164 | assert namespace['log'](DIV0) == DIV0 165 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../../src/pycel')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'Pycel' 23 | copyright = '2011, Dirk Gorissen & 2018, Stephen Rauch' 24 | author = 'Dirk Gorissen, Stephen Rauch' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.ifconfig', 44 | 'sphinx.ext.viewcode', 45 | 'sphinx.ext.githubpages', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # 63 | # This is also used if you do content translation via gettext catalogs. 64 | # Usually you set "language" from the command line for these cases. 65 | language = None 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = [] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = None 74 | 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | html_theme = 'alabaster' 82 | 83 | # Theme options are theme-specific and customize the look and feel of a theme 84 | # further. For a list of options available for each theme, see the 85 | # documentation. 86 | # 87 | # html_theme_options = {} 88 | 89 | # Add any paths that contain custom static files (such as style sheets) here, 90 | # relative to this directory. They are copied after the builtin static files, 91 | # so a file named "default.css" will overwrite the builtin "default.css". 92 | # html_static_path = ['_static'] 93 | 94 | # Custom sidebar templates, must be a dictionary that maps document names 95 | # to template names. 96 | # 97 | # The default sidebars (for documents that don't match any pattern) are 98 | # defined by theme itself. Builtin themes are using these templates by 99 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 100 | # 'searchbox.html']``. 101 | # 102 | # html_sidebars = {} 103 | 104 | 105 | # -- Options for HTMLHelp output --------------------------------------------- 106 | 107 | # Output file base name for HTML help builder. 108 | htmlhelp_basename = 'Pyceldoc' 109 | 110 | 111 | # -- Options for LaTeX output ------------------------------------------------ 112 | 113 | latex_elements = { 114 | # The paper size ('letterpaper' or 'a4paper'). 115 | # 116 | # 'papersize': 'letterpaper', 117 | 118 | # The font size ('10pt', '11pt' or '12pt'). 119 | # 120 | # 'pointsize': '10pt', 121 | 122 | # Additional stuff for the LaTeX preamble. 123 | # 124 | # 'preamble': '', 125 | 126 | # Latex figure (float) alignment 127 | # 128 | # 'figure_align': 'htbp', 129 | } 130 | 131 | 132 | # -- Options for manual page output ------------------------------------------ 133 | 134 | # One entry per manual page. List of tuples 135 | # (source start file, name, description, authors, manual section). 136 | man_pages = [ 137 | (master_doc, 'pycel', 'Pycel Documentation', 138 | [author], 1) 139 | ] 140 | 141 | 142 | # -- Options for Texinfo output ---------------------------------------------- 143 | 144 | # Grouping the document tree into Texinfo files. List of tuples 145 | # (source start file, target name, title, author, 146 | # dir menu entry, description, category) 147 | texinfo_documents = [ 148 | (master_doc, 'Pycel', 'Pycel Documentation', 149 | author, 'Pycel', 'One line description of project.', 150 | 'Miscellaneous'), 151 | ] 152 | 153 | 154 | # -- Options for Epub output ------------------------------------------------- 155 | 156 | # Bibliographic Dublin Core info. 157 | epub_title = project 158 | 159 | # The unique identifier of the text. This can be a ISBN number 160 | # or the project homepage. 161 | # 162 | # epub_identifier = '' 163 | 164 | # A unique identification for the text. 165 | # 166 | # epub_uid = '' 167 | 168 | # A list of files that should not be packed into the epub file. 169 | epub_exclude_files = ['search.html'] 170 | 171 | 172 | # -- Extension configuration ------------------------------------------------- 173 | -------------------------------------------------------------------------------- /src/pycel/lib/information.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Python equivalents of Information library functions 12 | """ 13 | import math 14 | 15 | from pycel.excelutil import ( 16 | coerce_to_number, 17 | ERROR_CODES, 18 | is_address, 19 | NA_ERROR, 20 | VALUE_ERROR, 21 | ) 22 | from pycel.lib.function_helpers import excel_helper 23 | 24 | 25 | CELL_INFO_TYPE = ['contents'] 26 | 27 | 28 | @excel_helper(cse_params=0, ref_params=1, str_params=0) 29 | def cell(info_type, ref): 30 | # Excel reference: https://support.microsoft.com/en-us/office/ 31 | # cell-function-51bd39a5-f338-4dbe-a33f-955d67c2b2cf 32 | if info_type not in CELL_INFO_TYPE: 33 | raise NotImplementedError(f"CELL function \'{info_type}\' info_type is not implemented!") 34 | 35 | if not is_address(ref): 36 | return ref 37 | else: 38 | if ref.is_range: 39 | current_cell = ref.start 40 | else: 41 | current_cell = ref 42 | 43 | _C_ = cell.excel_func_meta['name_space']['_C_'] 44 | return _C_(current_cell.address) 45 | 46 | 47 | # def error.type(value): 48 | # # Excel reference: https://support.microsoft.com/en-us/office/ 49 | # # error-type-function-10958677-7c8d-44f7-ae77-b9a9ee6eefaa 50 | 51 | 52 | # def info(value): 53 | # # Excel reference: https://support.microsoft.com/en-us/office/ 54 | # # info-function-725f259a-0e4b-49b3-8b52-58815c69acae 55 | 56 | 57 | @excel_helper(cse_params=0, err_str_params=None) 58 | def isblank(value): 59 | # Excel reference: https://support.microsoft.com/en-us/office/ 60 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 61 | return value is None 62 | 63 | 64 | @excel_helper(cse_params=0, err_str_params=None) 65 | def iserr(value): 66 | # Excel reference: https://support.microsoft.com/en-us/office/ 67 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 68 | # Value refers to any error value except #N/A. 69 | return isinstance(value, str) and value in ERROR_CODES and value != NA_ERROR 70 | 71 | 72 | @excel_helper(cse_params=0, err_str_params=None) 73 | def iserror(value): 74 | # Excel reference: https://support.microsoft.com/en-us/office/ 75 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 76 | # Value refers to any error value: 77 | # (#N/A, #VALUE!, #REF!, #DIV/0!, #NUM!, #NAME?, or #NULL!). 78 | return isinstance(value, str) and value in ERROR_CODES or ( 79 | isinstance(value, tuple)) 80 | 81 | 82 | @excel_helper(cse_params=0) 83 | def iseven(value): 84 | # Excel reference: https://support.microsoft.com/en-us/office/ 85 | # iseven-function-aa15929a-d77b-4fbb-92f4-2f479af55356 86 | result = isodd(value) 87 | return not result if isinstance(result, bool) else result 88 | 89 | 90 | # def isformula(value): 91 | # # Excel reference: https://support.microsoft.com/en-us/office/ 92 | # # isformula-function-e4d1355f-7121-4ef2-801e-3839bfd6b1e5 93 | 94 | 95 | @excel_helper(cse_params=0, err_str_params=None) 96 | def islogical(value): 97 | # Excel reference: https://support.microsoft.com/en-us/office/ 98 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 99 | return isinstance(value, bool) 100 | 101 | 102 | @excel_helper(cse_params=0, err_str_params=None) 103 | def isna(value): 104 | # Excel reference: https://support.microsoft.com/en-us/office/ 105 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 106 | return value == NA_ERROR or isinstance(value, tuple) 107 | 108 | 109 | @excel_helper(cse_params=0, err_str_params=None) 110 | def isnontext(value): 111 | # Excel reference: https://support.microsoft.com/en-us/office/ 112 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 113 | return not isinstance(value, str) or value in ERROR_CODES 114 | 115 | 116 | @excel_helper(cse_params=0, err_str_params=None) 117 | def isnumber(value): 118 | # Excel reference: https://support.microsoft.com/en-us/office/ 119 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 120 | return not isinstance(value, bool) and isinstance(value, (int, float)) 121 | 122 | 123 | @excel_helper(cse_params=0) 124 | def isodd(value): 125 | # Excel reference: https://support.microsoft.com/en-us/office/ 126 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 127 | if isinstance(value, bool): 128 | return VALUE_ERROR 129 | value = coerce_to_number(value) 130 | if isinstance(value, str): 131 | return VALUE_ERROR 132 | if value is None: 133 | value = 0 134 | return bool(math.floor(abs(value)) % 2) 135 | 136 | 137 | # def isref(value): 138 | # # Excel reference: https://support.microsoft.com/en-us/office/ 139 | # # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 140 | 141 | 142 | @excel_helper(cse_params=0, err_str_params=None) 143 | def istext(arg): 144 | # Excel reference: https://support.microsoft.com/en-us/office/ 145 | # is-functions-0f2d7971-6019-40a0-a171-f2d869135665 146 | return isinstance(arg, str) and arg not in ERROR_CODES 147 | 148 | 149 | @excel_helper(cse_params=0, err_str_params=0) 150 | def n(value): 151 | # Excel reference: https://support.microsoft.com/en-us/office/ 152 | # n-function-a624cad1-3635-4208-b54a-29733d1278c9 153 | if isinstance(value, str): 154 | return 0 155 | if isinstance(value, bool): 156 | return int(value) 157 | return value 158 | 159 | 160 | def na(): 161 | # Excel reference: https://support.microsoft.com/en-us/office/ 162 | # na-function-5469c2d1-a90c-4fb5-9bbc-64bd9bb6b47c 163 | return NA_ERROR 164 | 165 | 166 | # def sheet(value): 167 | # # Excel reference: https://support.microsoft.com/en-us/office/ 168 | # # sheet-function-44718b6f-8b87-47a1-a9d6-b701c06cff24 169 | 170 | 171 | # def sheets(value): 172 | # # Excel reference: https://support.microsoft.com/en-us/office/ 173 | # # sheets-function-770515eb-e1e8-45ce-8066-b557e5e4b80b 174 | 175 | 176 | # def type(value): 177 | # # Excel reference: https://support.microsoft.com/en-us/office/ 178 | # # type-function-45b4e688-4bc3-48b3-a105-ffa892995899 179 | -------------------------------------------------------------------------------- /tests/lib/test_information.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | import pycel.excellib 13 | from pycel.excelcompiler import ExcelCompiler 14 | from pycel.excelutil import ( 15 | AddressCell, 16 | AddressRange, 17 | DIV0, 18 | NA_ERROR, 19 | NUM_ERROR, 20 | REF_ERROR, 21 | VALUE_ERROR, 22 | ) 23 | from pycel.lib.function_helpers import load_to_test_module 24 | from pycel.lib.information import ( 25 | cell, 26 | isblank, 27 | iserr, 28 | iserror, 29 | iseven, 30 | islogical, 31 | isna, 32 | isnontext, 33 | isnumber, 34 | isodd, 35 | istext, 36 | n, 37 | na, 38 | ) 39 | 40 | 41 | # dynamic load the lib functions from excellib and apply metadata 42 | load_to_test_module(pycel.lib.information, __name__) 43 | 44 | 45 | def test_information_ws(fixture_xls_copy): 46 | compiler = ExcelCompiler(fixture_xls_copy('information.xlsx')) 47 | result = compiler.validate_serialized() 48 | assert result == {} 49 | 50 | 51 | @pytest.mark.parametrize( 52 | 'reference, info_type, expected', ( 53 | ('value', 'address', 'not implemented'), 54 | ('value', 'contents', 'value'), 55 | ('Information!A11', 'contents', 0), 56 | ('Information!A11:A12', 'contents', 0), 57 | ) 58 | ) 59 | def test_cell(reference, info_type, expected): 60 | assert isinstance(reference, str) 61 | if reference.find('!') == -1: 62 | refer = reference 63 | else: 64 | if reference.find(':') == -1: 65 | refer = AddressCell.create(reference) 66 | else: 67 | refer = AddressRange.create(reference) 68 | 69 | result = 'not implemented' 70 | try: 71 | result = cell(info_type, refer) 72 | except NotImplementedError: 73 | pass 74 | assert expected == result 75 | 76 | 77 | @pytest.mark.parametrize( 78 | 'value, expected', ( 79 | (None, True), 80 | (0, False), 81 | (1, False), 82 | (1.0, False), 83 | (-1, False), 84 | ('a', False), 85 | (True, False), 86 | (False, False), 87 | ) 88 | ) 89 | def test_isblank(value, expected): 90 | assert isblank(value) == expected 91 | 92 | 93 | @pytest.mark.parametrize( 94 | 'value, expected', ( 95 | (0, False), 96 | (1, False), 97 | (1.0, False), 98 | (-1, False), 99 | ('a', False), 100 | (((1, NUM_ERROR), ('2', DIV0)), ((False, True), (False, True))), 101 | (NA_ERROR, False), 102 | (NUM_ERROR, True), 103 | (REF_ERROR, True), 104 | ) 105 | ) 106 | def test_iserr(value, expected): 107 | assert iserr(value) == expected 108 | 109 | 110 | @pytest.mark.parametrize( 111 | '_iseven, _isodd, value', ( 112 | (True, False, -100.1), 113 | (True, False, '-100.1'), 114 | (True, False, -100), 115 | (False, True, -99.9), 116 | (True, False, 0), 117 | (False, True, 1), 118 | (True, False, 0.1), 119 | (True, False, '0.1'), 120 | (True, False, '2'), 121 | (True, False, 2.9), 122 | (False, True, 3), 123 | (False, True, 3.1), 124 | (True, False, None), 125 | (VALUE_ERROR, VALUE_ERROR, True), 126 | (VALUE_ERROR, VALUE_ERROR, False), 127 | (VALUE_ERROR, ) * 2 + ('xyzzy', ), 128 | (VALUE_ERROR, ) * 3, 129 | (DIV0, ) * 3, 130 | ) 131 | ) 132 | def test_is_even_odd(_iseven, _isodd, value): 133 | assert iseven(value) == _iseven 134 | assert isodd(value) == _isodd 135 | 136 | 137 | @pytest.mark.parametrize( 138 | 'value, expected', ( 139 | (0, False), 140 | (1, False), 141 | (1.0, False), 142 | (-1, False), 143 | ('a', False), 144 | (((1, NA_ERROR), ('2', DIV0)), ((False, True), (False, True))), 145 | (NUM_ERROR, True), 146 | (REF_ERROR, True), 147 | ) 148 | ) 149 | def test_iserror(value, expected): 150 | assert iserror(value) == expected 151 | 152 | 153 | @pytest.mark.parametrize( 154 | 'value, expected', ( 155 | (False, True), 156 | (True, True), 157 | (0, False), 158 | (1, False), 159 | (1.0, False), 160 | (-1, False), 161 | ('a', False), 162 | (((1, NA_ERROR), ('2', True)), ((False, False), (False, True))), 163 | (NA_ERROR, False), 164 | (VALUE_ERROR, False), 165 | ) 166 | ) 167 | def test_islogical(value, expected): 168 | assert islogical(value) == expected 169 | 170 | 171 | @pytest.mark.parametrize( 172 | 'value, expected', ( 173 | (0, False), 174 | (1, False), 175 | (1.0, False), 176 | (-1, False), 177 | ('a', False), 178 | (((1, NA_ERROR), ('2', 3)), ((False, True), (False, False))), 179 | (NA_ERROR, True), 180 | (VALUE_ERROR, False), 181 | ) 182 | ) 183 | def test_isna(value, expected): 184 | assert isna(value) == expected 185 | 186 | 187 | @pytest.mark.parametrize( 188 | 'value, expected', ( 189 | (0, True), 190 | (1, True), 191 | (1.0, True), 192 | (-1, True), 193 | ('a', False), 194 | (False, False), 195 | (True, False), 196 | (((1, NA_ERROR), ('2', 3)), ((True, False), (False, True))), 197 | (NA_ERROR, False), 198 | (VALUE_ERROR, False), 199 | ) 200 | ) 201 | def test_isnumber(value, expected): 202 | assert isnumber(value) == expected 203 | 204 | 205 | @pytest.mark.parametrize( 206 | 'value, expected', ( 207 | ('a', True), 208 | (1, False), 209 | (1.0, False), 210 | (None, False), 211 | (DIV0, False), 212 | (((1, NA_ERROR), ('2', 3)), ((False, False), (True, False))), 213 | (NA_ERROR, False), 214 | (VALUE_ERROR, False), 215 | ) 216 | ) 217 | def test_istext(value, expected): 218 | assert istext(value) == expected 219 | assert isnontext(value) != expected 220 | 221 | 222 | @pytest.mark.parametrize( 223 | 'value, expected', ( 224 | (False, 0), 225 | (True, 1), 226 | ('a', 0), 227 | (1, 1), 228 | (1.0, 1.0), 229 | (-1.0, -1.0), 230 | (None, None), 231 | (DIV0, DIV0), 232 | (((1, NA_ERROR), ('2', 3)), ((1, NA_ERROR), (0, 3))), 233 | (NA_ERROR, NA_ERROR), 234 | (VALUE_ERROR, VALUE_ERROR), 235 | ) 236 | ) 237 | def test_n(value, expected): 238 | assert n(value) == expected 239 | 240 | 241 | def test_na(): 242 | assert na() == NA_ERROR 243 | -------------------------------------------------------------------------------- /src/pycel/lib/logical.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Python equivalents of excel logical functions (bools) 12 | """ 13 | import itertools as it 14 | from numbers import Number 15 | 16 | import numpy as np 17 | 18 | from pycel.excelutil import ( 19 | ERROR_CODES, 20 | flatten, 21 | has_array_arg, 22 | in_array_formula_context, 23 | is_array_arg, 24 | NA_ERROR, 25 | VALUE_ERROR, 26 | ) 27 | from pycel.lib.function_helpers import cse_array_wrapper, excel_helper 28 | from pycel.lib.lookup import ExcelCmp 29 | 30 | 31 | def _clean_logical(test): 32 | """For logicals that take one argument, clean via excel rules""" 33 | if test in ERROR_CODES: 34 | return test 35 | 36 | if isinstance(test, str): 37 | if test.lower() in ('true', 'false'): 38 | test = len(test) == 4 39 | else: 40 | return VALUE_ERROR 41 | 42 | if test is None: 43 | return False 44 | elif isinstance(test, (Number, np.bool_)): 45 | return bool(test) 46 | else: 47 | return VALUE_ERROR 48 | 49 | 50 | def _clean_logicals(*args): 51 | """For logicals that take more than one argument, clean via excel rules""" 52 | values = tuple(flatten(args)) 53 | 54 | error = next((x for x in values if x in ERROR_CODES), None) 55 | 56 | if error is not None: 57 | # return the first error in the list 58 | return error 59 | else: 60 | values = tuple(x for x in values 61 | if not (x is None or isinstance(x, str))) 62 | return VALUE_ERROR if len(values) == 0 else values 63 | 64 | 65 | def and_(*args): 66 | # Excel reference: https://support.microsoft.com/en-us/office/ 67 | # and-function-5f19b2e8-e1df-4408-897a-ce285a19e9d9 68 | values = _clean_logicals(*args) 69 | if isinstance(values, str): 70 | # return error code 71 | return values 72 | else: 73 | return all(values) 74 | 75 | 76 | # def false(value): 77 | # A "compatibility function", needed only for use with other spreadsheet programs 78 | # Excel reference: https://support.microsoft.com/en-us/office/ 79 | # false-function-2d58dfa5-9c03-4259-bf8f-f0ae14346904 80 | 81 | 82 | @excel_helper(cse_params=(0, 1, 2), err_str_params=0) 83 | def if_(test, true_value, false_value=0): 84 | # Excel reference: https://support.microsoft.com/en-us/office/ 85 | # IF-function-69AED7C9-4E8A-4755-A9BC-AA8BBFF73BE2 86 | cleaned = _clean_logical(test) 87 | 88 | if isinstance(cleaned, str): 89 | # return error code 90 | return cleaned 91 | else: 92 | return true_value if cleaned else false_value 93 | 94 | 95 | def iferror(arg, value_if_error): 96 | # Excel reference: https://support.microsoft.com/en-us/office/ 97 | # IFERROR-function-C526FD07-CAEB-47B8-8BB6-63F3E417F611 98 | if in_array_formula_context and has_array_arg(arg, value_if_error): 99 | return cse_array_wrapper(iferror, (0, 1))(arg, value_if_error) 100 | elif arg in ERROR_CODES or is_array_arg(arg): 101 | return 0 if value_if_error is None else value_if_error 102 | else: 103 | return arg 104 | 105 | 106 | def ifna(arg, value_if_na): 107 | # Excel reference: https://support.microsoft.com/en-us/office/ 108 | # ifna-function-6626c961-a569-42fc-a49d-79b4951fd461 109 | if in_array_formula_context and has_array_arg(arg, value_if_na): 110 | return cse_array_wrapper(ifna, (0, 1))(arg, value_if_na) 111 | elif arg == NA_ERROR or is_array_arg(arg): 112 | return 0 if value_if_na is None else value_if_na 113 | else: 114 | return arg 115 | 116 | 117 | def ifs(*args): 118 | # IFS function 119 | # Excel 2016 120 | # Checks whether one or more conditions are met and returns a value that 121 | # corresponds to the first TRUE condition. 122 | # Excel reference: https://support.microsoft.com/en-us/office/ 123 | # ifs-function-36329a26-37b2-467c-972b-4a39bd951d45 124 | if not len(args) % 2: 125 | if in_array_formula_context and any(isinstance(a, tuple) for a in args): 126 | return cse_array_wrapper(ifs, tuple(range(len(args))))(*args) 127 | 128 | for test, value in zip(args[::2], args[1::2]): 129 | 130 | if test in ERROR_CODES: 131 | return test 132 | 133 | if isinstance(test, str): 134 | if test.lower() in ('true', 'false'): 135 | test = len(test) == 4 136 | else: 137 | return VALUE_ERROR 138 | 139 | if test: 140 | return value 141 | 142 | return NA_ERROR 143 | 144 | 145 | def not_(value): 146 | # Excel reference: https://support.microsoft.com/en-us/office/ 147 | # not-function-9cfc6011-a054-40c7-a140-cd4ba2d87d77 148 | cleaned = _clean_logical(value) 149 | 150 | if isinstance(cleaned, str): 151 | # return error code 152 | return cleaned 153 | else: 154 | return not cleaned 155 | 156 | 157 | def or_(*args): 158 | # Excel reference: https://support.microsoft.com/en-us/office/ 159 | # or-function-7d17ad14-8700-4281-b308-00b131e22af0 160 | values = _clean_logicals(*args) 161 | if isinstance(values, str): 162 | # return error code 163 | return values 164 | else: 165 | return any(values) 166 | 167 | 168 | @excel_helper(cse_params=-1) 169 | def switch(lookup_value, *args): 170 | # Evaluates an expression against a list of values and returns the result 171 | # corresponding to the first matching value. If there is no match, an optional 172 | # default value may be returned. 173 | # Excel reference: https://support.microsoft.com/en-us/office/ 174 | # switch-function-47ab33c0-28ce-4530-8a45-d532ec4aa25e 175 | if len(args) < 2: 176 | return VALUE_ERROR 177 | 178 | lookup_value = ExcelCmp(lookup_value) 179 | for to_match, result in zip(it.islice(args, 0, None, 2), it.islice(args, 1, None, 2)): 180 | to_match = ExcelCmp(to_match) 181 | if to_match == lookup_value: 182 | return result 183 | 184 | if len(args) % 2: 185 | return args[-1] 186 | return NA_ERROR 187 | 188 | 189 | # def true(value): 190 | # A "compatibility function", needed only for use with other spreadsheet programs 191 | # Excel reference: https://support.microsoft.com/en-us/office/ 192 | # true-function-7652c6e3-8987-48d0-97cd-ef223246b3fb 193 | 194 | 195 | def xor_(*args): 196 | # Excel reference: https://support.microsoft.com/en-us/office/ 197 | # xor-function-1548d4c2-5e47-4f77-9a92-0533bba14f37 198 | values = _clean_logicals(*args) 199 | if isinstance(values, str): 200 | # return error code 201 | return values 202 | else: 203 | return sum(bool(v) for v in values) % 2 204 | 205 | 206 | # Older mappings for excel functions that match Python built-in and keywords 207 | x_and = and_ 208 | x_if = if_ 209 | x_not = not_ 210 | x_or = or_ 211 | x_xor = xor_ 212 | -------------------------------------------------------------------------------- /tests/lib/test_engineering.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | import pycel.lib.engineering 13 | from pycel.excelutil import coerce_to_number, DIV0, ERROR_CODES, NUM_ERROR, VALUE_ERROR 14 | from pycel.lib import engineering 15 | from pycel.lib.engineering import ( 16 | bitand, 17 | bitlshift, 18 | bitor, 19 | bitrshift, 20 | bitxor, 21 | ) 22 | from pycel.lib.function_helpers import load_to_test_module 23 | 24 | # dynamic load the lib functions from engineering and apply metadata 25 | load_to_test_module(pycel.lib.engineering, __name__) 26 | 27 | MAX_BASE_2 = engineering._SIZE_MASK[2] 28 | MAX_BASE_8 = engineering._SIZE_MASK[8] 29 | MAX_BASE_16 = engineering._SIZE_MASK[16] 30 | 31 | 32 | def compare_result(expected, result): 33 | expected = coerce_to_number(expected) 34 | result = coerce_to_number(result) 35 | if isinstance(expected, (int, float)) and isinstance(result, (int, float)): 36 | return pytest.approx(expected) == result 37 | else: 38 | return expected == result 39 | 40 | 41 | @pytest.mark.parametrize( 42 | 'value, base, expected', ( 43 | ('0', 2, '0.0'), 44 | (0, 8, '0.0'), 45 | (0, 16, '0.0'), 46 | ('111111111', 2, 511.0), 47 | ('7777777777', 8, '-1.0'), 48 | ('9999999999', 16, -439804651111.0), 49 | (3.5, 2, '#NUM!'), 50 | (3.5, 8, '#NUM!'), 51 | ('3.5', 16, '#NUM!'), 52 | ('1000000000', 2, '-512.0'), 53 | ('11111111111', 8, '#NUM!'), 54 | ('11111111111', 16, '#NUM!'), 55 | (-1, 2, '#NUM!'), 56 | (-1, 8, '#NUM!'), 57 | (-1, 16, '#NUM!'), 58 | (None, 2, '0.0'), 59 | (None, 8, '0.0'), 60 | (None, 16, '0.0'), 61 | ('xyzzy', 2, '#NUM!'), 62 | ('a', 8, '#NUM!'), 63 | ('f10000001f', 16, -64424509409), 64 | (True, 2, '#VALUE!'), 65 | (True, 8, '#VALUE!'), 66 | (True, 16, '#VALUE!'), 67 | ) 68 | ) 69 | def test_base2dec(value, base, expected): 70 | assert compare_result(expected, engineering._base2dec(value, base)) 71 | 72 | mapped = {2: engineering.bin2dec, 8: engineering.oct2dec, 16: engineering.hex2dec} 73 | assert compare_result(expected, mapped[base](value)) 74 | 75 | 76 | @pytest.mark.parametrize('value', tuple(ERROR_CODES)) 77 | def test_base2dec_errors(value): 78 | for base in (2, 8, 16): 79 | assert compare_result(value, engineering._base2dec(value, base)) 80 | 81 | 82 | @pytest.mark.parametrize( 83 | 'value, base, expected', ( 84 | (MAX_BASE_2, 2, '#NUM!'), 85 | (MAX_BASE_8, 8, '#NUM!'), 86 | (MAX_BASE_16, 16, '#NUM!'), 87 | (MAX_BASE_2 - 1, 2, '111111111'), 88 | (MAX_BASE_8 - 1, 8, '3777777777'), 89 | (MAX_BASE_16 - 1, 16, '7FFFFFFFFF'), 90 | (-MAX_BASE_2, 2, '1000000000'), 91 | (-MAX_BASE_8, 8, '4000000000'), 92 | (-MAX_BASE_16, 16, '8000000000'), 93 | (-MAX_BASE_2 - 1, 2, '#NUM!'), 94 | (-MAX_BASE_8 - 1, 8, '#NUM!'), 95 | (-MAX_BASE_16 - 1, 16, '#NUM!'), 96 | ('xyzzy', 2, '#VALUE!'), 97 | ('xyzzy', 8, '#VALUE!'), 98 | ('xyzzy', 16, '#VALUE!'), 99 | (True, 2, '#VALUE!'), 100 | (True, 8, '#VALUE!'), 101 | (True, 16, '#VALUE!'), 102 | ) 103 | ) 104 | def test_dec2base(value, base, expected): 105 | assert compare_result(expected, engineering._dec2base(value, base=base)) 106 | 107 | mapped = {2: engineering.dec2bin, 8: engineering.dec2oct, 16: engineering.dec2hex} 108 | assert compare_result(expected, mapped[base](value)) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | 'value, base, places, expected', ( 113 | (100, 2, 1, '#NUM!'), 114 | (100, 8, 1, '#NUM!'), 115 | (100, 16, 1, '#NUM!'), 116 | (None, 2, 3, '000'), 117 | (None, 8, 0, '#NUM!'), 118 | (None, 16, 1, '0'), 119 | ) 120 | ) 121 | def test_dec2base_places(value, base, places, expected): 122 | assert compare_result(expected, 123 | engineering._dec2base(value, base=base, places=places)) 124 | 125 | 126 | @pytest.mark.parametrize('value', tuple(ERROR_CODES)) 127 | def test_dec2base_errors(value): 128 | for base in (2, 8, 16): 129 | assert compare_result(value, engineering._dec2base(value, base=base)) 130 | 131 | 132 | @pytest.mark.parametrize( 133 | 'value, bases, expected', ( 134 | ('111111111', (2, 8), '777'), 135 | ('111111111', (2, 16), '1FF'), 136 | ('7777777777', (8, 2), '1111111111'), 137 | ('7777777777', (8, 16), 'FFFFFFFFFF'), 138 | ('9999999999', (16, 2), '#NUM!'), 139 | ('9999999999', (16, 8), '#NUM!'), 140 | ('1000000000', (2, 8), '7777777000'), 141 | ('1000000000', (2, 16), 'FFFFFFFE00'), 142 | ('11111111111', (8, 2), '#NUM!'), 143 | ('11111111111', (8, 16), '#NUM!'), 144 | ('11111111111', (16, 2), '#NUM!'), 145 | ('11111111111', (16, 8), '#NUM!'), 146 | (None, (2, 8), '#NUM!'), 147 | (None, (2, 16), '#NUM!'), 148 | (None, (8, 2), '0'), 149 | (None, (8, 16), '0'), 150 | (None, (16, 2), '0'), 151 | (None, (16, 8), '0'), 152 | ('fffffffffe', (2, 8), '#NUM!'), 153 | ('fffffffffe', (2, 16), '#NUM!'), 154 | ('a', (8, 2), '#NUM!'), 155 | ('a', (8, 16), '#NUM!'), 156 | ('fffffffffe', (16, 2), '1111111110'), 157 | ('fffffffffe', (16, 8), '7777777776'), 158 | ) 159 | ) 160 | def test_base2base(value, bases, expected): 161 | base_in, base_out = bases 162 | assert compare_result(expected, engineering._base2base( 163 | value, base_in=base_in, base_out=base_out)) 164 | 165 | mapped = { 166 | (2, 8): engineering.bin2oct, 167 | (2, 16): engineering.bin2hex, 168 | (8, 2): engineering.oct2bin, 169 | (8, 16): engineering.oct2hex, 170 | (16, 2): engineering.hex2bin, 171 | (16, 8): engineering.hex2oct, 172 | } 173 | assert compare_result(expected, mapped[bases](value)) 174 | 175 | 176 | @pytest.mark.parametrize( 177 | 'value, expected', ( 178 | (True, '#VALUE!'), 179 | (-1, '#NUM!'), 180 | ('-1', '#NUM!'), 181 | ('3.5', '#NUM!'), 182 | (3.5, '#NUM!'), 183 | ('0', '0'), 184 | (0, '0'), 185 | ) 186 | ) 187 | def test_base2base_all_bases(value, expected): 188 | for base_in in (2, 8, 16): 189 | for base_out in (2, 8, 16): 190 | if base_in != base_out: 191 | assert compare_result( 192 | expected, engineering._base2base( 193 | value, base_in=base_in, base_out=base_out)) 194 | 195 | 196 | @pytest.mark.parametrize('value', tuple(ERROR_CODES)) 197 | def test_base2base_errors(value): 198 | for base_in in (2, 8, 16): 199 | for base_out in (2, 8, 16): 200 | assert compare_result(value, engineering._base2base( 201 | value, base_in=base_in, base_out=base_out)) 202 | 203 | 204 | @pytest.mark.parametrize( 205 | 'op_x, op_y, expected', ( 206 | (32, 48, 32), 207 | (1, 2, 0), 208 | (DIV0, 1, DIV0), 209 | (1, DIV0, DIV0), 210 | ('er', 1, VALUE_ERROR), 211 | (2, 'ze', VALUE_ERROR), 212 | (NUM_ERROR, 1, NUM_ERROR), 213 | (1, NUM_ERROR, NUM_ERROR), 214 | (-1, 1, NUM_ERROR), 215 | (1, -1, NUM_ERROR), 216 | ) 217 | ) 218 | def test_bitand(op_x, op_y, expected): 219 | assert bitand(op_x, op_y) == expected 220 | 221 | 222 | @pytest.mark.parametrize( 223 | 'number, pos, expected', ( 224 | (6, 1, 12), 225 | (6, -1, 3), 226 | (6, 0, 6), 227 | ('er', 1, VALUE_ERROR), 228 | (2, 'ze', VALUE_ERROR), 229 | (-1, 0, NUM_ERROR), 230 | (2**48, 0, NUM_ERROR), 231 | (6, 54, NUM_ERROR), 232 | (6, -54, NUM_ERROR), 233 | ) 234 | ) 235 | def test_bitlshift(number, pos, expected): 236 | assert bitlshift(number, pos) == expected 237 | 238 | 239 | @pytest.mark.parametrize( 240 | 'op_x, op_y, expected', ( 241 | (32, 16, 48), 242 | (1, 2, 3), 243 | (-1, 1, NUM_ERROR), 244 | (1, -1, NUM_ERROR), 245 | ) 246 | ) 247 | def test_bitor(op_x, op_y, expected): 248 | assert bitor(op_x, op_y) == expected 249 | 250 | 251 | @pytest.mark.parametrize( 252 | 'number, pos, expected', ( 253 | (6, 1, 3), 254 | (6, -1, 12), 255 | (6, 0, 6), 256 | ('er', 1, VALUE_ERROR), 257 | (2, 'ze', VALUE_ERROR), 258 | (-1, 0, NUM_ERROR), 259 | (2**48, 0, NUM_ERROR), 260 | (6, 54, NUM_ERROR), 261 | (6, -54, NUM_ERROR), 262 | ) 263 | ) 264 | def test_bitrshift(number, pos, expected): 265 | assert bitrshift(number, pos) == expected 266 | 267 | 268 | @pytest.mark.parametrize( 269 | 'op_x, op_y, expected', ( 270 | (16, 15, 31), 271 | (1, 3, 2), 272 | (-1, 1, NUM_ERROR), 273 | (1, -1, NUM_ERROR), 274 | ) 275 | ) 276 | def test_bitxor(op_x, op_y, expected): 277 | assert bitxor(op_x, op_y) == expected 278 | -------------------------------------------------------------------------------- /tests/test_excelwrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | from pycel.excelutil import AddressRange 13 | from pycel.excelwrapper import ( 14 | _OpxRange, 15 | ARRAY_FORMULA_FORMAT, 16 | ExcelOpxWrapperNoData, 17 | ) 18 | 19 | 20 | def test_set_and_get_active_sheet(excel): 21 | excel.set_sheet("Sheet2") 22 | assert excel.get_active_sheet_name() == 'Sheet2' 23 | 24 | excel.set_sheet("Sheet3") 25 | assert excel.get_active_sheet_name() == 'Sheet3' 26 | 27 | 28 | def test_get_range(excel): 29 | excel.set_sheet("Sheet2") 30 | excel_range = excel.get_range('Sheet2!A5:B7') 31 | assert excel_range.formula == (('', ''), ('', ''), ('', '')) 32 | assert sum(map(len, excel_range.values)) == 6 33 | 34 | 35 | def test_get_used_range(excel): 36 | excel.set_sheet("Sheet1") 37 | assert sum(map(len, excel.get_used_range())) == 72 38 | 39 | 40 | def test_get_formula_from_range(excel): 41 | excel.set_sheet("Sheet1") 42 | formulas = excel.get_formula_from_range("Sheet1!C2:C5") 43 | assert len(formulas) == 4 44 | assert formulas[1][0] == "=SIN(B3*A3^2)" 45 | 46 | formulas = excel.get_formula_from_range("Sheet1!C600:C601") 47 | assert formulas == ((None, ), (None, )) 48 | 49 | formula = excel.get_formula_from_range("Sheet1!C3") 50 | assert formula == "=SIN(B3*A3^2)" 51 | 52 | 53 | @pytest.mark.parametrize( 54 | 'address, value', 55 | [ 56 | ("Sheet1!A2", 2), 57 | ("Sheet1!B2", '=SUM(A2:A4)'), 58 | ("Sheet1!A2:C2", ((2, '=SUM(A2:A4)', '=SIN(B2*A2^2)'),)), 59 | ("Sheet1!A1:A3", ((1,), (2,), (3,))), 60 | ("Sheet1!1:2", ( 61 | (1, '=SUM(A1:A3)', '=SIN(B1*A1^2)', '=LINEST(C1:C18,B1:B18)'), 62 | (2, '=SUM(A2:A4)', '=SIN(B2*A2^2)', None))), 63 | ] 64 | ) 65 | def test_get_formula_or_value(excel, address, value): 66 | assert value == excel.get_formula_or_value(address) 67 | 68 | from_opxl = ExcelOpxWrapperNoData(excel.workbook) 69 | assert value == from_opxl.get_formula_or_value(address) 70 | 71 | 72 | @pytest.mark.parametrize( 73 | 'address1, address2', 74 | [ 75 | ("Sheet1!1:2", "Sheet1!A1:D2"), 76 | ("Sheet1!A:B", "Sheet1!A1:B18"), 77 | ("Sheet1!2:2", "Sheet1!A2:D2"), 78 | ("Sheet1!B:B", "Sheet1!B1:B18"), 79 | ] 80 | ) 81 | def test_get_unbounded_range(excel, address1, address2): 82 | assert excel.get_range(address1) == excel.get_range(address2) 83 | 84 | 85 | def test_get_value_with_formula(excel): 86 | result = excel.get_range("Sheet1!A2:C2").values 87 | assert ((2, 9, -0.9917788534431158),) == result 88 | 89 | result = excel.get_range("Sheet1!A1:A3").values 90 | assert ((1,), (2,), (3,)) == result 91 | 92 | result = excel.get_range("Sheet1!B2").values 93 | assert 9 == result 94 | 95 | excel.set_sheet('Sheet1') 96 | result = excel.get_range("B2").values 97 | assert 9 == result 98 | 99 | result = excel.get_range("Sheet1!AA1:AA3").values 100 | assert ((None,), (None,), (None,)) == result 101 | 102 | result = excel.get_range("Sheet1!CC2").values 103 | assert result is None 104 | 105 | 106 | def test_get_range_value(excel): 107 | result = excel.get_range("Sheet1!A2:C2").values 108 | assert ((2, 9, -0.9917788534431158),) == result 109 | 110 | result = excel.get_range("Sheet1!A1:A3").values 111 | assert ((1,), (2,), (3,)) == result 112 | 113 | result = excel.get_range("Sheet1!A1").values 114 | assert 1 == result 115 | 116 | result = excel.get_range("Sheet1!AA1:AA3").values 117 | assert ((None,), (None,), (None,)) == result 118 | 119 | result = excel.get_range("Sheet1!CC2").values 120 | assert result is None 121 | 122 | 123 | def test_get_defined_names(excel): 124 | expected = {'SINUS': [('$C$1:$C$18', 'Sheet1')]} 125 | assert expected == excel.defined_names 126 | 127 | assert excel.defined_names == excel.defined_names 128 | 129 | 130 | def test_get_tables(excel): 131 | for table_name in ('Table1', 'tAbLe1'): 132 | table, sheet_name = excel.table(table_name) 133 | assert 'sref' == sheet_name 134 | assert 'D1:F4' == table.ref 135 | assert 'Table1' == table.name 136 | 137 | assert (None, None) == excel.table('JUNK') 138 | 139 | 140 | @pytest.mark.parametrize( 141 | 'address, table_name', 142 | [ 143 | ('sref!D1', 'Table1'), 144 | ('sref!F1', 'Table1'), 145 | ('sref!D4', 'Table1'), 146 | ('sref!F4', 'Table1'), 147 | ('sref!F4', 'Table1'), 148 | ('sref!C1', None), 149 | ('sref!G1', None), 150 | ('sref!D5', None), 151 | ('sref!F5', None), 152 | ] 153 | ) 154 | def test_table_name_containing(excel, address, table_name): 155 | table = excel.table_name_containing(address) 156 | if table_name is None: 157 | assert table is None 158 | else: 159 | assert table.lower() == table_name.lower() 160 | 161 | 162 | @pytest.mark.parametrize( 163 | 'address, values, formula', 164 | [ 165 | ('ArrayForm!H1:I2', ((1, 2), (1, 2)), 166 | (('=INDEX(COLUMN(A1:B1),1,1,1,2)', '=INDEX(COLUMN(A1:B1),1,2,1,2)'), 167 | ('=INDEX(COLUMN(A1:B1),1,1)', '=INDEX(COLUMN(A1:B1),1,2)')), 168 | ), 169 | ('ArrayForm!E1:F3', ((1, 1), (2, 2), (3, 3)), 170 | (('=INDEX(ROW(A1:A3),1,1,3,1)', '=INDEX(ROW(A1:A3), 1)'), 171 | ('=INDEX(ROW(A1:A3),2,1,3,1)', '=INDEX(ROW(A1:A3), 2)'), 172 | ('=INDEX(ROW(A1:A3),3,1,3,1)', '=INDEX(ROW(A1:A3), 3)')) 173 | ), 174 | ('ArrayForm!E7:E9', ((11,), (10,), (16,)), 175 | (('=SUM((A7:A13="a")*(B7:B13="y")*C7:C13)',), 176 | ('=SUM((A7:A13<>"b")*(B7:B13<>"y")*C7:C13)',), 177 | ('=SUM((A7:A13>"b")*(B7:B13<"z")*(C7:C13+3.5))',)) 178 | ), 179 | ('ArrayForm!G16:H17', 180 | ((1, 6), (6, 16)), '={A16:B17*D16:E17}'), 181 | ('ArrayForm!E21:F24', 182 | ((6, 6), (8, 8), (10, 10), (12, 12)), '={A21:A24+C21:C24}' 183 | ), 184 | ('ArrayForm!A32:D33', 185 | ((6, 8, 10, 12), (6, 8, 10, 12)), '={A28:D28+A30:D30}' 186 | ), 187 | ('ArrayForm!F28:I31', 188 | ((5, 6, 7, 8), (10, 12, 14, 16), (15, 18, 21, 24), (20, 24, 28, 32)), 189 | '={A21:A24*A30:D30}', 190 | ), 191 | ] 192 | ) 193 | def test_array_formulas(excel, address, values, formula): 194 | result = excel.get_range(address) 195 | assert result.address == AddressRange(address) 196 | assert result.values == values 197 | if result.formula: 198 | assert result.formula == formula 199 | 200 | 201 | def test_get_datetimes(excel): 202 | result = excel.get_range("datetime!A1:B13").values 203 | for row in result: 204 | assert row[0] == row[1] 205 | 206 | 207 | @pytest.mark.parametrize( 208 | 'result_range, expected_range', 209 | [ 210 | ("Sheet1!C:C", "Sheet1!C1:C18"), 211 | ("Sheet1!2:2", "Sheet1!A2:D2"), 212 | ("Sheet1!B:C", "Sheet1!B1:C18"), 213 | ("Sheet1!2:3", "Sheet1!A2:D3"), 214 | ] 215 | ) 216 | def test_get_entire_rows_columns(excel, result_range, expected_range): 217 | 218 | result = excel.get_range(result_range).values 219 | expected = excel.get_range(expected_range).values 220 | assert result == expected 221 | 222 | 223 | @pytest.mark.parametrize( 224 | 'address, expecteds', 225 | ( 226 | ('Sheet1!B2', ((1, '=B2=2'), (2, '=B2>1'), (4, '=B2>0'), (5, '=B2<0'))), 227 | ('Sheet1!B5', ((1, '=B5=2'), (2, '=B5>1'), (4, '=B5>0'), (5, '=B5<0'))), 228 | ('Sheet1!A1', ()), 229 | ) 230 | ) 231 | def test_conditional_format(cond_format_ws, address, expecteds): 232 | excel = cond_format_ws.excel 233 | results = excel.conditional_format(address) 234 | for result, expected in zip(results, expecteds): 235 | assert (result.priority, result.formula) == expected 236 | 237 | 238 | @pytest.mark.parametrize( 239 | 'value, formula', 240 | ( 241 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 1, 1, 2, 2), '={xyzzy}'), 242 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 1, 2, 2, 2), None), 243 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 2, 1, 2, 2), None), 244 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 2, 2, 2, 2), None), 245 | ) 246 | ) 247 | def test_cell_to_formulax(value, formula, ATestCell): 248 | cells = ((ATestCell('A', 1, value=value), ), ) 249 | assert _OpxRange(cells, cells, '').formula == formula 250 | 251 | 252 | @pytest.mark.parametrize( 253 | 'value, formula', 254 | ( 255 | (None, ""), 256 | ("xyzzy", ""), 257 | ("=xyzzy", "=xyzzy"), 258 | ("={1,2;3,4}", "=index({1,2;3,4},1,1)"), 259 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 1, 1, 2, 2), "=index(s!E3:F4,1,1)"), 260 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 1, 2, 2, 2), "=index(s!D3:E4,1,2)"), 261 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 2, 1, 2, 2), "=index(s!E2:F3,2,1)"), 262 | (ARRAY_FORMULA_FORMAT % ('xyzzy', 2, 2, 2, 2), "=index(s!D2:E3,2,2)"), 263 | ) 264 | ) 265 | def test_cell_to_formula(value, formula): 266 | """""" 267 | from unittest import mock 268 | parent = mock.Mock() 269 | parent.title = 's' 270 | cell = mock.Mock() 271 | cell.value = value 272 | cell.row = 3 273 | cell.col_idx = 5 274 | cell.parent = parent 275 | assert _OpxRange.cell_to_formula(cell) == formula 276 | -------------------------------------------------------------------------------- /tests/lib/test_logical.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | import pycel.lib.logical 13 | from pycel.excelcompiler import ExcelCompiler 14 | from pycel.excelutil import ( 15 | DIV0, 16 | ERROR_CODES, 17 | in_array_formula_context, 18 | NA_ERROR, 19 | NAME_ERROR, 20 | NULL_ERROR, 21 | NUM_ERROR, 22 | REF_ERROR, 23 | VALUE_ERROR, 24 | ) 25 | from pycel.lib.function_helpers import load_to_test_module 26 | from pycel.lib.logical import ( 27 | _clean_logicals, 28 | and_, 29 | if_, 30 | iferror, 31 | ifna, 32 | ifs, 33 | not_, 34 | or_, 35 | switch, 36 | xor_, 37 | ) 38 | 39 | 40 | # dynamic load the lib functions from excellib and apply metadata 41 | load_to_test_module(pycel.lib.logical, __name__) 42 | 43 | 44 | def test_logical_ws(fixture_xls_copy): 45 | compiler = ExcelCompiler(fixture_xls_copy('logical.xlsx')) 46 | result = compiler.validate_serialized() 47 | assert result == {} 48 | 49 | 50 | @pytest.mark.parametrize( 51 | 'test_value, expected', ( 52 | ((1, '3', 2.0, 3.1, ('x', True, None)), 53 | (1, 2, 3.1, True)), 54 | ((1, '3', 2.0, 3.1, ('x', VALUE_ERROR)), 55 | VALUE_ERROR), 56 | ((1, NA_ERROR, 2.0, 3.1, ('x', VALUE_ERROR)), 57 | NA_ERROR), 58 | (('1', ('x', 'y')), 59 | VALUE_ERROR), 60 | ((), 61 | VALUE_ERROR), 62 | ) 63 | ) 64 | def test_clean_logicals(test_value, expected): 65 | assert _clean_logicals(*test_value) == expected 66 | 67 | 68 | @pytest.mark.parametrize( 69 | 'expected, test_value', ( 70 | (False, (False,)), 71 | (True, (True,)), 72 | (True, (1, '3', 2.0, 3.1, ('x', True))), 73 | (True, (1, '3', 2.0, 3.1, ('x', True, None))), 74 | (False, (1, '3', 2.0, 3.1, ('x', False))), 75 | (VALUE_ERROR, (1, '3', 2.0, 3.1, ('x', VALUE_ERROR))), 76 | (NA_ERROR, (1, NA_ERROR, 2.0, 3.1, ('x', VALUE_ERROR))), 77 | (VALUE_ERROR, ('1', ('x', 'y'))), 78 | (VALUE_ERROR, (),), 79 | (NA_ERROR, (NA_ERROR, 1),), 80 | ) 81 | ) 82 | def test_and_(expected, test_value): 83 | assert and_(*test_value) == expected 84 | 85 | 86 | @pytest.mark.parametrize( 87 | 'test_value, error_value, expected', ( 88 | ('A', 2, 'A'), 89 | (NULL_ERROR, 2, 2), 90 | (DIV0, 2, 2), 91 | (VALUE_ERROR, 2, 2), 92 | (REF_ERROR, 2, 2), 93 | (NAME_ERROR, 2, 2), 94 | (NUM_ERROR, 2, 2), 95 | (NA_ERROR, 2, 2), 96 | (NA_ERROR, None, 0), 97 | (((1, VALUE_ERROR), (VALUE_ERROR, 1)), 2, ((1, 2), (2, 1))), 98 | (((1, VALUE_ERROR), (VALUE_ERROR, 1)), None, ((1, 0), (0, 1))), 99 | ) 100 | ) 101 | def test_iferror(test_value, error_value, expected): 102 | if isinstance(test_value, tuple): 103 | with in_array_formula_context('A1'): 104 | assert iferror(test_value, error_value) == expected 105 | expected = 0 if error_value is None else error_value 106 | 107 | assert iferror(test_value, error_value) == expected 108 | 109 | 110 | @pytest.mark.parametrize( 111 | 'test_value, na_value, expected', ( 112 | ('A', 2, 'A'), 113 | (NULL_ERROR, 2, NULL_ERROR), 114 | (DIV0, 2, DIV0), 115 | (VALUE_ERROR, 2, VALUE_ERROR), 116 | (REF_ERROR, 2, REF_ERROR), 117 | (NAME_ERROR, 2, NAME_ERROR), 118 | (NUM_ERROR, 2, NUM_ERROR), 119 | (NA_ERROR, 2, 2), 120 | (NA_ERROR, None, 0), 121 | (((1, NA_ERROR), (NA_ERROR, 1)), 2, ((1, 2), (2, 1))), 122 | (((1, NA_ERROR), (NA_ERROR, 1)), None, ((1, 0), (0, 1))), 123 | ) 124 | ) 125 | def test_ifna(test_value, na_value, expected): 126 | if isinstance(test_value, tuple): 127 | with in_array_formula_context('A1'): 128 | assert ifna(test_value, na_value) == expected 129 | expected = 0 if na_value is None else na_value 130 | 131 | assert ifna(test_value, na_value) == expected 132 | 133 | 134 | @pytest.mark.parametrize( 135 | 'test_value, true_value, false_value, expected', ( 136 | ('xyzzy', 3, 2, VALUE_ERROR), 137 | ('0', 2, 1, VALUE_ERROR), 138 | (True, 2, 1, 2), 139 | (False, 2, 1, 1), 140 | ('True', 2, 1, 2), 141 | ('False', 2, 1, 1), 142 | (None, 2, 1, 1), 143 | (NA_ERROR, 0, 0, NA_ERROR), 144 | (DIV0, 0, 0, DIV0), 145 | (1, VALUE_ERROR, 1, VALUE_ERROR), 146 | (0, VALUE_ERROR, 1, 1), 147 | (0, 1, VALUE_ERROR, VALUE_ERROR), 148 | (1, 1, VALUE_ERROR, 1), 149 | (((1, 0), (0, 1)), 1, VALUE_ERROR, 150 | ((1, VALUE_ERROR), (VALUE_ERROR, 1))), 151 | (((1, 0), (0, 1)), 0, ((1, 2), (3, 4)), ((0, 2), (3, 0))), 152 | (((1, 0), (0, 1)), ((1, 2), (3, 4)), 0, ((1, 0), (0, 4))), 153 | (((1, 0), (0, 1)), ((1, 2), (3, 4)), ((5, 6), (7, 8)), 154 | ((1, 6), (7, 4))), 155 | (1, ((1, 2), (3, 4)), ((5, 6), (7, 8)), ((1, 2), (3, 4))), 156 | ) 157 | ) 158 | def test_if_(test_value, true_value, false_value, expected): 159 | assert if_(test_value, true_value, false_value) == expected 160 | 161 | 162 | @pytest.mark.parametrize( 163 | 'expected, value', ( 164 | (10, (True, 10, True, 20, False, 30)), 165 | (20, (False, 10, True, 20, True, 30)), 166 | (30, (False, 10, False, 20, True, 30)), 167 | (10, ("true", 10, True, 20)), 168 | (20, ("false", 10, True, 20)), 169 | (10, (2, 10, True, 20)), 170 | (20, (0, 10, True, 20)), 171 | (10, (2.1, 10, True, 20)), 172 | (20, (0.0, 10, True, 20)), 173 | (20, (None, 10, True, 20)), 174 | (10, (True, 10, "xyzzy", 20)), 175 | (VALUE_ERROR, ("xyzzy", 10, True, 20)), 176 | (DIV0, (DIV0, 10, True, 20)), 177 | (NA_ERROR, (False, 10, 0, 20, 'false', 30)), 178 | (NA_ERROR, (False, 10, True)), 179 | ((('A', DIV0), (NA_ERROR, 4)), 180 | (((1, DIV0), (0, 0)), 'A', ((0, 0), (0, 1)), ((1, 2), (3, 4))) 181 | ), 182 | ) 183 | ) 184 | def test_ifs(expected, value): 185 | if any(isinstance(v, tuple) for v in value): 186 | with in_array_formula_context('A1'): 187 | assert ifs(*value) == expected 188 | else: 189 | assert ifs(*value) == expected 190 | 191 | 192 | @pytest.mark.parametrize( 193 | 'expected, test_value', ( 194 | (False, True), 195 | (False, 1), 196 | (False, 2.1), 197 | (False, 'true'), 198 | (False, 'True'), 199 | (False, True), 200 | 201 | (True, False), 202 | (True, None), 203 | (True, 0), 204 | (True, 0.0), 205 | (True, 'false'), 206 | (True, 'faLSe'), 207 | 208 | (VALUE_ERROR, VALUE_ERROR), 209 | (NA_ERROR, NA_ERROR), 210 | (VALUE_ERROR, '3'), 211 | (VALUE_ERROR, ('1', ('x', 'y'))), 212 | (VALUE_ERROR, (),), 213 | ) 214 | ) 215 | def test_not_(expected, test_value): 216 | assert not_(test_value) == expected 217 | 218 | 219 | @pytest.mark.parametrize( 220 | 'expected, test_value', ( 221 | (False, (False,)), 222 | (True, (True,)), 223 | (True, (1, '3', 2.0, 3.1, ('x', True))), 224 | (True, (1, '3', 2.0, 3.1, ('x', False))), 225 | (False, (0, '3', 0.0, '3.1', ('x', False))), 226 | (VALUE_ERROR, (1, '3', 2.0, 3.1, ('x', VALUE_ERROR))), 227 | (NA_ERROR, (1, NA_ERROR, 2.0, 3.1, ('x', VALUE_ERROR))), 228 | (VALUE_ERROR, ('1', ('x', 'y'))), 229 | (VALUE_ERROR, (),), 230 | ) 231 | ) 232 | def test_or_(expected, test_value): 233 | assert or_(*test_value) == expected 234 | 235 | 236 | @pytest.mark.parametrize( 237 | 'expected, test_value', ( 238 | (1, (False, False, 1)), 239 | (NA_ERROR, (False, True, 1)), 240 | (NA_ERROR, (True, False, 1)), 241 | (1, (True, True, 1)), 242 | (True, ('plugh', False, False, 1, 1, 'xyzzy', 'xyzzy', 'plugh', True)), 243 | (NA_ERROR, (-2, '-2', 1)), 244 | (NA_ERROR, (0, False, 1)), 245 | (NA_ERROR, (1, NA_ERROR, 2.0, 3.1)), 246 | (NA_ERROR, ('1', 'x', 'y')), 247 | (DIV0, (DIV0, 0)), 248 | (DIV0, (0, DIV0)), 249 | (DIV0, (0, 0, DIV0)), 250 | (DIV0, (0, 1, -1, DIV0)), 251 | (VALUE_ERROR, (0, 1, VALUE_ERROR, DIV0)), 252 | (VALUE_ERROR, (0,)), 253 | (VALUE_ERROR, (0, 0)), 254 | ) 255 | ) 256 | def test_switch(expected, test_value): 257 | assert switch(*test_value) == expected 258 | if NA_ERROR not in test_value and expected == NA_ERROR: 259 | assert switch(*test_value, 'Hi Mom!') == 'Hi Mom!' 260 | if test_value[0] not in ERROR_CODES and len(test_value) > 2: 261 | assert switch(test_value[0], *(['no-match'] * 200), *test_value[1:]) == expected 262 | 263 | 264 | @pytest.mark.parametrize( 265 | 'expected, test_value', ( 266 | (False, (False,)), 267 | (True, (True,)), 268 | (False, (False, False)), 269 | (True, (False, True)), 270 | (True, (True, False)), 271 | (False, (False, False)), 272 | (False, (1, '3', 2.0, 3.1, ('x', True))), 273 | (True, (1, '3', 2.0, 3.1, ('x', False))), 274 | (False, (0, '3', 0.0, '3.1', ('x', False))), 275 | (VALUE_ERROR, (1, '3', 2.0, 3.1, ('x', VALUE_ERROR))), 276 | (NA_ERROR, (1, NA_ERROR, 2.0, 3.1, ('x', VALUE_ERROR))), 277 | (VALUE_ERROR, ('1', ('x', 'y'))), 278 | (VALUE_ERROR, (),), 279 | ) 280 | ) 281 | def test_xor_(expected, test_value): 282 | assert xor_(*test_value) == expected 283 | -------------------------------------------------------------------------------- /src/pycel/lib/function_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import collections 11 | import functools 12 | import inspect 13 | import sys 14 | 15 | from pycel.excelutil import ( 16 | AddressCell, 17 | AddressRange, 18 | coerce_to_number, 19 | coerce_to_string, 20 | ERROR_CODES, 21 | flatten, 22 | is_array_arg, 23 | is_number, 24 | NUM_ERROR, 25 | VALUE_ERROR, 26 | ) 27 | 28 | 29 | FUNC_META = 'excel_func_meta' 30 | 31 | ALL_ARG_INDICES = frozenset(range(512)) 32 | 33 | star_args = set() 34 | 35 | 36 | def excel_helper(cse_params=None, 37 | bool_params=None, 38 | err_str_params=-1, 39 | number_params=None, 40 | str_params=None, 41 | ref_params=None): 42 | """ Decorator to annotate a function with info on how to process params 43 | 44 | All parameters are encoded as: 45 | 46 | int >= 0: param number to check 47 | tuple of ints: params to check 48 | -1: check all params 49 | None: check no params 50 | 51 | :param cse_params: CSE Array Params. If array are passed the function 52 | will be called multiple times, once for each value, and the result 53 | will be a CSE Array 54 | :param bool_params: params to coerce to bools 55 | :param err_str_params: params to check for error strings 56 | :param number_params: params to coerce to numbers 57 | :param str_params: params to coerce to strings 58 | :param ref_params: params which can remain as references 59 | :return: decorator 60 | """ 61 | def mark(f): 62 | if any(param.kind == inspect.Parameter.VAR_POSITIONAL 63 | for param in inspect.signature(f).parameters.values()): 64 | star_args.add(f.__name__) 65 | 66 | setattr(f, FUNC_META, dict( 67 | cse_params=cse_params, 68 | bool_params=bool_params, 69 | err_str_params=err_str_params, 70 | number_params=number_params, 71 | str_params=str_params, 72 | ref_params=ref_params, 73 | )) 74 | return f 75 | return mark 76 | 77 | 78 | # Decorator for generic excel function 79 | excel_func = excel_helper() 80 | 81 | # Decorator for generic excel math function (all params are numbers) 82 | excel_math_func = excel_helper( 83 | cse_params=-1, err_str_params=-1, number_params=-1) 84 | 85 | 86 | def apply_meta(f, meta=None, name_space=None): 87 | """Take the metadata applied by excel_helper and wrap accordingly""" 88 | meta = meta or getattr(f, FUNC_META, None) 89 | if meta: 90 | meta['name_space'] = name_space 91 | 92 | # find what all_params for this function should look like 93 | try: 94 | sig = inspect.signature(f) 95 | if any(param.kind == inspect.Parameter.VAR_KEYWORD 96 | for param in sig.parameters.values()): 97 | raise RuntimeError( 98 | f'Function {f.__name__}: **kwargs not allowed in signature.') 99 | except ValueError: 100 | # some built-ins do not have signature information 101 | sig = None # pragma: no cover 102 | if sig and any(param.kind == inspect.Parameter.VAR_POSITIONAL 103 | for param in sig.parameters.values()): 104 | all_params = ALL_ARG_INDICES 105 | else: 106 | all_params = set(range(getattr(getattr(f, '__code__', None), 'co_argcount', 0)) 107 | ) or ALL_ARG_INDICES 108 | 109 | # process error strings 110 | err_str_params = meta['err_str_params'] 111 | if err_str_params is not None: 112 | f = error_string_wrapper( 113 | f, all_params if err_str_params == -1 else err_str_params) 114 | 115 | # process number parameters 116 | number_params = meta['number_params'] 117 | if number_params is not None: 118 | f = nums_wrapper( 119 | f, all_params if number_params == -1 else number_params) 120 | 121 | # process str parameters 122 | str_params = meta['str_params'] 123 | if str_params is not None: 124 | f = strs_wrapper(f, all_params if str_params == -1 else str_params) 125 | 126 | # process CSE parameters 127 | cse_params = meta['cse_params'] 128 | if cse_params is not None: 129 | f = cse_array_wrapper( 130 | f, all_params if cse_params == -1 else cse_params) 131 | 132 | # process reference parameters 133 | ref_params = meta['ref_params'] 134 | if ref_params != -1: 135 | if ref_params is None: 136 | ref_params = set() 137 | f = refs_wrapper(f, name_space, ref_params) 138 | 139 | return f, meta 140 | 141 | 142 | def convert_params_indices(f, param_indices): 143 | """Given parameter indices, return a set of parameter indices to process 144 | 145 | :param f: function to check for arg count 146 | :param param_indices: params to check if CSE array 147 | int: param number to check 148 | tuple: params to check 149 | :return: set of parameter indices 150 | """ 151 | if not isinstance(param_indices, collections.abc.Iterable): 152 | assert param_indices >= 0 153 | return {int(param_indices)} 154 | 155 | else: 156 | assert all(i >= 0 for i in param_indices) 157 | return set(map(int, param_indices)) 158 | 159 | 160 | def cse_array_wrapper(f, param_indices=None): 161 | """wrapper to take cse array input and call function once per element 162 | 163 | :param f: function to wrap 164 | :param param_indices: params to check if CSE array 165 | int: param number to check 166 | tuple: params to check 167 | None: check all params 168 | :return: wrapped function 169 | """ 170 | param_indices = convert_params_indices(f, param_indices) 171 | 172 | def pick_args(args, cse_arg_nums, row, col): 173 | return (arg[row][col] if i in cse_arg_nums else arg 174 | for i, arg in enumerate(args)) 175 | 176 | @functools.wraps(f) 177 | def wrapper(*args, **kwargs): 178 | looper = (i for i in param_indices if i < len(args)) 179 | cse_arg_nums = {arg_num for arg_num in looper if is_array_arg(args[arg_num])} 180 | 181 | if cse_arg_nums: 182 | a_cse_arg = next(iter(cse_arg_nums)) 183 | num_rows = len(args[a_cse_arg]) 184 | num_cols = len(args[a_cse_arg][0]) 185 | 186 | return tuple(tuple( 187 | f(*pick_args(args, cse_arg_nums, row, col), **kwargs) 188 | for col in range(num_cols)) for row in range(num_rows)) 189 | 190 | return f(*args, **kwargs) 191 | 192 | return wrapper 193 | 194 | 195 | def nums_wrapper(f, param_indices=None): 196 | """wrapper for functions that take numbers, does excel style conversions 197 | 198 | :param f: function to wrap 199 | :param param_indices: params to coerce to numbers. 200 | int: param number to convert 201 | tuple: params to convert 202 | None: convert all params 203 | :return: wrapped function 204 | """ 205 | param_indices = convert_params_indices(f, param_indices) 206 | 207 | @functools.wraps(f) 208 | def wrapper(*args): 209 | new_args = tuple(coerce_to_number(a, convert_all=True) 210 | if i in param_indices else a 211 | for i, a in enumerate(args)) 212 | error = next((a for i, a in enumerate(new_args) 213 | if i in param_indices and a in ERROR_CODES), None) 214 | if error: 215 | return error 216 | 217 | if any(i in param_indices and not is_number(a) 218 | for i, a in enumerate(new_args)): 219 | return VALUE_ERROR 220 | 221 | try: 222 | return f(*new_args) 223 | except ValueError as exc: 224 | if "math domain error" in str(exc): 225 | return NUM_ERROR 226 | raise # pragma: no cover 227 | 228 | return wrapper 229 | 230 | 231 | def strs_wrapper(f, param_indices=None): 232 | """wrapper for functions that take strings, does excel style conversions 233 | 234 | :param f: function to wrap 235 | :param param_indices: params to coerce to strings. 236 | int: param number to convert 237 | tuple: params to convert 238 | None: convert all params 239 | :return: wrapped function 240 | """ 241 | param_indices = convert_params_indices(f, param_indices) 242 | 243 | @functools.wraps(f) 244 | def wrapper(*args): 245 | new_args = tuple(coerce_to_string(a) 246 | if i in param_indices else a 247 | for i, a in enumerate(args)) 248 | error = next((a for i, a in enumerate(new_args) 249 | if i in param_indices and a in ERROR_CODES), None) 250 | if error: 251 | return error 252 | 253 | return f(*new_args) 254 | 255 | return wrapper 256 | 257 | 258 | def error_string_wrapper(f, param_indices=None): 259 | """wrapper to process error strings in arguments 260 | 261 | :param f: function to wrap 262 | :param param_indices: params to check for error strings. 263 | int: param number to check 264 | tuple: params to check 265 | None: check all params 266 | :return: wrapped function 267 | """ 268 | param_indices = sorted(convert_params_indices(f, param_indices)) 269 | 270 | @functools.wraps(f) 271 | def wrapper(*args): 272 | for arg_num in param_indices: 273 | try: 274 | arg = args[arg_num] 275 | except IndexError: 276 | break 277 | if isinstance(arg, str) and arg in ERROR_CODES: 278 | return arg 279 | elif isinstance(arg, tuple): 280 | error = next((a for a in flatten(arg) 281 | if isinstance(a, str) and a in ERROR_CODES), None) 282 | if error is not None: 283 | return error 284 | 285 | return f(*args) 286 | 287 | return wrapper 288 | 289 | 290 | def refs_wrapper(f, name_space, param_indices=None): 291 | """wrapper to process references in arguments 292 | 293 | :param f: function to wrap 294 | :param param_indices: params to check for error strings. 295 | int: param number to check 296 | tuple: params to check 297 | None: check all params 298 | :return: wrapped function 299 | """ 300 | param_indices = convert_params_indices(f, param_indices) 301 | 302 | _R_ = name_space.get('_R_') 303 | _C_ = name_space.get('_C_') 304 | 305 | def resolve_args(args): 306 | for arg_num, arg in enumerate(args): 307 | if arg_num in param_indices: 308 | yield arg 309 | elif isinstance(arg, AddressCell): 310 | # resolve cell if this is not reference param 311 | yield _C_(arg.address) 312 | elif isinstance(arg, AddressRange): 313 | # resolve range if this is not reference param 314 | yield _R_(arg.address) 315 | else: 316 | yield arg 317 | 318 | @functools.wraps(f) 319 | def wrapper(*args): 320 | return f(*tuple(resolve_args(args))) 321 | 322 | return wrapper 323 | 324 | 325 | def built_in_wrapper(f, wrapper_marker, name_space): 326 | meta = getattr(wrapper_marker(lambda x: x), FUNC_META) # pragma: no branch 327 | return apply_meta(f, meta, name_space)[0] 328 | 329 | 330 | def load_functions(names, name_space, modules): 331 | # load desired functions into namespace from modules 332 | not_found = set() 333 | for name in names: 334 | if name not in name_space: 335 | funcs = ((getattr(module, name, None), module) 336 | for module in modules) 337 | f, module = next( 338 | (f for f in funcs if f[0] is not None), (None, None)) 339 | if f is None: 340 | not_found.add(name) 341 | else: 342 | if module.__name__ == 'math': 343 | f = built_in_wrapper( 344 | f, excel_math_func, name_space=name_space) 345 | else: 346 | f, meta = apply_meta(f, name_space=name_space) 347 | name_space[name] = f 348 | 349 | return not_found 350 | 351 | 352 | def load_to_test_module(load_from, load_to_name): 353 | # dynamic load the lib functions from 'load_from' and apply metadata 354 | load_to = sys.modules[load_to_name] 355 | for name in dir(load_from): 356 | obj = getattr(load_from, name) 357 | if callable(obj) and getattr(load_to, name, None) == obj: 358 | setattr(load_to, name, apply_meta(obj, name_space={})[0]) 359 | -------------------------------------------------------------------------------- /src/pycel/lib/engineering.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Python equivalents of Engineering library functions 12 | """ 13 | import functools 14 | 15 | from pycel.excelutil import EMPTY, ERROR_CODES, flatten, NUM_ERROR, VALUE_ERROR 16 | from pycel.lib.function_helpers import ( 17 | excel_math_func, 18 | ) 19 | 20 | 21 | _SIZE_MASK = {2: 512, 8: 0x20000000, 16: 0x8000000000} 22 | _BASE_TO_FUNC = {2: bin, 8: oct, 16: hex} 23 | 24 | 25 | def _base2dec(value, base): 26 | value = list(flatten(value)) 27 | if len(value) != 1 or isinstance(value[0], bool): 28 | return VALUE_ERROR 29 | 30 | value = value[0] 31 | if value in ERROR_CODES: 32 | return value 33 | 34 | if value in (None, EMPTY): 35 | value = '0' 36 | elif isinstance(value, (int, float)) and value >= 0: 37 | if int(value) == value: 38 | value = str(int(value)) 39 | 40 | if isinstance(value, str) and len(value) <= 10: 41 | try: 42 | value, mask = int(value, base), _SIZE_MASK[base] 43 | if value >= 0: 44 | return (value & ~mask) - (value & mask) 45 | except ValueError: 46 | return NUM_ERROR 47 | return NUM_ERROR 48 | 49 | 50 | def _dec2base(value, places=None, base=16): 51 | value = list(flatten(value)) 52 | if len(value) != 1 or isinstance(value[0], bool): 53 | return VALUE_ERROR 54 | 55 | value = value[0] 56 | if value in ERROR_CODES: 57 | return value 58 | 59 | if value in (None, EMPTY): 60 | if base == 8: 61 | return NUM_ERROR 62 | value = 0 63 | 64 | try: 65 | value = int(value) 66 | except ValueError: 67 | return VALUE_ERROR 68 | 69 | mask = _SIZE_MASK[base] 70 | if not (-mask <= value < mask): 71 | return NUM_ERROR 72 | 73 | if value < 0: 74 | value += mask << 1 75 | 76 | value = _BASE_TO_FUNC[base](value)[2:].upper() 77 | if places is None: 78 | places = 0 79 | else: 80 | places = int(places) 81 | if places < len(value): 82 | return NUM_ERROR 83 | return value.zfill(int(places)) 84 | 85 | 86 | def _base2base(value, places=None, base_in=16, base_out=16): 87 | if value is None: 88 | if base_out == 10 or base_in != 2: 89 | value = 0 90 | else: 91 | return NUM_ERROR 92 | return _dec2base(_base2dec(value, base_in), places=places, base=base_out) 93 | 94 | 95 | # def besseli(value): 96 | # # Excel reference: https://support.microsoft.com/en-us/office/ 97 | # # besseli-function-8d33855c-9a8d-444b-98e0-852267b1c0df 98 | 99 | 100 | # def besselj(value): 101 | # # Excel reference: https://support.microsoft.com/en-us/office/ 102 | # # besselj-function-839cb181-48de-408b-9d80-bd02982d94f7 103 | 104 | 105 | # def besselk(value): 106 | # # Excel reference: https://support.microsoft.com/en-us/office/ 107 | # # besselk-function-606d11bc-06d3-4d53-9ecb-2803e2b90b70 108 | 109 | 110 | # def bessely(value): 111 | # # Excel reference: https://support.microsoft.com/en-us/office/ 112 | # # bessely-function-f3a356b3-da89-42c3-8974-2da54d6353a2 113 | 114 | 115 | # Excel reference: https://support.microsoft.com/en-us/office/ 116 | # BIN2DEC-function-63905B57-B3A0-453D-99F4-647BB519CD6C 117 | bin2dec = functools.partial(_base2dec, base=2) 118 | 119 | 120 | # Excel reference: https://support.microsoft.com/en-us/office/ 121 | # BIN2HEX-function-0375E507-F5E5-4077-9AF8-28D84F9F41CC 122 | bin2hex = functools.partial(_base2base, base_in=2, base_out=16) 123 | 124 | 125 | # Excel reference: https://support.microsoft.com/en-us/office/ 126 | # BIN2OCT-function-0A4E01BA-AC8D-4158-9B29-16C25C4C23FD 127 | bin2oct = functools.partial(_base2base, base_in=2, base_out=8) 128 | 129 | 130 | @excel_math_func 131 | def bitand(op_x, op_y): 132 | # Excel reference: https://support.microsoft.com/en-us/office/ 133 | # bitand-function-8a2be3d7-91c3-4b48-9517-64548008563a 134 | if op_x < 0 or op_y < 0: 135 | return NUM_ERROR 136 | return op_x & op_y 137 | 138 | 139 | @excel_math_func 140 | def bitlshift(number, pos): 141 | # Excel reference: https://support.microsoft.com/en-us/office/ 142 | # bitlshift-function-c55bb27e-cacd-4c7c-b258-d80861a03c9c 143 | if number < 0 or abs(pos) > 53 or number >= 2**48: 144 | return NUM_ERROR 145 | if pos < 0: 146 | return bitrshift(number, abs(pos)) 147 | return number << pos 148 | 149 | 150 | @excel_math_func 151 | def bitor(op_x, op_y): 152 | # Excel reference: https://support.microsoft.com/en-us/office/ 153 | # bitor-function-f6ead5c8-5b98-4c9e-9053-8ad5234919b2 154 | if op_x < 0 or op_y < 0: 155 | return NUM_ERROR 156 | return op_x | op_y 157 | 158 | 159 | @excel_math_func 160 | def bitrshift(number, pos): 161 | # Excel reference: https://support.microsoft.com/en-us/office/ 162 | # bitrshift-function-274d6996-f42c-4743-abdb-4ff95351222c 163 | if number < 0 or abs(pos) > 53 or number >= 2 ** 48: 164 | return NUM_ERROR 165 | if pos < 0: 166 | return bitlshift(number, abs(pos)) 167 | return number >> pos 168 | 169 | 170 | @excel_math_func 171 | def bitxor(op_x, op_y): 172 | # Excel reference: https://support.microsoft.com/en-us/office/ 173 | # bitxor-function-c81306a1-03f9-4e89-85ac-b86c3cba10e4 174 | if op_x < 0 or op_y < 0: 175 | return NUM_ERROR 176 | return op_x ^ op_y 177 | 178 | 179 | # def complex(value): 180 | # # Excel reference: https://support.microsoft.com/en-us/office/ 181 | # # complex-function-f0b8f3a9-51cc-4d6d-86fb-3a9362fa4128 182 | 183 | 184 | # def convert(value): 185 | # # Excel reference: https://support.microsoft.com/en-us/office/ 186 | # # convert-function-d785bef1-808e-4aac-bdcd-666c810f9af2 187 | 188 | 189 | # Excel reference: https://support.microsoft.com/en-us/office/ 190 | # DEC2BIN-function-0F63DD0E-5D1A-42D8-B511-5BF5C6D43838 191 | dec2bin = functools.partial(_dec2base, base=2) 192 | 193 | 194 | # Excel reference: https://support.microsoft.com/en-us/office/ 195 | # DEC2HEX-function-6344EE8B-B6B5-4C6A-A672-F64666704619 196 | dec2hex = functools.partial(_dec2base, base=16) 197 | 198 | 199 | # Excel reference: https://support.microsoft.com/en-us/office/ 200 | # DEC2OCT-function-C9D835CA-20B7-40C4-8A9E-D3BE351CE00F 201 | dec2oct = functools.partial(_dec2base, base=8) 202 | 203 | 204 | # def delta(value): 205 | # # Excel reference: https://support.microsoft.com/en-us/office/ 206 | # # delta-function-2f763672-c959-4e07-ac33-fe03220ba432 207 | 208 | 209 | # def erf(value): 210 | # # Excel reference: https://support.microsoft.com/en-us/office/ 211 | # # erf-function-c53c7e7b-5482-4b6c-883e-56df3c9af349 212 | 213 | 214 | # def erf.precise(value): 215 | # # Excel reference: https://support.microsoft.com/en-us/office/ 216 | # # erf-precise-function-9a349593-705c-4278-9a98-e4122831a8e0 217 | 218 | 219 | # def erfc(value): 220 | # # Excel reference: https://support.microsoft.com/en-us/office/ 221 | # # erfc-function-736e0318-70ba-4e8b-8d08-461fe68b71b3 222 | 223 | 224 | # def erfc.precise(value): 225 | # # Excel reference: https://support.microsoft.com/en-us/office/ 226 | # # erfc-precise-function-e90e6bab-f45e-45df-b2ac-cd2eb4d4a273 227 | 228 | 229 | # def gestep(value): 230 | # # Excel reference: https://support.microsoft.com/en-us/office/ 231 | # # gestep-function-f37e7d2a-41da-4129-be95-640883fca9df 232 | 233 | 234 | # Excel reference: https://support.microsoft.com/en-us/office/ 235 | # HEX2BIN-function-A13AAFAA-5737-4920-8424-643E581828C1 236 | hex2bin = functools.partial(_base2base, base_in=16, base_out=2) 237 | 238 | 239 | # Excel reference: https://support.microsoft.com/en-us/office/ 240 | # HEX2DEC-function-8C8C3155-9F37-45A5-A3EE-EE5379EF106E 241 | hex2dec = functools.partial(_base2dec, base=16) 242 | 243 | 244 | # Excel reference: https://support.microsoft.com/en-us/office/ 245 | # HEX2OCT-function-54D52808-5D19-4BD0-8A63-1096A5D11912 246 | hex2oct = functools.partial(_base2base, base_in=16, base_out=8) 247 | 248 | 249 | # def imabs(value): 250 | # # Excel reference: https://support.microsoft.com/en-us/office/ 251 | # # imabs-function-b31e73c6-d90c-4062-90bc-8eb351d765a1 252 | 253 | 254 | # def imaginary(value): 255 | # # Excel reference: https://support.microsoft.com/en-us/office/ 256 | # # imaginary-function-dd5952fd-473d-44d9-95a1-9a17b23e428a 257 | 258 | 259 | # def imargument(value): 260 | # # Excel reference: https://support.microsoft.com/en-us/office/ 261 | # # imargument-function-eed37ec1-23b3-4f59-b9f3-d340358a034a 262 | 263 | 264 | # def imconjugate(value): 265 | # # Excel reference: https://support.microsoft.com/en-us/office/ 266 | # # imconjugate-function-2e2fc1ea-f32b-4f9b-9de6-233853bafd42 267 | 268 | 269 | # def imcos(value): 270 | # # Excel reference: https://support.microsoft.com/en-us/office/ 271 | # # imcos-function-dad75277-f592-4a6b-ad6c-be93a808a53c 272 | 273 | 274 | # def imcosh(value): 275 | # # Excel reference: https://support.microsoft.com/en-us/office/ 276 | # # imcosh-function-053e4ddb-4122-458b-be9a-457c405e90ff 277 | 278 | 279 | # def imcot(value): 280 | # # Excel reference: https://support.microsoft.com/en-us/office/ 281 | # # imcot-function-dc6a3607-d26a-4d06-8b41-8931da36442c 282 | 283 | 284 | # def imcsc(value): 285 | # # Excel reference: https://support.microsoft.com/en-us/office/ 286 | # # imcsc-function-9e158d8f-2ddf-46cd-9b1d-98e29904a323 287 | 288 | 289 | # def imcsch(value): 290 | # # Excel reference: https://support.microsoft.com/en-us/office/ 291 | # # imcsch-function-c0ae4f54-5f09-4fef-8da0-dc33ea2c5ca9 292 | 293 | 294 | # def imdiv(value): 295 | # # Excel reference: https://support.microsoft.com/en-us/office/ 296 | # # imdiv-function-a505aff7-af8a-4451-8142-77ec3d74d83f 297 | 298 | 299 | # def imexp(value): 300 | # # Excel reference: https://support.microsoft.com/en-us/office/ 301 | # # imexp-function-c6f8da1f-e024-4c0c-b802-a60e7147a95f 302 | 303 | 304 | # def imln(value): 305 | # # Excel reference: https://support.microsoft.com/en-us/office/ 306 | # # imln-function-32b98bcf-8b81-437c-a636-6fb3aad509d8 307 | 308 | 309 | # def imlog10(value): 310 | # # Excel reference: https://support.microsoft.com/en-us/office/ 311 | # # imlog10-function-58200fca-e2a2-4271-8a98-ccd4360213a5 312 | 313 | 314 | # def imlog2(value): 315 | # # Excel reference: https://support.microsoft.com/en-us/office/ 316 | # # imlog2-function-152e13b4-bc79-486c-a243-e6a676878c51 317 | 318 | 319 | # def impower(value): 320 | # # Excel reference: https://support.microsoft.com/en-us/office/ 321 | # # impower-function-210fd2f5-f8ff-4c6a-9d60-30e34fbdef39 322 | 323 | 324 | # def improduct(value): 325 | # # Excel reference: https://support.microsoft.com/en-us/office/ 326 | # # improduct-function-2fb8651a-a4f2-444f-975e-8ba7aab3a5ba 327 | 328 | 329 | # def imreal(value): 330 | # # Excel reference: https://support.microsoft.com/en-us/office/ 331 | # # imreal-function-d12bc4c0-25d0-4bb3-a25f-ece1938bf366 332 | 333 | 334 | # def imsec(value): 335 | # # Excel reference: https://support.microsoft.com/en-us/office/ 336 | # # imsec-function-6df11132-4411-4df4-a3dc-1f17372459e0 337 | 338 | 339 | # def imsech(value): 340 | # # Excel reference: https://support.microsoft.com/en-us/office/ 341 | # # imsech-function-f250304f-788b-4505-954e-eb01fa50903b 342 | 343 | 344 | # def imsin(value): 345 | # # Excel reference: https://support.microsoft.com/en-us/office/ 346 | # # imsin-function-1ab02a39-a721-48de-82ef-f52bf37859f6 347 | 348 | 349 | # def imsinh(value): 350 | # # Excel reference: https://support.microsoft.com/en-us/office/ 351 | # # imsinh-function-dfb9ec9e-8783-4985-8c42-b028e9e8da3d 352 | 353 | 354 | # def imsqrt(value): 355 | # # Excel reference: https://support.microsoft.com/en-us/office/ 356 | # # imsqrt-function-e1753f80-ba11-4664-a10e-e17368396b70 357 | 358 | 359 | # def imsub(value): 360 | # # Excel reference: https://support.microsoft.com/en-us/office/ 361 | # # imsub-function-2e404b4d-4935-4e85-9f52-cb08b9a45054 362 | 363 | 364 | # def imsum(value): 365 | # # Excel reference: https://support.microsoft.com/en-us/office/ 366 | # # imsum-function-81542999-5f1c-4da6-9ffe-f1d7aaa9457f 367 | 368 | 369 | # def imtan(value): 370 | # # Excel reference: https://support.microsoft.com/en-us/office/ 371 | # # imtan-function-8478f45d-610a-43cf-8544-9fc0b553a132 372 | 373 | 374 | # Excel reference: https://support.microsoft.com/en-us/office/ 375 | # OCT2BIN-function-55383471-3C56-4D27-9522-1A8EC646C589 376 | oct2bin = functools.partial(_base2base, base_in=8, base_out=2) 377 | 378 | 379 | # Excel reference: https://support.microsoft.com/en-us/office/ 380 | # OCT2DEC-function-87606014-CB98-44B2-8DBB-E48F8CED1554 381 | oct2dec = functools.partial(_base2dec, base=8) 382 | 383 | 384 | # Excel reference: https://support.microsoft.com/en-us/office/ 385 | # OCT2HEX-function-912175B4-D497-41B4-A029-221F051B858F 386 | oct2hex = functools.partial(_base2base, base_in=8, base_out=16) 387 | -------------------------------------------------------------------------------- /src/pycel/excellib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Python equivalents of various excel functions 12 | """ 13 | import math 14 | import sys 15 | from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP, ROUND_UP 16 | 17 | import numpy as np 18 | 19 | from pycel.excelutil import ( 20 | coerce_to_number, 21 | DIV0, 22 | ERROR_CODES, 23 | flatten, 24 | handle_ifs, 25 | is_array_arg, 26 | is_number, 27 | list_like, 28 | NA_ERROR, 29 | NUM_ERROR, 30 | VALUE_ERROR, 31 | ) 32 | from pycel.lib.function_helpers import ( 33 | excel_helper, 34 | excel_math_func, 35 | ) 36 | 37 | 38 | if sys.version_info >= (3, 8): # pragma: no cover 39 | prod = math.prod 40 | else: # pragma: no cover 41 | # ::TODO:: remove when Pyton 3.7 is obsolete 42 | def prod(values): 43 | return np.prod(list(values)) 44 | 45 | 46 | def _numerics(*args, keep_bools=False, to_number=lambda x: x): 47 | # ignore non numeric cells 48 | args = tuple(flatten(args)) 49 | error = next((x for x in args if x in ERROR_CODES), None) 50 | if error is not None: 51 | # return the first error in the list 52 | return error 53 | else: 54 | args = ( 55 | to_number(a) for a in args if keep_bools or not isinstance(a, bool) 56 | ) 57 | return tuple(x for x in args if isinstance(x, (int, float))) 58 | 59 | 60 | @excel_math_func 61 | def abs_(value1): 62 | # Excel reference: https://support.microsoft.com/en-us/office/ 63 | # ABS-function-3420200F-5628-4E8C-99DA-C99D7C87713C 64 | return abs(value1) 65 | 66 | 67 | @excel_math_func 68 | def atan2_(x_num, y_num): 69 | # Excel reference: https://support.microsoft.com/en-us/office/ 70 | # ATAN2-function-C04592AB-B9E3-4908-B428-C96B3A565033 71 | 72 | # swap arguments 73 | return math.atan2(y_num, x_num) 74 | 75 | 76 | @excel_math_func 77 | def ceiling(number, significance): 78 | # Excel reference: https://support.microsoft.com/en-us/office/ 79 | # CEILING-function-0A5CD7C8-0720-4F0A-BD2C-C943E510899F 80 | if significance < 0 < number: 81 | return NUM_ERROR 82 | 83 | if number == 0 or significance == 0: 84 | return 0 85 | 86 | if number < 0 < significance: 87 | return significance * int(number / significance) 88 | else: 89 | return significance * math.ceil(number / significance) 90 | 91 | 92 | @excel_math_func 93 | def ceiling_math(number, significance=1, mode=0): 94 | # Excel reference: https://support.microsoft.com/en-us/office/ 95 | # ceiling-math-function-80f95d2f-b499-4eee-9f16-f795a8e306c8 96 | if significance == 0: 97 | return 0 98 | 99 | significance = abs(significance) 100 | if mode and number < 0: 101 | significance = -significance 102 | return significance * math.ceil(number / significance) 103 | 104 | 105 | @excel_math_func 106 | def ceiling_precise(number, significance=1): 107 | # Excel reference: https://support.microsoft.com/en-us/office/ 108 | # ceiling-precise-function-f366a774-527a-4c92-ba49-af0a196e66cb 109 | if significance == 0: 110 | return 0 111 | 112 | significance = abs(significance) 113 | return significance * math.ceil(number / significance) 114 | 115 | 116 | def conditional_format_ids(*args): 117 | """helper function for getting conditional format ids""" 118 | # Excel reference: https://support.microsoft.com/en-us/office/ 119 | # E09711A3-48DF-4BCB-B82C-9D8B8B22463D 120 | 121 | results = [] 122 | for condition, dxf_id, stop_if_true in args: 123 | if condition: 124 | results.append(dxf_id) 125 | if stop_if_true: 126 | break 127 | return tuple(results) 128 | 129 | 130 | @excel_math_func 131 | def even(value): 132 | # Excel reference: https://support.microsoft.com/en-us/office/ 133 | # even-function-197b5f06-c795-4c1e-8696-3c3b8a646cf9 134 | return math.copysign(math.ceil(abs(value) / 2) * 2, value) 135 | 136 | 137 | @excel_math_func 138 | def fact(value): 139 | # Excel reference: https://support.microsoft.com/en-us/office/ 140 | # fact-function-ca8588c2-15f2-41c0-8e8c-c11bd471a4f3 141 | return math.factorial(int(value)) if value >= 0 else NUM_ERROR 142 | 143 | 144 | @excel_helper(cse_params=-1) 145 | def factdouble(value): 146 | # Excel reference: https://support.microsoft.com/en-us/office/ 147 | # fact-function-ca8588c2-15f2-41c0-8e8c-c11bd471a4f3 148 | if isinstance(value, bool): 149 | return VALUE_ERROR 150 | value = coerce_to_number(value, convert_all=True) 151 | if isinstance(value, str): 152 | return VALUE_ERROR 153 | if value < 0: 154 | return NUM_ERROR 155 | 156 | return np.sum(np.prod(range(int(value), 0, -2), axis=0)) 157 | 158 | 159 | @excel_math_func 160 | def floor(number, significance): 161 | # Excel reference: https://support.microsoft.com/en-us/office/ 162 | # FLOOR-function-14BB497C-24F2-4E04-B327-B0B4DE5A8886 163 | if significance < 0 < number: 164 | return NUM_ERROR 165 | 166 | if number == 0: 167 | return 0 168 | 169 | if significance == 0: 170 | return DIV0 171 | 172 | return significance * math.floor(number / significance) 173 | 174 | 175 | @excel_math_func 176 | def floor_math(number, significance=1, mode=0): 177 | # Excel reference: https://support.microsoft.com/en-us/office/ 178 | # floor-math-function-c302b599-fbdb-4177-ba19-2c2b1249a2f5 179 | if significance == 0: 180 | return 0 181 | 182 | significance = abs(significance) 183 | if mode and number < 0: 184 | significance = -significance 185 | return significance * math.floor(number / significance) 186 | 187 | 188 | @excel_math_func 189 | def floor_precise(number, significance=1): 190 | # Excel reference: https://support.microsoft.com/en-us/office/ 191 | # floor-precise-function-f769b468-1452-4617-8dc3-02f842a0702e 192 | if significance == 0: 193 | return 0 194 | 195 | significance = abs(significance) 196 | return significance * math.floor(number / significance) 197 | 198 | 199 | @excel_math_func 200 | def int_(value1): 201 | # Excel reference: https://support.microsoft.com/en-us/office/ 202 | # INT-function-A6C4AF9E-356D-4369-AB6A-CB1FD9D343EF 203 | return math.floor(value1) 204 | 205 | 206 | @excel_math_func 207 | def ln(arg): 208 | # Excel reference: https://support.microsoft.com/en-us/office/ 209 | # LN-function-81FE1ED7-DAC9-4ACD-BA1D-07A142C6118F 210 | return math.log(arg) 211 | 212 | 213 | @excel_math_func 214 | def log(number, base=10): 215 | # Excel reference: https://support.microsoft.com/en-us/office/ 216 | # LOG-function-4E82F196-1CA9-4747-8FB0-6C4A3ABB3280 217 | return math.log(number, base) 218 | 219 | 220 | @excel_math_func 221 | def mod(number, divisor): 222 | # Excel reference: https://support.microsoft.com/en-us/office/ 223 | # MOD-function-9b6cd169-b6ee-406a-a97b-edf2a9dc24f3 224 | if divisor == 0: 225 | return DIV0 226 | 227 | return number % divisor 228 | 229 | 230 | @excel_helper(cse_params=None, err_str_params=-1, number_params=0) 231 | def npv(rate, *args): 232 | # Excel reference: https://support.microsoft.com/en-us/office/ 233 | # NPV-function-8672CB67-2576-4D07-B67B-AC28ACF2A568 234 | 235 | rate += 1 236 | cashflow = [x for x in flatten(args, coerce=coerce_to_number) 237 | if is_number(x) and not isinstance(x, bool)] 238 | return sum(x * rate ** -i for i, x in enumerate(cashflow, start=1)) 239 | 240 | 241 | @excel_math_func 242 | def odd(value): 243 | # Excel reference: https://support.microsoft.com/en-us/office/ 244 | # odd-function-deae64eb-e08a-4c88-8b40-6d0b42575c98 245 | return math.copysign(math.ceil((abs(value) - 1) / 2) * 2 + 1, value) 246 | 247 | 248 | @excel_math_func 249 | def power(number, power): 250 | # Excel reference: https://support.microsoft.com/en-us/office/ 251 | # POWER-function-D3F2908B-56F4-4C3F-895A-07FB519C362A 252 | if number == power == 0: 253 | # Really excel? What were you thinking? 254 | return NA_ERROR 255 | 256 | try: 257 | return number ** power 258 | except ZeroDivisionError: 259 | return DIV0 260 | 261 | 262 | @excel_math_func 263 | def pv(rate, nper, pmt, fv=0, type_=0): 264 | # Excel reference: https://support.microsoft.com/en-us/office/ 265 | # pv-function-23879d31-0e02-4321-be01-da16e8168cbd 266 | 267 | if rate != 0: 268 | val = pmt * (1 + rate * type_) * ((1 + rate) ** nper - 1) / rate 269 | return 1 / (1 + rate) ** nper * (-fv - val) 270 | else: 271 | return -fv - pmt * nper 272 | 273 | 274 | @excel_math_func 275 | def round_(number, num_digits=0): 276 | # Excel reference: https://support.microsoft.com/en-us/office/ 277 | # ROUND-function-c018c5d8-40fb-4053-90b1-b3e7f61a213c 278 | 279 | num_digits = int(num_digits) 280 | if num_digits >= 0: # round to the right side of the point 281 | return float(Decimal(repr(number)).quantize( 282 | Decimal(repr(pow(10, -num_digits))), 283 | rounding=ROUND_HALF_UP 284 | )) 285 | # see https://docs.python.org/2/library/functions.html#round 286 | # and https://gist.github.com/ejamesc/cedc886c5f36e2d075c5 287 | 288 | else: 289 | return round(number, num_digits) 290 | 291 | 292 | def _round(number, num_digits, rounding): 293 | num_digits = int(num_digits) 294 | quant = Decimal(f'1E{"+-"[num_digits >= 0]}{abs(num_digits)}') 295 | return float(Decimal(repr(number)).quantize(quant, rounding=rounding)) 296 | 297 | 298 | @excel_math_func 299 | def rounddown(number, num_digits): 300 | # Excel reference: https://support.microsoft.com/en-us/office/ 301 | # ROUNDDOWN-function-2EC94C73-241F-4B01-8C6F-17E6D7968F53 302 | return _round(number, num_digits, rounding=ROUND_DOWN) 303 | 304 | 305 | @excel_math_func 306 | def roundup(number, num_digits): 307 | # Excel reference: https://support.microsoft.com/en-us/office/ 308 | # ROUNDUP-function-F8BC9B23-E795-47DB-8703-DB171D0C42A7 309 | return _round(number, num_digits, rounding=ROUND_UP) 310 | 311 | 312 | @excel_math_func 313 | def sign(value): 314 | # Excel reference: https://support.microsoft.com/en-us/office/ 315 | # sign-function-109c932d-fcdc-4023-91f1-2dd0e916a1d8 316 | return -1 if value < 0 else int(bool(value)) 317 | 318 | 319 | def sum_(*args): 320 | data = _numerics(*args) 321 | if isinstance(data, str): 322 | return data 323 | 324 | # if no non numeric cells, return zero (is what excel does) 325 | return sum(data) 326 | 327 | 328 | def sumif(rng, criteria, sum_range=None): 329 | # Excel reference: https://support.microsoft.com/en-us/office/ 330 | # SUMIF-function-169b8c99-c05c-4483-a712-1697a653039b 331 | 332 | # WARNING: 333 | # - The following is not currently implemented: 334 | # The sum_range argument does not have to be the same size and shape as 335 | # the range argument. The actual cells that are added are determined by 336 | # using the upper leftmost cell in the sum_range argument as the 337 | # beginning cell, and then including cells that correspond in size and 338 | # shape to the range argument. 339 | 340 | if sum_range is None: 341 | sum_range = rng 342 | return sumifs(sum_range, rng, criteria) 343 | 344 | 345 | def sumifs(sum_range, *args): 346 | # Excel reference: https://support.microsoft.com/en-us/office/ 347 | # SUMIFS-function-C9E748F5-7EA7-455D-9406-611CEBCE642B 348 | if not list_like(sum_range): 349 | sum_range = ((sum_range, ), ) 350 | 351 | coords = handle_ifs(args, sum_range) 352 | 353 | # A returned string is an error code 354 | if isinstance(coords, str): 355 | return coords 356 | 357 | return sum(_numerics( 358 | (sum_range[r][c] for r, c in coords), 359 | keep_bools=True 360 | )) 361 | 362 | 363 | def sumproduct(*args): 364 | # Excel reference: https://support.microsoft.com/en-us/office/ 365 | # SUMPRODUCT-function-16753E75-9F68-4874-94AC-4D2145A2FD2E 366 | 367 | # find any errors 368 | error = next((i for i in flatten(args) if i in ERROR_CODES), None) 369 | if error: 370 | return error 371 | 372 | # verify array sizes match 373 | sizes = set() 374 | for arg in args: 375 | if not isinstance(arg, tuple): 376 | if all(not isinstance(arg, tuple) for arg in args): 377 | # the all scalers case is valid. 378 | values = ( 379 | x if isinstance(x, (float, int, type(None))) and not isinstance(x, bool) else 0 380 | for x in args 381 | ) 382 | try: 383 | return prod(values) 384 | except TypeError: 385 | pass 386 | return VALUE_ERROR 387 | assert is_array_arg(arg) 388 | sizes.add((len(arg), len(arg[0]))) 389 | if len(sizes) != 1: 390 | return VALUE_ERROR 391 | 392 | # put the values into numpy vectors 393 | values = np.array(tuple(tuple( 394 | x if isinstance(x, (float, int)) and not isinstance(x, bool) else 0 395 | for x in flatten(arg)) for arg in args)) 396 | 397 | # return the sum product 398 | return np.sum(np.prod(values, axis=0)) 399 | 400 | 401 | @excel_math_func 402 | def trunc(number, num_digits=0): 403 | # Excel reference: https://support.microsoft.com/en-us/office/ 404 | # TRUNC-function-8B86A64C-3127-43DB-BA14-AA5CEB292721 405 | factor = 10 ** int(num_digits) 406 | return int(number * factor) / factor 407 | 408 | 409 | # Older mappings for excel functions that match Python built-in and keywords 410 | x_abs = abs_ 411 | xatan2 = atan2_ 412 | x_int = int_ 413 | x_round = round_ 414 | xsum = sum_ 415 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ######### 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on `Keep a Changelog `_, 7 | and this project adheres to `Semantic Versioning `_. 8 | 9 | .. keepachangelog headings 10 | 11 | [unreleased] 12 | ============ 13 | Added 14 | ----- 15 | Changed 16 | ------- 17 | Deprecated 18 | ---------- 19 | Removed 20 | ------- 21 | Fixed 22 | ----- 23 | Security 24 | -------- 25 | 26 | 27 | [unreleased] 28 | ============ 29 | 30 | Added 31 | ----- 32 | 33 | * Added SWITCH() function 34 | 35 | Changed 36 | ------- 37 | 38 | * Allow continued calculations after UnknownFunction exception (thanks @igheorghita) 39 | 40 | Fixed 41 | ----- 42 | 43 | * Fixed SUMPRODUCT() for scalar case (thanks @igheorghita) 44 | 45 | 46 | [1.0b30] - 2021-10-13 47 | ===================== 48 | 49 | Changed 50 | ------- 51 | 52 | - Better handle indirect cells in compiled workbooks 53 | - Better handle numpy floats in compiled workbooks 54 | 55 | 56 | [1.0b29] - 2021-09-13 57 | ===================== 58 | 59 | Added 60 | ----- 61 | 62 | - Add support for openpyxl >= 3.0.8 63 | 64 | 65 | [1.0b28] - 2021-08-31 66 | ===================== 67 | 68 | Added 69 | ----- 70 | 71 | - Add support for networkx 2.6 72 | 73 | Changed 74 | ------- 75 | 76 | - Some minor improvements for Iterative Calculations 77 | 78 | 79 | [1.0b27] - 2021-07-10 80 | ===================== 81 | 82 | Added 83 | ----- 84 | 85 | * Added CHOOSE() function 86 | * Added FORECAST() function 87 | * Added INTERCEPT() function 88 | * Added IFNA() function 89 | * Added ISBLANK() function 90 | * Added ISLOGICAL() function 91 | * Added ISNONTEXT() function 92 | * Added N() function 93 | * Added NA() function 94 | * Added SLOPE() function 95 | * Added SUBSTITUTE() function 96 | * Added TEXT() function (Thanks, Luckykarter) 97 | * Added TREND() function 98 | * Added Reference Form for INDEX() 99 | * Added str_params to excel_helper() 100 | * Added ExcelCompiler.validate_serialized() 101 | 102 | Changed 103 | ------- 104 | 105 | * Improve LINEST() compatibilty w/ Excel 106 | * Improve TEXT() compatibilty w/ Excel 107 | * Improve error and number handling in some Text functions 108 | * Improve IFS() to support array context 109 | * Missing references from INDIRECT() and OFFSET() resolve more often 110 | 111 | Fixed 112 | ----- 113 | 114 | * Fix #111, Incorrect implementation of YEARFRAC 115 | * Fixed some exceptions in LINEST() 116 | * Fix serialize ranges with formulas 117 | * Fixed a minor bug in DATE() 118 | * Fixed TIMEVALUE() parsing for elapsed times 119 | 120 | 121 | [1.0b26] - 2021-06-18 122 | ===================== 123 | 124 | Added 125 | ----- 126 | 127 | * Python 3.9 now supported 128 | * Add bitwise functions: bitand, bitor, bitxor, bitlshift and bitrshift (Thanks, bogdan-oprescu-nxp) 129 | * Add PV function (Thanks, estandiaa-marain) 130 | 131 | Changed 132 | ------- 133 | 134 | * Allow plugins to be passed to the deserialization function from_file() (Thanks, nanaposo) 135 | 136 | Removed 137 | ------- 138 | 139 | * Drop support for Python 3.5 140 | 141 | Fixed 142 | ----- 143 | * Fix openpyxl >= 3.0.4 (Thanks, ckp95) 144 | * Fix HLOOKUP row_index_num validation to use num rows (Thanks, nanaposo) 145 | * Fix #86, tokenize.TokenError: ('EOF in multi-line statement', 146 | * Fix #88, Handle calcPR in workbook (Thanks, andreif) 147 | * Fix #89, NPV function fails when passed range of cashflows (Thanks, jpp-0) 148 | * Fix #93, AssertionError during set_value(), by adding a better error message 149 | * Fix #99, Pycel raises NotImplementedError on rectangular ranges (Thanks, rmorel) 150 | * Fix #103, build_operator_operand_fixup() throws #VALUE error when concatenating AddressCell objects (Thanks, nboukraa) 151 | * Fix #104, Insufficient coverage and testing after recent merges 152 | * Fix #105, Incorrect RPN for expressions with consecutive negations (Thanks, victorjmarin) 153 | * Fix #109, String concatenation fails for particular cases (Thanks, bogdan-oprescu-nxp) 154 | * Fix issue in =IF() when comparing to numpy result 155 | * Fix MID() and REPLACE() and LEN() in a CSE context 156 | * Fix INDEX() error handling 157 | * Fix error handling for lookup variants 158 | 159 | 160 | [1.0b22] - 2019-10-17 161 | ===================== 162 | 163 | Fixed 164 | ----- 165 | * Fix #80, incompatible w/ networkx 2.4 166 | 167 | 168 | [1.0b21] - 2019-10-13 169 | ===================== 170 | 171 | Changed 172 | ------- 173 | 174 | * Speed up compile 175 | * Implement defined names in multicolon ranges 176 | * Tokenize ':' when adjoining functions as infix operator 177 | * Various changes in prep to improve references, including 178 | * Add reference expansion to function helpers 179 | * Add sheet to indirect() and ref_param=0 to offset() 180 | * Implement is_address() helper 181 | * Implement intersection and union for AddressCell 182 | 183 | Fixed 184 | ----- 185 | * Fix #77, empty arg in IFERROR() 186 | * Fix #78, None compare and cleanup error handling for various IFS() funcs 187 | 188 | 189 | [1.0b20] - 2019-09-22 190 | ===================== 191 | 192 | Changed 193 | ------- 194 | 195 | * Implement multi colon ranges 196 | * Add support for missing (empty) function parameters 197 | 198 | Fixed 199 | ----- 200 | * Fix threading issue in iterative evaluator 201 | * Fix range intersection with null result for ROW and COLUMN 202 | * Fix #74 - Count not working for ranges 203 | 204 | 205 | [1.0b19] - 2019-09-12 206 | ===================== 207 | 208 | Changed 209 | ------- 210 | 211 | * Implement INDIRECT & OFFSET 212 | * Implement SMALL, LARGE & ROUNDDOWN (Thanks, nanaposo) 213 | * Add error message for unhandled missing function parameter 214 | 215 | Fixed 216 | ----- 217 | * Fix threading issue w/ CSE evaluator 218 | 219 | 220 | [1.0b18] - 2019-09-07 221 | ===================== 222 | 223 | Changed 224 | ------- 225 | 226 | * Implement CEILING_MATH, CEILING_PRECISION, FLOOR_MATH & FLOOR_PRECISION 227 | * Implement FACT & FACTDOUBLE 228 | * Implement AVERAGEIF, MAXIFS, MINIFS 229 | * Implement ODD, EVEN, ISODD, ISEVEN, SIGN 230 | 231 | Fixed 232 | ----- 233 | * Fix #67 - Evaluation with unbounded range 234 | * Fix bugs w/ single cells for xIFS functions 235 | 236 | 237 | [1.0b17] - 2019-09-02 238 | ===================== 239 | 240 | Changed 241 | ------- 242 | * Add Formula Support for Multi Area Ranges from defined names 243 | * Allow ExcelCompiler init from openpyxl workbook 244 | * Implement LOWER(), REPLACE(), TRIM() & UPPER() 245 | * Implement DATEVALUE(), IFS() and ISERR() (Thanks, int128t) 246 | 247 | * Reorganized time and time utils and text functions 248 | * Add excelutil.AddressMultiAreaRange. 249 | * Add abs_coordinate() property to AddressRange and AddressCell 250 | * Cleanup import statements 251 | 252 | Fixed 253 | ----- 254 | * Resolved tox version issue on travis 255 | * Fix defined names with Multi Area Range 256 | 257 | 258 | [1.0b16] - 2019-07-07 259 | ===================== 260 | 261 | Changed 262 | ------- 263 | * Add twelve date and time functions 264 | * Serialize workbook filename and use it instead of the serialization filename (Thanks, nanaposo) 265 | 266 | 267 | [1.0b15] - 2019-06-30 268 | ===================== 269 | 270 | Changed 271 | ------- 272 | * Implement AVERAGEIFS() 273 | * Take Iterative Calc Parameter defaults from workbook 274 | 275 | Fixed 276 | ----- 277 | * #60, Binder Notebook Example not Working 278 | 279 | 280 | [1.0b14] - 2019-06-16 281 | ===================== 282 | 283 | Changed 284 | ------- 285 | * Added method to evaluate the conditional format (formulas) for a cell or cells 286 | * Added ExcelCompiler(..., cycles=True) to allow Excel iterative calculations 287 | 288 | 289 | [1.0b13] - 2019-05-10 290 | ===================== 291 | 292 | Changed 293 | ------- 294 | * Implement VALUE() 295 | * Improve compile performance reversion from CSE work 296 | 297 | Fixed 298 | ----- 299 | * #54, In normalize_year(), month % 12 can be 0 -> IllegalMonthError 300 | 301 | 302 | [1.0b12] - 2019-04-22 303 | ===================== 304 | 305 | Changed 306 | ------- 307 | * Add library plugin support 308 | * Improve evaluate of unbounded row/col (ie: A:B) 309 | * Fix some regressions from 1.0b11 310 | 311 | 312 | [1.0b11] - 2019-04-21 313 | ===================== 314 | 315 | Added 316 | ----- 317 | 318 | * Implement LEFT() 319 | * Implement ISERROR() 320 | * Implement FIND() 321 | * Implement ISNUMBER() 322 | * Implement SUMPRODUCT() 323 | * Implement CEILING() 324 | * Implement TRUNC() and FLOOR() 325 | * Add support for LOG() 326 | * Improve ABS(), INT() and ROUND() 327 | 328 | * Add quoted_address() method to AddressRange and AddressCell 329 | * Add public interface to get list of formula_cells() 330 | * Add NotImplementedError for "linked" sheet names 331 | * Add reference URL to function info 332 | * Added considerable extensions to CSE Array Formula Support 333 | * Add CSE Array handling to excelformula and excelcompiler 334 | * Change Row, Column & Index to rectangular arrays only 335 | * Add in_array_formula_context 336 | * Add cse_array_wrapper() to allow calling functions in array context 337 | * Add error_string_wrapper() to check for excel errors 338 | * Move math_wrap() to function_helpers. 339 | * Handle Direct CSE Array in cell 340 | * Reorganize CSE Array Formula handling in excelwrapper 341 | * For CSE Arrays that are smaller than target fill w/ None 342 | * Trim oversize array results to fit target range 343 | * Improve needed addresses parser from python code 344 | * Improve _coerce_to_number() and _numerics() for CSE arrays 345 | * Remove formulas from excelwrapper._OpxRange() 346 | 347 | Changed 348 | ------- 349 | 350 | * Refactored ExcelWrapper, ExcelFormula & ExcelCompiler to allow... 351 | * Refactored function_helpers to add decorators for excelizing library functions 352 | * Improved various messages and exceptions in validate_calcs() and trim_graph() 353 | * Improve Some NotImplementedError() messages 354 | * Only build compiler eval context once 355 | 356 | Fixed 357 | ----- 358 | 359 | * Address Range Union and Intersection need sheet_name 360 | * Fix function info for paired functions from same line 361 | * Fix Range Intersection 362 | * Fix Unary Minus on Empty cell 363 | * Fix ISNA() 364 | * Fix AddressCell create from tuple 365 | * Power(0,-1) now returns DIV0 366 | * Cleanup index() 367 | 368 | 369 | [1.0b8] - 2019-03-20 370 | ==================== 371 | 372 | Added 373 | ----- 374 | 375 | * Implement operators for Array Formulas 376 | * Implement concatenate and concat 377 | * Implement subtotal 378 | * Add support for expanding array formulas 379 | * Add support for table relative references 380 | * Add function information methods 381 | 382 | Changed 383 | ------- 384 | 385 | * Improve messages for validate_calcs and not implemented functions 386 | 387 | Fixed 388 | ----- 389 | * Fix column and row for array formulas 390 | 391 | 392 | [1.0b7] - 2019-03-10 393 | ==================== 394 | 395 | Added 396 | ----- 397 | 398 | * Implement Array (CSE) Formulas 399 | 400 | Fixed 401 | ----- 402 | 403 | * Fix #45 - Unbounded Range Addresses (ie: A:B or 1:2) broken 404 | 405 | 406 | [1.0b6] - 2019-03-03 407 | ==================== 408 | 409 | Fixed 410 | ----- 411 | 412 | * Fix #42 - 'ReadOnlyWorksheet' object has no attribute 'iter_cols' 413 | * Fix #43 - Fix error with leading/trailing whitespace 414 | 415 | 416 | [1.0b5] - 2019-02-24 417 | ==================== 418 | 419 | Added 420 | ----- 421 | 422 | * Implement XOR(), NOT(), TRUE(), FALSE() 423 | * Improve error handling for AND(), OR() 424 | * Implement POWER() function 425 | 426 | 427 | [1.0b4] - 2019-02-17 428 | ==================== 429 | 430 | Changed 431 | ------- 432 | 433 | * Move to openpyxl 2.6+ 434 | 435 | Removed 436 | ------- 437 | 438 | * Remove support for Python 3.4 439 | 440 | 441 | [1.0b3] - 2019-02-02 442 | ==================== 443 | 444 | Changed 445 | ------- 446 | 447 | * Work around openpyxl returning datetimes 448 | * Pin to openpyxl 2.5.12 to avoid bug in 2.5.14 (fixed in PR #315) 449 | 450 | 451 | [1.0b2] - 2019-01-05 452 | ==================== 453 | 454 | Changed 455 | ------- 456 | 457 | * Much work to better match Excel error processing 458 | * Extend validate_calcs() to allow testing entire workbook 459 | * Improvements to match(), including wildcard support 460 | * Finished implementing match(), lookup(), vlookup() and hlookup() 461 | * Implement COLUMN() and ROW() 462 | * Implement % operator 463 | * Implement len() 464 | * Implement binary base number Excel functions (hex2dec, etc.) 465 | 466 | Fixed 467 | ----- 468 | 469 | * Fix PI() 470 | 471 | 472 | [1.0b0] - 2018-12-25 473 | ===================== 474 | 475 | Added 476 | ----- 477 | 478 | * Converted to Python 3.4+ 479 | * Removed Windows Excel COM driver (openpyxl is used for all xlsx reading) 480 | * Add support for defined names 481 | * Add support for structured references 482 | * Fix support for relative formulas 483 | * set_value() and evaluate() support ranges and lists 484 | * Add several more library functions 485 | * Add AddressRange and AddressCell classes to encapsulate address calcs 486 | * Add validate_calcs() to aid debugging excellib functions 487 | * Add `build` feature which can limit recompile to only when excel file changes 488 | 489 | Changed 490 | ------- 491 | 492 | * Improved handling for #DIV0! and #VALUE! 493 | * Tests run on Python 3.4, 3.5, 3.6, 3.7 (via tox) 494 | * Heavily refactored ExcelCompiler 495 | * Moved all formula evaluation, parsing, etc, code to ExcelFormula class 496 | * Convert to using openpyxl tokenizer 497 | * Converted prints to logging calls 498 | * Convert to using pytest 499 | * Add support for travis and codecov.io 500 | * 100% unit test coverage (mostly) 501 | * Add debuggable formula evaluation 502 | * Cleanup generated Python code to make easier to read 503 | * Add a text format (yaml or json) serialization format 504 | * flake8 (pep8) checks added 505 | * pip now handles which Python versions can be used 506 | * Release to PyPI 507 | * Docs updated 508 | 509 | Removed 510 | ------- 511 | 512 | * Python 2 no longer supported 513 | 514 | Fixed 515 | ----- 516 | 517 | * Numerous 518 | 519 | 520 | [0.0.1] - (UNRELEASED) 521 | ====================== 522 | 523 | * Original version available from `Dirk Ggorissen's Pycel Github Page`_. 524 | * Supports Python 2 525 | 526 | .. _Dirk Ggorissen's Pycel Github Page: https://github.com/dgorissen/pycel/tree/33c1370d499c629476c5506c7da308713b5842dc 527 | -------------------------------------------------------------------------------- /src/pycel/excelwrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | ExcelOpxWrapper : Can be run anywhere but only with post 2010 Excel formats 12 | ExcelOpxWrapperNoData : 13 | Can be initialized with a instance of an OpenPyXl workbook 14 | """ 15 | 16 | import abc 17 | import collections 18 | import os 19 | from unittest import mock 20 | 21 | from openpyxl import load_workbook, Workbook 22 | from openpyxl.cell.cell import Cell, MergedCell 23 | from openpyxl.formula.translate import Translator 24 | 25 | from pycel.excelutil import AddressCell, AddressRange, flatten, is_address 26 | 27 | ARRAY_FORMULA_NAME = '=CSE_INDEX' 28 | ARRAY_FORMULA_FORMAT = '{}(%s,%s,%s,%s,%s)'.format(ARRAY_FORMULA_NAME) 29 | 30 | 31 | class ExcelWrapper: 32 | __metaclass__ = abc.ABCMeta 33 | 34 | RangeData = collections.namedtuple('RangeData', 'address formula values') 35 | 36 | @abc.abstractmethod 37 | def get_range(self, address): 38 | """""" 39 | 40 | @abc.abstractmethod 41 | def get_used_range(self): 42 | """""" 43 | 44 | @abc.abstractmethod 45 | def get_active_sheet_name(self): 46 | """""" 47 | 48 | def get_formula_from_range(self, address): 49 | if not is_address(address): 50 | address = AddressRange(address) 51 | result = self.get_range(address) 52 | if isinstance(address, AddressCell): 53 | return result.formula if result.formula.startswith("=") else None 54 | else: 55 | return tuple(tuple( 56 | self.get_formula_from_range(a) for a in row 57 | ) for row in result.resolve_range) 58 | 59 | def get_formula_or_value(self, address): 60 | if not is_address(address): 61 | address = AddressRange(address) 62 | result = self.get_range(address) 63 | if isinstance(address, AddressCell): 64 | return result.formula or result.values 65 | else: 66 | return tuple(tuple( 67 | self.get_formula_or_value(a) for a in row 68 | ) for row in result.resolve_range) 69 | 70 | 71 | class _OpxRange(ExcelWrapper.RangeData): 72 | """ Excel range wrapper that distributes reduced api used by compiler 73 | (Formula & Value) 74 | """ 75 | def __new__(cls, cells, cells_dataonly, address): 76 | formula = None 77 | value = cells[0][0].value 78 | if isinstance(value, str) and value.startswith(ARRAY_FORMULA_NAME): 79 | # if this range refers to a CSE Array Formula, get the formula 80 | front, *args = cells[0][0].value[:-1].rsplit(',', 4) 81 | 82 | # if this range corresponds to the top left of a CSE Array formula 83 | if (args[0] == args[1] == '1') and all( 84 | c.value and c.value.startswith(front) 85 | for c in flatten(cells)): 86 | # apply formula to the range 87 | formula = '={%s}' % front[len(ARRAY_FORMULA_NAME) + 1:] 88 | else: 89 | formula = tuple(tuple(cls.cell_to_formula(cell) for cell in row) 90 | for row in cells) 91 | 92 | values = tuple(tuple(cell.value for cell in row) 93 | for row in cells_dataonly) 94 | return ExcelWrapper.RangeData.__new__(cls, address, formula, values) 95 | 96 | @classmethod 97 | def cell_to_formula(cls, cell): 98 | if cell.value is None: 99 | return '' 100 | else: 101 | formula = str(cell.value) 102 | if not formula.startswith('='): 103 | return '' 104 | 105 | elif formula.startswith('={') and formula[-1] == '}': 106 | # This is not in a CSE Array Context 107 | return f'=index({formula[1:]},1,1)' 108 | 109 | elif formula.startswith(ARRAY_FORMULA_NAME): 110 | # These are CSE Array formulas as encoded from sheet 111 | params = formula[len(ARRAY_FORMULA_NAME) + 1:-1].rsplit(',', 4) 112 | start_row = cell.row - int(params[1]) + 1 113 | start_col_idx = cell.col_idx - int(params[2]) + 1 114 | end_row = start_row + int(params[3]) - 1 115 | end_col_idx = start_col_idx + int(params[4]) - 1 116 | cse_range = AddressRange( 117 | (start_col_idx, start_row, end_col_idx, end_row), 118 | sheet=cell.parent.title) 119 | return f'=index({cse_range.quoted_address},{params[1]},{params[2]})' 120 | else: 121 | return formula 122 | 123 | @property 124 | def resolve_range(self): 125 | return AddressRange( 126 | (self.address.start.col_idx, 127 | self.address.start.row, 128 | self.address.start.col_idx + len(self.values[0]) - 1, 129 | self.address.start.row + len(self.values) - 1), 130 | sheet=self.address.sheet 131 | ).resolve_range 132 | 133 | 134 | class _OpxCell(_OpxRange): 135 | """ Excel cell wrapper that distributes reduced api used by compiler 136 | (Formula & Value) 137 | """ 138 | def __new__(cls, cell, cell_dataonly, address): 139 | assert isinstance(address, AddressCell) 140 | return ExcelWrapper.RangeData.__new__( 141 | cls, address, cls.cell_to_formula(cell), cell_dataonly.value) 142 | 143 | 144 | class ExcelOpxWrapper(ExcelWrapper): 145 | """ OpenPyXl implementation for ExcelWrapper interface """ 146 | 147 | CfRule = collections.namedtuple( 148 | 'CfRule', 'formula priority dxf_id dxf stop_if_true') 149 | 150 | def __init__(self, filename, app=None): 151 | super(ExcelWrapper, self).__init__() 152 | 153 | self.filename = os.path.abspath(filename) 154 | self._defined_names = None 155 | self._tables = None 156 | self._table_refs = {} 157 | self.workbook = None 158 | self.workbook_dataonly = None 159 | self._max_col_row = {} 160 | 161 | def max_col_row(self, sheet): 162 | if sheet not in self._max_col_row: 163 | worksheet = self.workbook[sheet] 164 | self._max_col_row[sheet] = worksheet.max_column, worksheet.max_row 165 | return self._max_col_row[sheet] 166 | 167 | @property 168 | def defined_names(self): 169 | if self.workbook is not None and self._defined_names is None: 170 | self._defined_names = {} 171 | 172 | for d_name in self.workbook.defined_names.definedName: 173 | destinations = [ 174 | (alias, wksht) for wksht, alias in d_name.destinations 175 | if wksht in self.workbook] 176 | if len(destinations): 177 | self._defined_names[str(d_name.name)] = destinations 178 | return self._defined_names 179 | 180 | def table(self, table_name): 181 | """ Return the table and the sheet it was found on 182 | 183 | :param table_name: name of table to retrieve 184 | :return: table, sheet_name 185 | """ 186 | # table names are case insensitive 187 | if self._tables is None: 188 | TableAndSheet = collections.namedtuple( 189 | 'TableAndSheet', 'table, sheet_name') 190 | self._tables = { 191 | t.name.lower(): TableAndSheet(t, ws.title) 192 | for ws in self.workbook for t in self._worksheet_tables(ws)} 193 | self._tables[None] = TableAndSheet(None, None) 194 | return self._tables.get(table_name.lower(), self._tables[None]) 195 | 196 | def table_name_containing(self, address): 197 | """ Return the table name containing the address given """ 198 | address = AddressCell(address) 199 | if address not in self._table_refs: 200 | for t in self._worksheet_tables(self.workbook[address.sheet]): 201 | if address in AddressRange(t.ref): 202 | self._table_refs[address] = t.name.lower() 203 | break 204 | 205 | return self._table_refs.get(address) 206 | 207 | def _worksheet_tables(self, ws): # pragma: no cover 208 | """::HACK:: workaround for unsupported tables access in openpyxl < 3.0.4""" 209 | try: 210 | return ws.tables.values() 211 | except AttributeError: 212 | # hack for openpyxl versions < 3.0.4 213 | return ws._tables 214 | 215 | def conditional_format(self, address): 216 | """ Return the conditional formats applicable for this cell """ 217 | address = AddressCell(address) 218 | all_formats = self.workbook[address.sheet].conditional_formatting 219 | formats = (cf for cf in all_formats if address.coordinate in cf) 220 | rules = [] 221 | for cf in formats: 222 | origin = AddressRange(cf.cells.ranges[0].coord).start 223 | row_offset = address.row - origin.row 224 | col_offset = address.col_idx - origin.col_idx 225 | for rule in cf.rules: 226 | if rule.formula: 227 | trans = Translator(f'={rule.formula[0]}', origin.coordinate) 228 | formula = trans.translate_formula( 229 | row_delta=row_offset, col_delta=col_offset) 230 | rules.append(self.CfRule( 231 | formula=formula, 232 | priority=rule.priority, 233 | dxf_id=rule.dxfId, 234 | dxf=rule.dxf, 235 | stop_if_true=rule.stopIfTrue, 236 | )) 237 | return sorted(rules, key=lambda x: x.priority) 238 | 239 | def load(self): 240 | # work around type coercion to datetime that causes some issues 241 | with mock.patch('openpyxl.worksheet._reader.from_excel', 242 | self.from_excel): 243 | self.workbook = load_workbook(self.filename) 244 | self.workbook_dataonly = load_workbook( 245 | self.filename, data_only=True) 246 | self.load_array_formulas() 247 | 248 | def load_array_formulas(self): 249 | # expand array formulas 250 | for ws in self.workbook: 251 | if not hasattr(ws, 'array_formulae'): # pragma: no cover 252 | # array_formulae was introduced in openpyxl 3.0.8 & removed in 3.0.9 253 | # https://foss.heptapod.net/openpyxl/openpyxl/-/ 254 | # commit/b71b6ba667e9fcf8de3f899382d446626e55970c 255 | # ... evidently will be coming back in 3.1 256 | # https://openpyxl.readthedocs.io/en/stable/changes.html 257 | self.old_load_array_formulas() 258 | return 259 | 260 | for address, ref_addr in ws.array_formulae.items(): # pragma: no cover 261 | # get the reference address for the array formula 262 | ref_addr = AddressRange(ref_addr) 263 | if not isinstance(ref_addr, AddressRange): 264 | ref_addr = AddressRange(ref_addr) 265 | 266 | if isinstance(ref_addr, AddressRange): 267 | formula = ws[address].value 268 | for i, row in enumerate(ref_addr.rows, start=1): 269 | for j, addr in enumerate(row, start=1): 270 | ws[addr.coordinate] = ARRAY_FORMULA_FORMAT % ( 271 | formula.text[1:], i, j, *ref_addr.size) 272 | else: 273 | # ::TODO:: At some point consider dropping support for openpyxl < 3.0.8 274 | # This has the effect of replacing the ArrayFormula object with just the 275 | # formula text. This matches the openpyxl < 3.0.8 behavior, at some point 276 | # consider using the new behavior. 277 | ws[ref_addr.coordinate] = ws[ref_addr.coordinate].value.text 278 | 279 | def old_load_array_formulas(self): # pragma: no cover 280 | """expand array formulas""" 281 | # formula_attributes was dropped in openpyxl 3.0.8 282 | # https://foss.heptapod.net/openpyxl/openpyxl/-/ 283 | # commit/b71b6ba667e9fcf8de3f899382d446626e55970c 284 | for ws in self.workbook: 285 | for address, props in ws.formula_attributes.items(): 286 | if props.get('t') != 'array': 287 | continue # pragma: no cover 288 | 289 | # get the reference address for the array formula 290 | ref_addr = AddressRange(props.get('ref')) 291 | 292 | if isinstance(ref_addr, AddressRange): 293 | formula = ws[address].value 294 | for i, row in enumerate(ref_addr.rows, start=1): 295 | for j, addr in enumerate(row, start=1): 296 | ws[addr.coordinate] = ARRAY_FORMULA_FORMAT % ( 297 | formula[1:], i, j, *ref_addr.size) 298 | 299 | def set_sheet(self, s): 300 | self.workbook.active = self.workbook.index(self.workbook[s]) 301 | self.workbook_dataonly.active = self.workbook_dataonly.index( 302 | self.workbook_dataonly[s]) 303 | return self.workbook.active 304 | 305 | @staticmethod 306 | def from_excel(value, *args, **kwargs): 307 | # ::HACK:: excel thinks that 1900/02/29 was a thing. In certain 308 | # circumstances openpyxl will return a datetime. This is a problem 309 | # as we don't want them, and having been mapped to datetime 310 | # information may have been lost, so ignore the conversions 311 | return value 312 | 313 | def get_range(self, address): 314 | if not is_address(address): 315 | address = AddressRange(address) 316 | 317 | if address.has_sheet: 318 | sheet = self.workbook[address.sheet] 319 | sheet_dataonly = self.workbook_dataonly[address.sheet] 320 | else: 321 | sheet = self.workbook.active 322 | sheet_dataonly = self.workbook_dataonly.active 323 | 324 | with mock.patch('openpyxl.worksheet._reader.from_excel', 325 | self.from_excel): 326 | # work around type coercion to datetime that causes some issues 327 | 328 | if address.is_unbounded_range: 329 | # bound the address range to the data in the spreadsheet 330 | address = address & AddressRange( 331 | (1, 1, *self.max_col_row(sheet.title)), 332 | sheet=sheet.title) 333 | 334 | cells = sheet[address.coordinate] 335 | cells_dataonly = sheet_dataonly[address.coordinate] 336 | if isinstance(cells, (Cell, MergedCell)): 337 | return _OpxCell(cells, cells_dataonly, address) 338 | else: 339 | return _OpxRange(cells, cells_dataonly, address) 340 | 341 | def get_used_range(self): 342 | return self.workbook.active.iter_rows() 343 | 344 | def get_active_sheet_name(self): 345 | return self.workbook.active.title 346 | 347 | 348 | class ExcelOpxWrapperNoData(ExcelOpxWrapper): 349 | """ ExcelWrapper interface from openpyxl workbook, 350 | without data_only workbook """ 351 | 352 | @staticmethod 353 | def excel_value(formula, value): 354 | """A openpyxl sheet does not have values for formula cells""" 355 | return None if formula else value 356 | 357 | class OpxRange(_OpxRange): 358 | def __new__(cls, range_data): 359 | values = tuple( 360 | tuple(ExcelOpxWrapperNoData.excel_value(*cell) 361 | for cell in zip(row_f, row_v)) 362 | for row_f, row_v in zip(range_data.formula, range_data.values) 363 | ) 364 | return ExcelWrapper.RangeData.__new__( 365 | cls, range_data.address, range_data.formula, values) 366 | 367 | class OpxCell(_OpxCell): 368 | def __new__(cls, cell_data): 369 | value = ExcelOpxWrapperNoData.excel_value( 370 | cell_data.formula, cell_data.values) 371 | return ExcelWrapper.RangeData.__new__( 372 | cls, cell_data.address, cell_data.formula, value) 373 | 374 | def __init__(self, workbook, filename='Unknown'): 375 | super().__init__(filename=filename) 376 | assert isinstance(workbook, Workbook) 377 | self.workbook = workbook 378 | self.workbook_dataonly = workbook 379 | self.load_array_formulas() 380 | 381 | def get_range(self, address): 382 | data = super().get_range(address) 383 | if isinstance(data.values, tuple): 384 | return self.OpxRange(data) 385 | else: 386 | return self.OpxCell(data) 387 | -------------------------------------------------------------------------------- /tests/lib/test_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | import pycel.excellib 13 | from pycel.excelcompiler import ExcelCompiler 14 | from pycel.excelutil import ( 15 | DIV0, 16 | NA_ERROR, 17 | NAME_ERROR, 18 | VALUE_ERROR, 19 | ) 20 | from pycel.lib.function_helpers import load_to_test_module 21 | from pycel.lib.text import ( 22 | concat, 23 | concatenate, 24 | exact, 25 | find, 26 | left, 27 | len_, 28 | lower, 29 | mid, 30 | replace, 31 | right, 32 | substitute, 33 | text as text_func, 34 | trim, 35 | upper, 36 | value, 37 | ) 38 | 39 | # dynamic load the lib functions from excellib and apply metadata 40 | load_to_test_module(pycel.lib.text, __name__) 41 | 42 | 43 | def test_text_ws(fixture_xls_copy): 44 | compiler = ExcelCompiler(fixture_xls_copy('text.xlsx')) 45 | result = compiler.validate_serialized() 46 | assert result == {} 47 | 48 | 49 | @pytest.mark.parametrize( 50 | 'args, expected', ( 51 | ('a 1 abc'.split(), 'a1abc'), 52 | ('a Jan-00 abc'.split(), 'aJan-00abc'), 53 | ('a #DIV/0! abc'.split(), DIV0), 54 | ('a 1 #DIV/0!'.split(), DIV0), 55 | ('a #NAME? abc'.split(), NAME_ERROR), 56 | (('a', True, 'abc'), 'aTRUEabc'), 57 | (('a', False, 'abc'), 'aFALSEabc'), 58 | (('a', 2, 'abc'), 'a2abc'), 59 | ) 60 | ) 61 | def test_concatenate(args, expected): 62 | assert concat(*args) == expected 63 | assert concatenate(*args) == expected 64 | assert concat(args) == expected 65 | assert concatenate(args) == VALUE_ERROR 66 | 67 | 68 | @pytest.mark.parametrize( 69 | 'text1, text2, expected', ( 70 | ('word', 'word', True), 71 | ('Word', 'word', False), 72 | ('w ord', 'word', False), 73 | ) 74 | ) 75 | def test_exact(text1, text2, expected): 76 | assert exact(text1, text2) == expected 77 | 78 | 79 | @pytest.mark.parametrize( 80 | 'to_find, find_in, expected', ( 81 | (2, 2.5, 1), 82 | ('.', 2.5, 2), 83 | (5, 2.5, 3), 84 | ('2', 2.5, 1), 85 | ('.', 2.5, 2), 86 | ('5', 2.5, 3), 87 | ('2', '2.5', 1), 88 | ('.', '2.5', 2), 89 | ('T', True, 1), 90 | ('U', True, 3), 91 | ('u', True, VALUE_ERROR), 92 | (DIV0, 'x' + DIV0, DIV0), 93 | ('V', DIV0, DIV0), 94 | ) 95 | ) 96 | def test_find(to_find, find_in, expected): 97 | assert find(to_find, find_in) == expected 98 | 99 | 100 | @pytest.mark.parametrize( 101 | 'text, num_chars, expected', ( 102 | ('abcd', 5, 'abcd'), 103 | ('abcd', 4, 'abcd'), 104 | ('abcd', 3, 'abc'), 105 | ('abcd', 2, 'ab'), 106 | ('abcd', 1, 'a'), 107 | ('abcd', 0, ''), 108 | 109 | (1.234, 3, '1.2'), 110 | 111 | (True, 3, 'TRU'), 112 | (False, 2, 'FA'), 113 | 114 | ('abcd', -1, VALUE_ERROR), 115 | ('abcd', 'x', VALUE_ERROR), 116 | (DIV0, 1, DIV0), 117 | ('abcd', NAME_ERROR, NAME_ERROR), 118 | ) 119 | ) 120 | def test_left(text, num_chars, expected): 121 | assert left(text, num_chars) == expected 122 | 123 | 124 | @pytest.mark.parametrize( 125 | 'text, expected', ( 126 | ('aBcD', 'abcd'), 127 | (1.234, '1.234'), 128 | (1, '1'), 129 | (True, 'true'), 130 | (False, 'false'), 131 | ('TRUe', 'true'), 132 | (DIV0, DIV0), 133 | ) 134 | ) 135 | def test_lower(text, expected): 136 | assert lower(text) == expected 137 | 138 | 139 | @pytest.mark.parametrize( 140 | 'text, start, count, expected', ( 141 | (VALUE_ERROR, 2, 2, VALUE_ERROR), 142 | ('Romain', VALUE_ERROR, 2, VALUE_ERROR), 143 | ('Romain', 2, VALUE_ERROR, VALUE_ERROR), 144 | (DIV0, 2, 2, DIV0), 145 | ('Romain', DIV0, 2, DIV0), 146 | ('Romain', 2, DIV0, DIV0), 147 | 148 | ('Romain', 'x', 2, VALUE_ERROR), 149 | ('Romain', 2, 'x', VALUE_ERROR), 150 | 151 | ('Romain', 1, 2.1, 'Ro'), 152 | 153 | ('Romain', 0, 3, VALUE_ERROR), 154 | ('Romain', 1, -1, VALUE_ERROR), 155 | 156 | (1234, 2, 2, '23'), 157 | (12.34, 2, 2, '2.'), 158 | 159 | (True, 2, 2, 'RU'), 160 | (False, 2, 2, 'AL'), 161 | (None, 2, 2, ''), 162 | 163 | ('Romain', 2, 9, 'omain'), 164 | ('Romain', 2.1, 2, 'om'), 165 | ('Romain', 2, 2.1, 'om'), 166 | ) 167 | ) 168 | def test_mid(text, start, count, expected): 169 | assert mid(text, start, count) == expected 170 | 171 | 172 | @pytest.mark.parametrize( 173 | 'expected, old_text, start_num, num_chars, new_text', ( 174 | ('AB CD_X_', 'AB CD', 7, 2, '_X_'), 175 | ('AB CD_X_', 'AB CD', 6, 2, '_X_'), 176 | ('AB C_X_', 'AB CD', 5, 2, '_X_'), 177 | ('AB _X_', 'AB CD', 4, 2, '_X_'), 178 | ('AB_X_D', 'AB CD', 3, 2, '_X_'), 179 | ('A_X_CD', 'AB CD', 2, 2, '_X_'), 180 | ('_X_ CD', 'AB CD', 1, 2, '_X_'), 181 | (VALUE_ERROR, 'AB CD', 0, 2, '_X_'), 182 | ('_X_', 'AB CD', 1, 6, '_X_'), 183 | ('_X_', 'AB CD', 1, 5, '_X_'), 184 | ('_X_D', 'AB CD', 1, 4, '_X_'), 185 | ('AB C_X_', 'AB CD', 5, 1, '_X_'), 186 | ('AB C_X_', 'AB CD', 5, 2, '_X_'), 187 | ('AB _X_D', 'AB CD', 4, 1, '_X_'), 188 | ('AB _X_', 'AB CD', 4, 2, '_X_'), 189 | ('AB_X_ CD', 'AB CD', 3, 0, '_X_'), 190 | (VALUE_ERROR, 'AB CD', 3, -1, '_X_'), 191 | ('_X_ CD', 'AB CD', True, 2, '_X_'), 192 | (VALUE_ERROR, 'AB CD', False, 2, '_X_'), 193 | ('AB_X_CD', 'AB CD', 3, True, '_X_'), 194 | ('AB_X_ CD', 'AB CD', 3, False, '_X_'), 195 | ('_X_ CD', 'AB CD', 1, 2, '_X_'), 196 | (VALUE_ERROR, 'AB CD', 0, 2, '_X_'), 197 | (DIV0, DIV0, 2, 2, '_X_'), 198 | (DIV0, 'AB CD', DIV0, 2, '_X_'), 199 | (DIV0, 'AB CD', 2, DIV0, '_X_'), 200 | (DIV0, 'AB CD', 2, 2, DIV0), 201 | ('A0CD', 'AB CD', 2, 2, '0'), 202 | ('AFALSECD', 'AB CD', 2, 2, 'FALSE'), 203 | ('T_X_E', 'TRUE', 2, 2, '_X_'), 204 | ('F_X_SE', 'FALSE', 2, 2, '_X_'), 205 | ('A_X_', 'A', 2, 2, '_X_'), 206 | ('1_X_1', '1.1', 2, 1, '_X_'), 207 | (VALUE_ERROR, '1.1', 'A', 1, '_X_'), 208 | (VALUE_ERROR, '1.1', 2, 'A', '_X_'), 209 | ('1_X_1', '1.1', 2.2, 1, '_X_'), 210 | ('1_X_1', '1.1', 2.9, 1, '_X_'), 211 | ('1._X_', '1.1', 3, 1, '_X_'), 212 | ('1_X_1', '1.1', 2, 1.5, '_X_'), 213 | ('1.0', '1.1', 3, 1, 0), 214 | ) 215 | ) 216 | def test_replace(expected, old_text, start_num, num_chars, new_text): 217 | assert replace(old_text, start_num, num_chars, new_text) == expected 218 | 219 | 220 | @pytest.mark.parametrize( 221 | 'text, num_chars, expected', ( 222 | ('abcd', 5, 'abcd'), 223 | ('abcd', 4, 'abcd'), 224 | ('abcd', 3, 'bcd'), 225 | ('abcd', 2, 'cd'), 226 | ('abcd', 1, 'd'), 227 | ('abcd', 0, ''), 228 | 229 | (1234.1, 2, '.1'), 230 | 231 | (True, 3, 'RUE'), 232 | (False, 2, 'SE'), 233 | 234 | ('abcd', -1, VALUE_ERROR), 235 | ('abcd', 'x', VALUE_ERROR), 236 | (VALUE_ERROR, 1, VALUE_ERROR), 237 | ('abcd', VALUE_ERROR, VALUE_ERROR), 238 | ) 239 | ) 240 | def test_right(text, num_chars, expected): 241 | assert right(text, num_chars) == expected 242 | 243 | 244 | @pytest.mark.parametrize( 245 | 'text, old_text, new_text, instance_num, expected', ( 246 | ('abcdef', 'cd', '', None, 'abef'), 247 | ('abcdef', 'cd', 'X', None, 'abXef'), 248 | ('abcdef', 'cd', 'XY', None, 'abXYef'), 249 | ('abcdef', 'cd', '', True, VALUE_ERROR), 250 | ('abcdef', 'cd', '', 'PLUGH', VALUE_ERROR), 251 | 252 | ('abcdabcdab', 'a', 'X', 1, 'Xbcdabcdab'), 253 | ('abcdabcdab', 'a', 'X', 2, 'abcdXbcdab'), 254 | ('abcdabcdab', 'a', 'X', 3, 'abcdabcdXb'), 255 | ('abcdabcdab', 'ab', 'X', None, 'XcdXcdX'), 256 | ('abcdabcdab', 'ab', 'X', 0, VALUE_ERROR), 257 | ('abcdabcdab', 'ab', 'X', 1, 'Xcdabcdab'), 258 | ('abcdabcdab', 'ab', 'X', 2, 'abcdXcdab'), 259 | ('abcdabcdab', 'ab', 'X', 3, 'abcdabcdX'), 260 | ('abcdabcdab', 'ab', 'X', 4, 'abcdabcdab'), 261 | ('abcdabcdab', 'abc', 'X', 1, 'Xdabcdab'), 262 | ('abcdabcdab', 'abc', 'X', 2, 'abcdXdab'), 263 | 264 | ('abcdabcdab', 'cd', 'X', None, 'abXabXab'), 265 | ('abcdabcdab', 'cd', 'X', -1, VALUE_ERROR), 266 | ('abcdabcdab', 'cd', 'X', 0, VALUE_ERROR), 267 | ('abcdabcdab', 'cd', 'X', 1, 'abXabcdab'), 268 | ('abcdabcdab', 'cd', 'X', 2, 'abcdabXab'), 269 | ('abcdabcdab', 'cd', 'X', 3, 'abcdabcdab'), 270 | 271 | (VALUE_ERROR, 'ab', 'X', None, VALUE_ERROR), 272 | ('abcdabcdab', DIV0, 'X', None, DIV0), 273 | ('abcdabcdab', 'ab', NAME_ERROR, None, NAME_ERROR), 274 | ('abcdabcdab', 'ab', 'X', NA_ERROR, NA_ERROR), 275 | 276 | (True, 'R', '', None, 'TUE'), 277 | (False, 'AL', '^', None, 'F^SE'), 278 | (False, 'AL', 1.2, None, 'F1.2SE'), 279 | (321.245, 21, 1.2, None, '31.2.245'), 280 | ) 281 | ) 282 | def test_substitute(text, old_text, new_text, instance_num, expected): 283 | assert substitute(text, old_text, new_text, instance_num) == expected 284 | 285 | 286 | @pytest.mark.parametrize( 287 | 'text_value, value_format, expected', ( 288 | 289 | # Thousand separator 290 | ('12200000', '#,###', '12,200,000'), 291 | ('12200000', '0,000.00', '12,200,000.00'), 292 | 293 | # Number, currency, accounting 294 | ('1234.56', '0.00', '1234.56'), 295 | ('1234.56', '#,##0', '1,235'), 296 | ('1234.56', '#,##0.00', '1,234.56'), 297 | ('1234.56', '$#,##0', '$1,235'), 298 | ('1234.56', '$#,##0.00', '$1,234.56'), 299 | ('1234.56', '$ * #,##0', '$ 1,235'), 300 | ('1234.56', '$ * #,##0.00', '$ 1,234.56'), 301 | 302 | # Months, days, years 303 | ('2021-01-05', 'm', '1'), 304 | ('2021-01-05', 'mm', '01'), 305 | ('2021-01-05', 'mmm', 'Jan'), 306 | ('2021-01-05', 'mmmm', 'January'), 307 | ('2021-01-05', 'mmmmm', 'J'), 308 | ('2021-01-05', 'd', '5'), 309 | ('2021-01-05', 'dd', '05'), 310 | ('2021-01-05', 'ddd', 'Tue'), 311 | ('2021-01-05', 'dddd', 'Tuesday'), 312 | ('2021-01-05', 'ddddd', 'Tuesday'), 313 | ('2021-01-05', 'y', '21'), 314 | ('2021-01-05', 'yy', '21'), 315 | ('2021-01-05', 'yyy', '2021'), 316 | ('2021-01-05', 'yyyy', '2021'), 317 | ('2021-01-05', 'yyyyy', '2021'), 318 | 319 | # Hours, minutes and seconds 320 | ('3:33 am', 'h', '3'), 321 | ('3:33 am', 'hh', '03'), 322 | ('3:33 pm', 'h', '15'), 323 | ('3:33 pm', 'hh', '15'), 324 | ('3:33 pm', 'm', '1'), 325 | ('3:33 pm', 'mm', '01'), 326 | ('3:33:03 pm', 's', '3'), 327 | ('3:33:30 am', 's', '30'), 328 | ('3:33:30 pm', 'ss', '30'), 329 | ('3:33 pm', 'h AM/PM', '3 pm'), 330 | ('3:33 am', 'h AM/PM', '3 am'), 331 | ('3:33 pm', 'h:mm AM/PM', '3:33 PM'), 332 | ('3:33:30 pm', 'h:mm:ss A/P', '3:33:30 P'), 333 | ('3:33 pm', 'h:mm:ss.00', '15:33:00.00'), 334 | ('3:22:33.67 pm', 'mm:ss.00', '22:33.67'), 335 | ('99:99', '', '99:99'), 336 | 337 | # Elapsed Time 338 | ('3:33 pm', '[h]:mm', '15:33'), 339 | ('3:33:14 pm', '[mm]:ss', '933:14'), 340 | ('3:33:14.78 pm', '[ss].00', '55994.78'), 341 | (39815.17021, '[hh]:mm', '955564:05'), 342 | ('-1', '[hh]', '-24'), 343 | ('-1', '[hh]:mm', VALUE_ERROR), 344 | ('-1', '[mm]', '-1440'), 345 | ('-1', '[mm]:ss', VALUE_ERROR), 346 | ('-1', '[ss]', '-86400'), 347 | ('-1', '[ss].000', VALUE_ERROR), 348 | ('2958466', '[hh]', '71003184'), # > 9999-12-31 ok w/ elapsed 349 | ('2958466', 'hh', VALUE_ERROR), 350 | ('23:59.012345', '#.#######', '.0166552'), 351 | ('59:9999.0123', '#.########', '.15670153'), 352 | ('60:9999.012345', '#.###########', '60:9999.012345'), 353 | 354 | # Date & Time 355 | ('1989-12-31 15:30:00', 'MM/DD/YYYY', '12/31/1989'), 356 | ('1989-12-31', 'YYYY-MM-DD', '1989-12-31'), 357 | ('1989-12-31', 'YYYY/MM/DD', '1989/12/31'), 358 | (39815.17, 'dddd, mmmm dd, yyyy hh:mm:ss', 'Friday, January 02, 2009 04:04:48'), 359 | (39815.17, 'dddd, mmmm dd, yyyy', 'Friday, January 02, 2009'), 360 | ('1989-12-31 15:30:00', 'MM/DD/YYYY hh:mm AM/PM', '12/31/1989 03:30 pm'), 361 | 362 | # Percentage 363 | ('0.244740088392962', '0%', '24%'), 364 | ('0.244740088392962', '0.0%', '24.5%'), 365 | ('0.244740088392962', '0.00%', '24.47%'), 366 | ('0.244740088392962', '0#,#%%%%%%%', '24,474,008,839,296%%%%%%%'), 367 | 368 | # text without formatting - returned as-is 369 | ('test', '', 'test'), 370 | (55, '', ''), 371 | (0, '', ''), 372 | ('-55', '', '-'), 373 | 374 | # non-numerics 375 | ('FALSE', '#.#', 'FALSE'), 376 | ('TRUE', '#.#', 'TRUE'), 377 | ('PLUGH', '#.#', 'PLUGH'), 378 | (None, 'hh', '00'), 379 | (None, '#', ''), 380 | (None, '#.##', '.'), 381 | 382 | # m is month 383 | ('2000-12-30 15:35', 'am/p', 'a12/p'), 384 | # dates are converted to serial numbers 385 | ('2021-01-01 10:3', '#.00000', '44197.41875'), 386 | 387 | # multiple fields 388 | ('-1', '##.00;##.0;"ZERO";->@<-', '1.0'), 389 | ('0', '##.00;##.0;"ZERO";->@<-', 'ZERO'), 390 | ('1', '##.00;##.0;"ZERO";->@<-', '1.00'), 391 | ('X', '##.00;##.0;"ZERO";->@<-', '->X<-'), 392 | 393 | # mixed fields 394 | (-1, '#,#ZZ.##', '-1ZZ.'), 395 | (None, '#,#"ZZ"#.0z00', 'ZZ.0z00'), 396 | ('0', '#,#"ZZ"#.0z00', 'ZZ.0z00'), 397 | ('', '#,#"ZZ"#.0z00', ''), 398 | (-1, 'w;x@', '-w'), 399 | (890123.456789, ',#.00.0', ',890123.45.7'), 400 | (890123.456789, '.#', '890123.5'), 401 | (890123.456789, '%#', '%89012346'), 402 | (890123.456789, '%#%#%', '%89012345678%9%'), 403 | (890123.456789, '%#%#%#%', '%890123456789%0%0%'), 404 | ('1234.56', '.#', '1234.6'), 405 | ('1234.56', '.##', '1234.56'), 406 | ('1234.56', '.##0', '1234.560'), 407 | 408 | # format parse errors 409 | (0, '\\', VALUE_ERROR), 410 | (0, 'a\\a', 'aa'), 411 | (0, '[', VALUE_ERROR), 412 | (0, '[h', VALUE_ERROR), 413 | (0, '[hm]', VALUE_ERROR), 414 | (0, '#@', VALUE_ERROR), 415 | (0, '#m', VALUE_ERROR), 416 | (0, 'm@', VALUE_ERROR), 417 | (0, '@;@', VALUE_ERROR), 418 | (0, '@;#', VALUE_ERROR), 419 | ('', '0.00*', VALUE_ERROR), 420 | ) 421 | ) 422 | def test_text(text_value, value_format, expected): 423 | assert text_func(text_value, value_format).lower() == expected.lower() 424 | 425 | 426 | @pytest.mark.parametrize( 427 | 'text, expected', ( 428 | ('ABCD', 'ABCD'), 429 | ('AB CD', 'AB CD'), 430 | ('AB CD', 'AB CD'), 431 | ('AB CD EF', 'AB CD EF'), 432 | (1.234, '1.234'), 433 | (1, '1'), 434 | (True, 'TRUE'), 435 | (False, 'FALSE'), 436 | ('tRUe', 'tRUe'), 437 | (DIV0, DIV0), 438 | ) 439 | ) 440 | def test_trim(text, expected): 441 | assert trim(text) == expected 442 | 443 | 444 | @pytest.mark.parametrize( 445 | 'text, expected', ( 446 | ('aBcD', 'ABCD'), 447 | (1.234, '1.234'), 448 | (1, '1'), 449 | (True, 'TRUE'), 450 | (False, 'FALSE'), 451 | ('tRUe', 'TRUE'), 452 | (DIV0, DIV0), 453 | ) 454 | ) 455 | def test_upper(text, expected): 456 | assert upper(text) == expected 457 | 458 | 459 | @pytest.mark.parametrize( 460 | 'param, expected', ( 461 | (0, 0), 462 | (2, 2), 463 | (2.1, 2.1), 464 | (-2.1, -2.1), 465 | ('-2.1', -2.1), 466 | ('3', 3), 467 | ('3.', 3), 468 | ('3.0', 3), 469 | ('.01', 0.01), 470 | ('1E5', 100000), 471 | (None, 0), 472 | ('X', VALUE_ERROR), 473 | ('`1', VALUE_ERROR), 474 | (False, VALUE_ERROR), 475 | (True, VALUE_ERROR), 476 | (NA_ERROR, NA_ERROR), 477 | (DIV0, DIV0), 478 | ) 479 | ) 480 | def test_value(param, expected): 481 | assert value(param) == expected 482 | 483 | 484 | @pytest.mark.parametrize( 485 | 'param, expected', ( 486 | ('A', 1), 487 | ('BB', 2), 488 | (3.0, 3), 489 | (True, 4), 490 | (False, 5), 491 | (None, 0), 492 | (NA_ERROR, NA_ERROR), 493 | (DIV0, DIV0), 494 | ) 495 | ) 496 | def test_len_(param, expected): 497 | assert len_(param) == expected 498 | -------------------------------------------------------------------------------- /tests/lib/test_stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | import pytest 11 | 12 | import pycel.excellib 13 | from pycel.excelcompiler import ExcelCompiler 14 | from pycel.excellib import ( 15 | sumif, 16 | sumifs, 17 | ) 18 | from pycel.excelutil import ( 19 | DIV0, 20 | EMPTY, 21 | ERROR_CODES, 22 | flatten, 23 | NA_ERROR, 24 | NAME_ERROR, 25 | NUM_ERROR, 26 | REF_ERROR, 27 | VALUE_ERROR, 28 | ) 29 | from pycel.lib.function_helpers import load_to_test_module 30 | from pycel.lib.stats import ( 31 | average, 32 | averageif, 33 | averageifs, 34 | count, 35 | countif, 36 | countifs, 37 | forecast, 38 | intercept, 39 | large, 40 | linest, 41 | max_, 42 | maxifs, 43 | min_, 44 | minifs, 45 | slope, 46 | small, 47 | trend, 48 | ) 49 | 50 | # dynamic load the lib functions from excellib and apply metadata 51 | load_to_test_module(pycel.excellib, __name__) 52 | load_to_test_module(pycel.lib.stats, __name__) 53 | 54 | 55 | def test_stats_ws(fixture_xls_copy): 56 | compiler = ExcelCompiler(fixture_xls_copy('stats.xlsx')) 57 | result = compiler.validate_serialized(tolerance=1e-6) 58 | assert result == {} 59 | 60 | 61 | def assert_np_close(result, expected): 62 | for res, exp in zip(flatten(result), flatten(expected)): 63 | if res not in ERROR_CODES and exp not in ERROR_CODES: 64 | exp = pytest.approx(exp) 65 | if res != exp: 66 | assert result == expected 67 | 68 | 69 | @pytest.mark.parametrize( 70 | 'data, expected', ( 71 | ((1, '3', 2.0, pytest, 3, 'x'), 2), 72 | (((1, '3', (2.0, pytest, 3), 'x'),), 2), 73 | (((-0.1, None, 'x', True),), -0.1), 74 | ((['x'],), DIV0), 75 | ((VALUE_ERROR,), VALUE_ERROR), 76 | (((2, VALUE_ERROR),), VALUE_ERROR), 77 | ((DIV0,), DIV0), 78 | (((2, DIV0),), DIV0), 79 | ) 80 | ) 81 | def test_average(data, expected): 82 | assert average(*data) == expected 83 | 84 | 85 | @pytest.mark.parametrize( 86 | 'data, expected', ( 87 | ((12, 12), AssertionError), 88 | ((12, 12, 12), 12), 89 | ((((1, 1, 2, 2, 2), ), ((1, 1, 2, 2, 2), ), 2), 2), 90 | ((((1, 1, 2, 2, 2), ), ((1, 1, 2, 2, 2), ), 3), DIV0), 91 | ((((1, 2, 3, 4, 5), ), ((1, 2, 3, 4, 5), ), ">=3"), 4), 92 | ((((100, 123, 12, 23, 634), ), 93 | ((1, 2, 3, 4, 5), ), ">=3"), 223), 94 | ((((100, 123), (12, 23)), ((1, 2), (3, 4)), ">=3"), 35 / 2), 95 | ((((100, 123, 12, 23, None), ), 96 | ((1, 2, 3, 4, 5), ), ">=3"), 35 / 2), 97 | (('JUNK', ((), ), ((), ), ), VALUE_ERROR), 98 | ((((1, 2), ), ((1,), ), '', ((1, 2), ), ''), VALUE_ERROR), 99 | ((((1, 2, 3, 4, 5), ), 100 | ((1, 2, 3, 4, 5), ), ">=3", 101 | ((1, 2, 3, 4, 5), ), "<=4"), 7 / 2), 102 | ) 103 | ) 104 | def test_averageifs(data, expected): 105 | if isinstance(expected, type(Exception)): 106 | with pytest.raises(expected): 107 | averageifs(*data) 108 | else: 109 | assert averageifs(*data) == expected 110 | 111 | 112 | def test_count(): 113 | data = ( 114 | 0, 115 | 1, 116 | 1.1, 117 | '1.1', 118 | True, 119 | False, 120 | 'A', 121 | 'TRUE', 122 | 'FALSE', 123 | ) 124 | assert count(data, data[3], data[5], data[7]) 125 | 126 | 127 | @pytest.mark.parametrize( 128 | 'value, criteria, expected', ( 129 | (((7, 25, 13, 25), ), '>10', 3), 130 | (((7, 25, 13, 25), ), '<10', 1), 131 | (((7, 10, 13, 25), ), '>=10', 3), 132 | (((7, 10, 13, 25), ), '<=10', 2), 133 | (((7, 10, 13, 25), ), '<>10', 3), 134 | (((7, 'e', 13, 'e'), ), 'e', 2), 135 | (((7, 'e', 13, 'f'), ), '>e', 1), 136 | (((7, 25, 13, 25), ), 25, 2), 137 | (((7, 25, None, 25),), '<10', 1), 138 | (((7, 25, None, 25),), '>10', 2), 139 | ) 140 | ) 141 | def test_countif(value, criteria, expected): 142 | assert countif(value, criteria) == expected 143 | 144 | 145 | class TestCountIfs: 146 | # more tests might be welcomed 147 | 148 | def test_countifs_regular(self): 149 | assert 1 == countifs(((7, 25, 13, 25), ), 25, 150 | ((100, 102, 201, 20), ), ">100") 151 | 152 | def test_countifs_odd_args_len(self): 153 | with pytest.raises(AssertionError): 154 | countifs(((7, 25, 13, 25), ), 25, ((100, 102, 201, 20), )) 155 | 156 | 157 | @pytest.mark.parametrize( 158 | 'Y, X, expected_slope, expected_intercept, expected_fit, input_x', ( 159 | ([[1, 2, 3, 4]], [[2, 3, 4, 5]], 1, -1, 1.5, 2.5), 160 | ([[1, 2, 3, 4]], [[-2, -3, -4, -5]], -1, -1, 1.5, -2.5), 161 | ([[-1, -2, -3, -4]], [[2, 3, 4, 5]], -1, 1, -1.5, 2.5), 162 | ([[1, 2, 3, 'a']], [[2, 3, 4, 5]], VALUE_ERROR, VALUE_ERROR, VALUE_ERROR, 2.5), 163 | ([[1, 2, 3, 4]], [[2, 3, 4, 'a']], VALUE_ERROR, VALUE_ERROR, VALUE_ERROR, 2.5), 164 | ([[1, 2], [3, 4]], [[2, 3, 4, 5]], NA_ERROR, NA_ERROR, NA_ERROR, 2.5), 165 | (NUM_ERROR, [[2, 3, 4, 5]], NUM_ERROR, NUM_ERROR, NUM_ERROR, 2.5), 166 | ([[1, 2, 3, 4]], NAME_ERROR, NAME_ERROR, NAME_ERROR, NAME_ERROR, 2.5), 167 | ([[1, 2, 3, 4]], [[2, 3, 4, 5]], None, None, REF_ERROR, REF_ERROR), 168 | ([[1, 2, 3, 4]], [[2, 2, 2, 2]], DIV0, DIV0, DIV0, 2.5), 169 | ([[1, 2, 3, 4]], [[2, 3, 4, 5], [1, 2, 4, 8]], NA_ERROR, NA_ERROR, 1.5, ((2.5, 2),)), 170 | ([[1, 2, 3, 4]], [[2, 3, 4, 5], [1, 2, 4, 8]], NA_ERROR, NA_ERROR, REF_ERROR, ((2.5,),)), 171 | ) 172 | ) 173 | def test_forecast_intercept_slope_trend( 174 | Y, X, expected_slope, expected_intercept, expected_fit, input_x): 175 | def approx_with_error(result): 176 | if result in ERROR_CODES: 177 | return result 178 | else: 179 | return pytest.approx(result) 180 | 181 | if expected_fit is not None: 182 | expected = NA_ERROR if isinstance(input_x, tuple) else expected_fit 183 | assert forecast(input_x, Y, X) == approx_with_error(expected) 184 | if expected_intercept is not None: 185 | assert intercept(Y, X) == approx_with_error(expected_intercept) 186 | if expected_slope is not None: 187 | assert slope(Y, X) == approx_with_error(expected_slope) 188 | 189 | if expected_fit not in ERROR_CODES and not isinstance(input_x, tuple): 190 | assert trend(Y, X, [[input_x, input_x]])[0][0] == approx_with_error(expected_fit) 191 | assert trend(Y, X, [[input_x, input_x]])[0][1] == approx_with_error(expected_fit) 192 | 193 | if expected_fit == NA_ERROR: 194 | expected_fit = REF_ERROR 195 | if expected_fit == DIV0: 196 | expected_fit = sum(Y[0]) / len(Y[0]) 197 | assert trend(Y, X, input_x) == approx_with_error(expected_fit) 198 | 199 | 200 | class TestVariousIfsSizing: 201 | 202 | test_vector = tuple(range(1, 7)) + tuple('abcdef') 203 | test_vectors = ((test_vector, ), ) * 4 + (test_vector[0],) * 4 204 | 205 | conditions = '>3', '<=2', '<=c', '>d' 206 | data_columns = ('averageif', 'countif', 'sumif', 'averageifs', 207 | 'countifs', 'maxifs', 'minifs', 'sumifs') 208 | 209 | responses_list = ( 210 | (5, 3, 15, 5, 3, 6, 4, 15), 211 | (1.5, 2, 3, 1.5, 2, 2, 1, 3), 212 | (DIV0, 3, 0, DIV0, 3, 0, 0, 0), 213 | (DIV0, 2, 0, DIV0, 2, 0, 0, 0), 214 | 215 | (DIV0, 0, 0, DIV0, 0, 0, 0, 0), 216 | (1, 1, 1, 1, 1, 1, 1, 1), 217 | (DIV0, 0, 0, DIV0, 0, 0, 0, 0), 218 | (DIV0, 0, 0, DIV0, 0, 0, 0, 0), 219 | ) 220 | 221 | responses = dict( 222 | (dc, tuple((r, cond, tv) for r, cond, tv in zip(resp, conds, tvs))) 223 | for dc, resp, tvs, conds in zip( 224 | data_columns, zip(*responses_list), (test_vectors, ) * 8, 225 | ((conditions + conditions), ) * 8 226 | )) 227 | 228 | params = 'expected, criteria, values' 229 | 230 | @staticmethod 231 | @pytest.mark.parametrize(params, responses['averageif']) 232 | def test_averageif(expected, criteria, values): 233 | assert averageif(values, criteria) == expected 234 | assert averageif(values, criteria, values) == expected 235 | 236 | @staticmethod 237 | @pytest.mark.parametrize(params, responses['countif']) 238 | def test_countif(expected, criteria, values): 239 | assert countif(values, criteria) == expected 240 | 241 | @staticmethod 242 | @pytest.mark.parametrize(params, responses['sumif']) 243 | def test_sumif(expected, criteria, values): 244 | assert sumif(values, criteria) == expected 245 | assert sumif(values, criteria, values) == expected 246 | 247 | @staticmethod 248 | @pytest.mark.parametrize(params, responses['averageifs']) 249 | def test_averageifs(expected, criteria, values): 250 | assert averageifs(values, values, criteria) == expected 251 | 252 | @staticmethod 253 | @pytest.mark.parametrize(params, responses['countifs']) 254 | def test_countifs(expected, criteria, values): 255 | assert countifs(values, criteria) == expected 256 | 257 | @staticmethod 258 | @pytest.mark.parametrize(params, responses['maxifs']) 259 | def test_maxifs(expected, criteria, values): 260 | assert maxifs(values, values, criteria) == expected 261 | 262 | @staticmethod 263 | @pytest.mark.parametrize(params, responses['minifs']) 264 | def test_minifs(expected, criteria, values): 265 | assert minifs(values, values, criteria) == expected 266 | 267 | @staticmethod 268 | @pytest.mark.parametrize(params, responses['sumifs']) 269 | def test_sumifs(expected, criteria, values): 270 | assert sumifs(values, values, criteria) == expected 271 | 272 | def test_ifs_size_errors(self): 273 | criteria, v1 = self.responses['sumifs'][0][1:] 274 | v2 = (v1[0][:-1], ) 275 | assert countifs(v1, criteria, v2, criteria) == VALUE_ERROR 276 | assert sumifs(v1, v1, criteria, v2, criteria) == VALUE_ERROR 277 | assert maxifs(v1, v1, criteria, v2, criteria) == VALUE_ERROR 278 | assert minifs(v1, v1, criteria, v2, criteria) == VALUE_ERROR 279 | assert averageifs(v1, v1, criteria, v2, criteria) == VALUE_ERROR 280 | 281 | 282 | @pytest.mark.parametrize( 283 | 'data, k, expected', ( 284 | ([3, 1, 2], 0, NUM_ERROR), 285 | ([3, 1, 2], 1, 3), 286 | ([3, 1, 2], 2, 2), 287 | ([3, 1, 2], 3, 1), 288 | ([3, 1, 2], 4, NUM_ERROR), 289 | ([3, 1, 2], '2', 2), 290 | ([3, 1, 2], 1.1, 2), 291 | ([3, 1, 2], '1.1', 2), 292 | ([3, 1, 2], 0.1, NUM_ERROR), 293 | ([3, 1, 2], 3.1, NUM_ERROR), 294 | ([3, 1, 2], 'abc', VALUE_ERROR), 295 | ([3, 1, 2], True, 3), 296 | ([3, 1, 2], False, NUM_ERROR), 297 | ([3, 1, 2], 'True', VALUE_ERROR), 298 | ([3, 1, 2], REF_ERROR, REF_ERROR), 299 | ([3, 1, 2], EMPTY, VALUE_ERROR), 300 | (REF_ERROR, 2, REF_ERROR), 301 | (None, 2, NUM_ERROR), 302 | ('abc', 2, NUM_ERROR), 303 | (99, 1, 99), 304 | ('99', 1, 99), 305 | ('99.9', 1, 99.9), 306 | (['99', 9], 1, 99), 307 | (['99.9', 9], 1, 99.9), 308 | ([3, 1, 2], None, NUM_ERROR), 309 | ([3, 1, 2], 0, NUM_ERROR), 310 | ([3, 1, 2], 4, NUM_ERROR), 311 | ([3, 1, 'aa'], 2, 1), 312 | ([3, 1, 'aa'], 3, NUM_ERROR), 313 | ([3, 1, True], 1, 3), 314 | ([3, 1, True], 3, NUM_ERROR), 315 | ([3, 1, '2'], 2, 2), 316 | ([3, 1, REF_ERROR], 1, REF_ERROR), 317 | ) 318 | ) 319 | def test_large(data, k, expected): 320 | assert large(data, k) == expected 321 | 322 | 323 | @pytest.mark.parametrize( 324 | 'X, Y, const, stats, expected', ( 325 | ([[1, 2, 3]], [[2, 3, 4]], None, None, ((1, -1),)), 326 | ([[1, 2, 3]], [[2, 3, 4]], True, None, ((1, -1),)), 327 | ([[1, 2, 3]], [[2, 3, 4]], False, None, ((0.6896551724137928, 0),)), 328 | ([[1, 2, 3]], [[2, 3, 4]], True, False, ((1, -1),)), 329 | ([[1, 2, 3]], [[2, 3, 4]], False, False, ((0.6896551724137928, 0),)), 330 | ([[1, 2, 3]], [[2, 3, 4]], None, True, ( 331 | (1, -1.0), 332 | (0, 0), 333 | (1.0, 0), 334 | ('#NUM!', 1), 335 | (2, 0), 336 | )), 337 | ([[1, 2, 3]], [[2, 3, 4]], True, True, ( 338 | (1, -1.0), 339 | (0, 0), 340 | (1.0, 0), 341 | ('#NUM!', 1), 342 | (2, 0), 343 | )), 344 | ([[1, 2, 3]], [[2, 3, 4]], False, True, ( 345 | (0.6896551724137928, 0), 346 | (0.05972588991616818, NA_ERROR), 347 | (0.9852216748768473, 0.32163376045133846), 348 | (133.33333333333348, 2), 349 | (13.79310344827585, 0.20689655172413793), 350 | )), 351 | ([[True, 2, 3]], [[2, 3, 4]], None, None, ((1, -1),)), 352 | ([['1', 2, 3]], [[2, 3, 4]], None, None, VALUE_ERROR), 353 | ([[1, 2, 3]], [[2, 3, '4']], None, None, VALUE_ERROR), 354 | ([[1, 2, 3]], [[2, 3, VALUE_ERROR]], None, None, VALUE_ERROR), 355 | ([[1, 2, 3]], [[2, 3, DIV0]], None, None, VALUE_ERROR), 356 | ([[NAME_ERROR, 2, 3]], [[2, 3, 4]], None, None, VALUE_ERROR), 357 | ([[1, 2, 3]], [[2, 3]], None, None, REF_ERROR), 358 | ([[1, 2, 3]], [[2, 2, 2]], None, None, ((0, 2),)), 359 | ([[1, 2, 3]], [[2, 2, 2], [3, 3, 3]], None, None, ((0, 0, 2),)), 360 | ([[1, 2, 3]], [[2, 2, 2], [3, 3, 3]], None, True, ( 361 | (0, 0, 2), 362 | (0, 0, 0), 363 | (1, 0, NA_ERROR), 364 | (NUM_ERROR, 0, NA_ERROR), 365 | (2, 0, NA_ERROR), 366 | )), 367 | ) 368 | ) 369 | def test_linest(X, Y, const, stats, expected): 370 | assert_np_close(linest(X, Y, const, stats), expected) 371 | 372 | 373 | @pytest.mark.parametrize( 374 | 'data, max_expected, min_expected', ( 375 | ('abcd', 0, 0), 376 | ((2, None, 'x', 3), 3, 2), 377 | ((-0.1, None, 'x', True), -0.1, -0.1), 378 | (VALUE_ERROR, VALUE_ERROR, VALUE_ERROR), 379 | ((2, VALUE_ERROR), VALUE_ERROR, VALUE_ERROR), 380 | (DIV0, DIV0, DIV0), 381 | ((2, DIV0), DIV0, DIV0), 382 | ) 383 | ) 384 | def test_max_min(data, max_expected, min_expected): 385 | assert max_(data) == max_expected 386 | assert min_(data) == min_expected 387 | 388 | 389 | @pytest.mark.parametrize( 390 | 'data, k, expected', ( 391 | ([3, 1, 2], 0, NUM_ERROR), 392 | ([3, 1, 2], 1, 1), 393 | ([3, 1, 2], 2, 2), 394 | ([3, 1, 2], 3, 3), 395 | ([3, 1, 2], 4, NUM_ERROR), 396 | ([3, 1, 2], '2', 2), 397 | ([3, 1, 2], 1.1, 2), 398 | ([3, 1, 2], '1.1', 2), 399 | ([3, 1, 2], 0.1, NUM_ERROR), 400 | ([3, 1, 2], 3.1, NUM_ERROR), 401 | ([3, 1, 2], 'abc', VALUE_ERROR), 402 | ([3, 1, 2], True, 1), 403 | ([3, 1, 2], False, NUM_ERROR), 404 | ([3, 1, 2], 'True', VALUE_ERROR), 405 | ([3, 1, 2], REF_ERROR, REF_ERROR), 406 | ([3, 1, 2], EMPTY, VALUE_ERROR), 407 | (REF_ERROR, 2, REF_ERROR), 408 | (None, 2, NUM_ERROR), 409 | ('abc', 2, NUM_ERROR), 410 | (99, 1, 99), 411 | ('99', 1, 99), 412 | ('99.9', 1, 99.9), 413 | (['99', 999], 1, 99), 414 | (['99.9', 999], 1, 99.9), 415 | ([3, 1, 2], None, NUM_ERROR), 416 | ([3, 1, 2], 0, NUM_ERROR), 417 | ([3, 1, 2], 4, NUM_ERROR), 418 | ([3, 1, 'aa'], 2, 3), 419 | ([3, 1, 'aa'], 3, NUM_ERROR), 420 | ([3, 1, True], 1, 1), 421 | ([3, 1, True], 3, NUM_ERROR), 422 | ([3, 1, '2'], 2, 2), 423 | ([3, 1, REF_ERROR], 1, REF_ERROR), 424 | ) 425 | ) 426 | def test_small(data, k, expected): 427 | assert small(data, k) == expected 428 | 429 | 430 | @pytest.mark.parametrize( 431 | 'Y, X, new_X, expected', ( 432 | ([[1, 2, 3, 4]], None, None, [[1, 2, 3, 4]]), 433 | ([[1, 2, 3, 4]], [[2, 3, 4, 5]], None, [[1, 2, 3, 4]]), 434 | ([[1, 2, 3, 4]], [[2, 3, 4, 5]], [[1, 2]], [[0, 1]]), 435 | ([[1, 2, 3, 4]], None, [[2, 3, 4, 5]], [[2, 3, 4, 5]]), 436 | ([[1, 2, 3, 4]], None, 3, 3), 437 | ([[1, 2, 3]], [[2, 2, 2], [3, 3, 3]], 1, REF_ERROR), 438 | ([[1, 2, 3]], [[2, 2, 2], [3, 3, 3]], None, ((2, 2, 2),)), 439 | ) 440 | ) 441 | def test_trend_shapes(Y, X, new_X, expected): 442 | import numpy as np 443 | 444 | expected = tuple(flatten(expected)) 445 | result = np.array(trend(Y, X, new_X, True)).ravel() 446 | assert_np_close(result, expected) 447 | 448 | result = np.array(trend([[x] for x in Y[0]], X, new_X)).ravel() 449 | assert_np_close(result, expected) 450 | 451 | if X is not None and new_X is None: 452 | result = np.array(trend([[x] for x in Y[0]], np.array(X).transpose(), None)).ravel() 453 | assert_np_close(result, expected) 454 | -------------------------------------------------------------------------------- /src/pycel/lib/lookup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # 3 | # Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors 4 | # All rights reserved. 5 | # This file is part of the Pycel Library, Licensed under GPLv3 (the 'License') 6 | # You may not use this work except in compliance with the License. 7 | # You may obtain a copy of the Licence at: 8 | # https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | """ 11 | Python equivalents of Lookup and Reference library functions 12 | """ 13 | from bisect import bisect_right 14 | 15 | import numpy as np 16 | 17 | from pycel.excelutil import ( 18 | AddressCell, 19 | AddressRange, 20 | build_wildcard_re, 21 | ERROR_CODES, 22 | ExcelCmp, 23 | flatten, 24 | is_address, 25 | list_like, 26 | MAX_COL, 27 | MAX_ROW, 28 | NA_ERROR, 29 | REF_ERROR, 30 | VALUE_ERROR, 31 | ) 32 | from pycel.lib.function_helpers import ( 33 | excel_helper, 34 | ) 35 | 36 | 37 | """ Functions consuming or producing references. 38 | INDEX() - Takes an array or reference, returns the value pointed to 39 | OFFSET() - Takes a reference, returns a reference 40 | INDIRECT() - Returns the reference specified by a text string. 41 | ROW() - Takes a reference, returns row number 42 | COLUMN() - Takes a reference, returns column number 43 | 44 | All of these can cause problems when compiling the workbook. 45 | 46 | OFFSET() and INDIRECT() should generally be avoided, as they can cause 47 | performance problems in any large spreadsheet. That is because the 48 | outputs are volatile. When compiling, this means they are not necessarily 49 | known when the sheet is compiled. In general use INDEX() instead of 50 | OFFSET(), and don't use INDIRECT() at all, if needing to compile the workbook. 51 | 52 | As a general reminder, all of these functions are volatile and can 53 | cause performance problems in large spreadsheets because of frequent 54 | need to recalc. 55 | 56 | OFFSET() 57 | INDIRECT() 58 | ROWS() 59 | COLUMNS() 60 | CELL() 61 | NOW() 62 | TODAY() 63 | """ 64 | 65 | 66 | def _match(lookup_value, lookup_array, match_type=1): 67 | # Excel reference: https://support.microsoft.com/en-us/office/ 68 | # MATCH-function-E8DFFD45-C762-47D6-BF89-533F4A37673A 69 | 70 | """ The relative position of a specified item in a range of cells. 71 | 72 | Match_type Behavior 73 | 74 | 1: return the largest value that is less than or equal to 75 | `lookup_value`. `lookup_array` must be in ascending order. 76 | 77 | 0: return the first value that is exactly equal to lookup_value. 78 | `lookup_array` can be in any order. 79 | 80 | -1: return the smallest value that is greater than or equal to 81 | `lookup_value`. `lookup_array` must be in descending order. 82 | 83 | If `match_type` is 0 and lookup_value is a text string, you can use the 84 | wildcard characters — the question mark (?) and asterisk (*). 85 | 86 | :param lookup_value: value to match (value or cell reference) 87 | :param lookup_array: range of cells being searched. 88 | :param match_type: The number -1, 0, or 1. 89 | :return: #N/A if not found, or relative position in `lookup_array` 90 | """ 91 | lookup_value = ExcelCmp(lookup_value) 92 | 93 | if match_type == 1: 94 | # Use a binary search to speed it up. Excel seems to do this as it 95 | # would explain the results seen when doing out of order searches. 96 | lo = 0 97 | while lo < len(lookup_array) and lookup_array[lo] is None: 98 | lo += 1 99 | 100 | hi = len(lookup_array) 101 | while hi > 0 and lookup_array[hi - 1] is None: 102 | hi -= 1 103 | 104 | result = bisect_right(lookup_array, lookup_value, lo=lo, hi=hi) 105 | while result and lookup_value.cmp_type != ExcelCmp( 106 | lookup_array[result - 1]).cmp_type: 107 | result -= 1 108 | if result == 0 or lookup_array[result - 1] is None: 109 | result = NA_ERROR 110 | return result 111 | 112 | result = [NA_ERROR] 113 | 114 | if match_type == 0: 115 | def compare(idx, val): 116 | if val == lookup_value: 117 | result[0] = idx 118 | return True 119 | 120 | if lookup_value.cmp_type == 1: 121 | # string matches might be wildcards 122 | re_compare = build_wildcard_re(lookup_value.value) 123 | if re_compare is not None: 124 | def compare(idx, val): # noqa: F811 125 | if re_compare(val.value): 126 | result[0] = idx 127 | return True 128 | else: 129 | def compare(idx, val): 130 | if val < lookup_value: 131 | return True 132 | result[0] = idx 133 | return val == lookup_value 134 | 135 | for i, value in enumerate(lookup_array, 1): 136 | if value not in ERROR_CODES: 137 | value = ExcelCmp(value) 138 | if value.cmp_type == lookup_value.cmp_type and compare(i, value): 139 | break 140 | 141 | return result[0] 142 | 143 | 144 | # def address(value): 145 | # Excel reference: https://support.microsoft.com/en-us/office/ 146 | # address-function-d0c26c0d-3991-446b-8de4-ab46431d4f89 147 | 148 | 149 | # def areas(value): 150 | # Excel reference: https://support.microsoft.com/en-us/office/ 151 | # areas-function-8392ba32-7a41-43b3-96b0-3695d2ec6152 152 | 153 | 154 | @excel_helper(cse_params=0, number_params=0, err_str_params=0) 155 | def choose(index, *args): 156 | # Excel reference: https://support.microsoft.com/en-us/office/ 157 | # choose-function-fc5c184f-cb62-4ec7-a46e-38653b98f5bc 158 | index = int(index) 159 | if index < 1 or len(args) < index or not args: 160 | return VALUE_ERROR 161 | return args[index - 1] 162 | 163 | 164 | @excel_helper(ref_params=0) 165 | def column(ref): 166 | # Excel reference: https://support.microsoft.com/en-us/office/ 167 | # COLUMN-function-44E8C754-711C-4DF3-9DA4-47A55042554B 168 | if ref.is_range: 169 | if ref.end.col_idx == 0: 170 | return range(1, MAX_COL + 1) 171 | else: 172 | return (tuple(range(ref.start.col_idx, ref.end.col_idx + 1)), ) 173 | else: 174 | return ref.col_idx 175 | 176 | 177 | # def columns(value): 178 | # Excel reference: https://support.microsoft.com/en-us/office/ 179 | # columns-function-4e8e7b4e-e603-43e8-b177-956088fa48ca 180 | 181 | 182 | # def filter(value): 183 | # Excel reference: https://support.microsoft.com/en-us/office/ 184 | # filter-function-f4f7cb66-82eb-4767-8f7c-4877ad80c759 185 | 186 | 187 | # def formulatext(value): 188 | # Excel reference: https://support.microsoft.com/en-us/office/ 189 | # formulatext-function-0a786771-54fd-4ae2-96ee-09cda35439c8 190 | 191 | 192 | # def getpivotdata(value): 193 | # Excel reference: https://support.microsoft.com/en-us/office/ 194 | # getpivotdata-function-8c083b99-a922-4ca0-af5e-3af55960761f 195 | 196 | 197 | @excel_helper(cse_params=0, bool_params=3, number_params=2, err_str_params=(0, 2, 3)) 198 | def hlookup(lookup_value, table_array, row_index_num, range_lookup=True): 199 | """ Horizontal Lookup 200 | 201 | :param lookup_value: value to match (value or cell reference) 202 | :param table_array: range of cells being searched. 203 | :param row_index_num: column number to return 204 | :param range_lookup: True, assumes sorted, finds nearest. False: find exact 205 | :return: #N/A if not found else value 206 | """ 207 | # Excel reference: https://support.microsoft.com/en-us/office/ 208 | # hlookup-function-a3034eec-b719-4ba3-bb65-e1ad662ed95f 209 | 210 | if not list_like(table_array): 211 | return NA_ERROR 212 | 213 | if row_index_num <= 0: 214 | return VALUE_ERROR 215 | 216 | if row_index_num > len(table_array): 217 | return REF_ERROR 218 | 219 | result_idx = _match( 220 | lookup_value, table_array[0], match_type=bool(range_lookup)) 221 | 222 | if isinstance(result_idx, int): 223 | return table_array[row_index_num - 1][result_idx - 1] 224 | else: 225 | # error string 226 | return result_idx 227 | 228 | 229 | # def hyperlink(value): 230 | # Excel reference: https://support.microsoft.com/en-us/office/ 231 | # hyperlink-function-333c7ce6-c5ae-4164-9c47-7de9b76f577f 232 | 233 | 234 | @excel_helper(err_str_params=(1, 2), number_params=(1, 2)) 235 | def index(array, row_num, col_num=None): 236 | # Excel reference: https://support.microsoft.com/en-us/office/ 237 | # index-function-a5dcf0dd-996d-40a4-a822-b56b061328bd 238 | 239 | if not list_like(array): 240 | if array in ERROR_CODES: 241 | return array 242 | else: 243 | return VALUE_ERROR 244 | if not list_like(array[0]): 245 | return VALUE_ERROR 246 | 247 | if is_address(array[0][0]): 248 | assert len({a for a in flatten(array)}) == 1 249 | _C_ = index.excel_func_meta['name_space']['_C_'] 250 | ref_addr = array[0][0].address_at_offset 251 | else: 252 | ref_addr = None 253 | 254 | def array_data(row, col): 255 | if ref_addr: 256 | return _C_(ref_addr(row, col).address) 257 | else: 258 | return array[row][col] 259 | 260 | try: 261 | # rectangular array 262 | if row_num and col_num: 263 | if row_num < 0 or col_num < 0: 264 | return VALUE_ERROR 265 | else: 266 | return array_data(row_num - 1, col_num - 1) 267 | 268 | elif row_num: 269 | if row_num < 0: 270 | return VALUE_ERROR 271 | elif len(array[0]) == 1: 272 | return array_data(row_num - 1, 0) 273 | elif len(array) == 1: 274 | return array_data(0, row_num - 1) 275 | elif isinstance(array, np.ndarray): 276 | return array[row_num - 1, :] 277 | else: 278 | return (tuple(array_data(row_num - 1, col) for col in range(len(array[0]))),) 279 | 280 | elif col_num: 281 | if col_num < 0: 282 | return VALUE_ERROR 283 | elif len(array) == 1: 284 | return array_data(0, col_num - 1) 285 | elif len(array[0]) == 1: 286 | return array_data(col_num - 1, 0) 287 | elif isinstance(array, np.ndarray): 288 | result = array[:, col_num - 1] 289 | result.shape = result.shape + (1,) 290 | return result 291 | else: 292 | return tuple((array_data(row, col_num - 1), ) for row in range(len(array))) 293 | 294 | except IndexError: 295 | return REF_ERROR 296 | 297 | else: 298 | return array 299 | 300 | 301 | @excel_helper(cse_params=0, number_params=1) 302 | def indirect(ref_text, a1=True, sheet=''): 303 | # Excel reference: https://support.microsoft.com/en-us/office/ 304 | # indirect-function-474b3a3a-8a26-4f44-b491-92b6306fa261 305 | try: 306 | address = AddressRange.create(ref_text) 307 | except ValueError: 308 | return REF_ERROR 309 | if address.row > MAX_ROW or address.col_idx > MAX_COL: 310 | return REF_ERROR 311 | if not address.has_sheet: 312 | address = AddressRange.create(address, sheet=sheet) 313 | return address 314 | 315 | 316 | @excel_helper(cse_params=0, err_str_params=0) 317 | def lookup(lookup_value, lookup_array, result_range=None): 318 | """ 319 | There are two ways to use LOOKUP: Vector form and Array form 320 | 321 | Vector form: lookup_array is list like (ie: n x 1) 322 | 323 | Array form: lookup_array is rectangular (ie: n x m) 324 | 325 | First row or column is the lookup vector. 326 | Last row or column is the result vector 327 | The longer dimension is the search dimension 328 | 329 | :param lookup_value: value to match (value or cell reference) 330 | :param lookup_array: range of cells being searched. 331 | :param result_range: (optional vector form) values are returned from here 332 | :return: #N/A if not found else value 333 | """ 334 | # Excel reference: https://support.microsoft.com/en-us/office/ 335 | # lookup-function-446d94af-663b-451d-8251-369d5e3864cb 336 | if not list_like(lookup_array): 337 | return NA_ERROR 338 | 339 | height = len(lookup_array) 340 | width = len(lookup_array[0]) 341 | 342 | # match across the largest dimension 343 | if width <= height: 344 | match_idx = _match(lookup_value, tuple(i[0] for i in lookup_array)) 345 | result = tuple(i[-1] for i in lookup_array) 346 | else: 347 | match_idx = _match(lookup_value, lookup_array[0]) 348 | result = lookup_array[-1] 349 | 350 | if result_range is not None: 351 | # if not a vector return NA 352 | if not list_like(result_range): 353 | return NA_ERROR 354 | rr_height = len(result_range) 355 | rr_width = len(result_range[0]) 356 | 357 | if rr_width < rr_height: 358 | if rr_width != 1: 359 | return NA_ERROR 360 | result = tuple(i[0] for i in result_range) 361 | else: 362 | if rr_height != 1: 363 | return NA_ERROR 364 | result = result_range[0] 365 | 366 | if isinstance(match_idx, int): 367 | return result[match_idx - 1] 368 | 369 | else: 370 | # error string 371 | return match_idx 372 | 373 | 374 | @excel_helper(cse_params=0, number_params=2, err_str_params=(0, 2)) 375 | def match(lookup_value, lookup_array, match_type=1): 376 | # Excel reference: https://support.microsoft.com/en-us/office/ 377 | # match-function-e8dffd45-c762-47d6-bf89-533f4a37673a 378 | if len(lookup_array) == 1: 379 | lookup_array = lookup_array[0] 380 | else: 381 | lookup_array = tuple(row[0] for row in lookup_array) 382 | 383 | return _match(lookup_value, lookup_array, match_type) 384 | 385 | 386 | @excel_helper(cse_params=(1, 2, 3, 4), ref_params=0, number_params=(1, 2)) 387 | def offset(reference, row_inc, col_inc, height=None, width=None): 388 | # Excel reference: https://support.microsoft.com/en-us/office/ 389 | # offset-function-c8de19ae-dd79-4b9b-a14e-b4d906d11b66 390 | """ 391 | Returns a reference to a range that is a specified number of rows and 392 | columns from a cell or range of cells. 393 | """ 394 | base_addr = AddressRange.create(reference) 395 | 396 | if height is None: 397 | height = base_addr.size.height 398 | if width is None: 399 | width = base_addr.size.width 400 | 401 | new_row = base_addr.row + row_inc 402 | end_row = new_row + height - 1 403 | new_col = base_addr.col_idx + col_inc 404 | end_col = new_col + width - 1 405 | 406 | if new_row <= 0 or end_row > MAX_ROW or new_col <= 0 or end_col > MAX_COL: 407 | return REF_ERROR 408 | 409 | top_left = AddressCell((new_col, new_row, new_col, new_row), 410 | sheet=base_addr.sheet) 411 | if height == width == 1: 412 | return top_left 413 | else: 414 | bottom_right = AddressCell((end_col, end_row, end_col, end_row), 415 | sheet=base_addr.sheet) 416 | 417 | return AddressRange(f'{top_left.coordinate}:{bottom_right.coordinate}', 418 | sheet=top_left.sheet) 419 | 420 | 421 | @excel_helper(ref_params=0) 422 | def row(ref): 423 | # Excel reference: https://support.microsoft.com/en-us/office/ 424 | # row-function-3a63b74a-c4d0-4093-b49a-e76eb49a6d8d 425 | if ref.is_range: 426 | if ref.end.row == 0: 427 | return range(1, MAX_ROW + 1) 428 | else: 429 | return tuple((c, ) for c in range(ref.start.row, ref.end.row + 1)) 430 | else: 431 | return ref.row 432 | 433 | 434 | # def rows(value): 435 | # Excel reference: https://support.microsoft.com/en-us/office/ 436 | # rows-function-b592593e-3fc2-47f2-bec1-bda493811597 437 | 438 | 439 | # def rtd(value): 440 | # Excel reference: https://support.microsoft.com/en-us/office/ 441 | # rtd-function-e0cc001a-56f0-470a-9b19-9455dc0eb593 442 | 443 | 444 | # def single(value): 445 | # Excel reference: https://support.microsoft.com/en-us/office/ 446 | # single-function-7ca229ca-13ae-420b-928e-2ef52a3805ff 447 | 448 | 449 | # def sort(value): 450 | # Excel reference: https://support.microsoft.com/en-us/office/ 451 | # sort-function-22f63bd0-ccc8-492f-953d-c20e8e44b86c 452 | 453 | 454 | # def sortby(value): 455 | # Excel reference: https://support.microsoft.com/en-us/office/ 456 | # sortby-function-cd2d7a62-1b93-435c-b561-d6a35134f28f 457 | 458 | 459 | # def transpose(value): 460 | # Excel reference: https://support.microsoft.com/en-us/office/ 461 | # transpose-function-ed039415-ed8a-4a81-93e9-4b6dfac76027 462 | 463 | 464 | # def unique(value): 465 | # Excel reference: https://support.microsoft.com/en-us/office/ 466 | # unique-function-c5ab87fd-30a3-4ce9-9d1a-40204fb85e1e 467 | 468 | 469 | @excel_helper(cse_params=0, bool_params=3, number_params=2, err_str_params=(0, 2, 3)) 470 | def vlookup(lookup_value, table_array, col_index_num, range_lookup=True): 471 | """ Vertical Lookup 472 | 473 | :param lookup_value: value to match (value or cell reference) 474 | :param table_array: range of cells being searched. 475 | :param col_index_num: column number to return 476 | :param range_lookup: True, assumes sorted, finds nearest. False: find exact 477 | :return: #N/A if not found else value 478 | """ 479 | # Excel reference: https://support.microsoft.com/en-us/office/ 480 | # VLOOKUP-function-0BBC8083-26FE-4963-8AB8-93A18AD188A1 481 | 482 | if not list_like(table_array): 483 | return NA_ERROR 484 | 485 | if col_index_num <= 0: 486 | return '#VALUE!' 487 | 488 | if col_index_num > len(table_array[0]): 489 | return REF_ERROR 490 | 491 | result_idx = _match( 492 | lookup_value, 493 | [row[0] for row in table_array], 494 | match_type=bool(range_lookup) 495 | ) 496 | 497 | if isinstance(result_idx, int): 498 | return table_array[result_idx - 1][col_index_num - 1] 499 | else: 500 | # error string 501 | return result_idx 502 | 503 | 504 | # def xlookup(value): 505 | # Excel reference: https://support.microsoft.com/en-us/office/ 506 | # xlookup-function-b7fd680e-6d10-43e6-84f9-88eae8bf5929 507 | 508 | 509 | # def xmatch(value): 510 | # Excel reference: https://support.microsoft.com/en-us/office/ 511 | # xmatch-function-d966da31-7a6b-4a13-a1c6-5a33ed6a0312 512 | --------------------------------------------------------------------------------