Skip to content
3 changes: 2 additions & 1 deletion binder/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
matplotlib
networkx>=2.0,<2.7
networkx>=2.0
numpy
numpy-financial
openpyxl>=2.6.2
python-dateutil
pydot
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ def changes():
tests_require=tests_require,
test_suite='pytest',
install_requires=[
'networkx>=2.0,<2.7',
'networkx>=2.0',
'numpy',
'numpy-financial',
'openpyxl>=2.6.2',
'python-dateutil',
'ruamel.yaml',
Expand Down
35 changes: 35 additions & 0 deletions src/pycel/lib/financial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: UTF-8 -*-
#
# Copyright 2011-2019 by Dirk Gorissen, Stephen Rauch and Contributors
# All rights reserved.
# This file is part of the Pycel Library, Licensed under GPLv3 (the 'License')
# You may not use this work except in compliance with the License.
# You may obtain a copy of the Licence at:
# https://www.gnu.org/licenses/gpl-3.0.en.html

"""
Python equivalents of Excel financial functions
"""
import numpy_financial as npf

from pycel.excelutil import flatten


def irr(values, guess=None):
# Excel reference: https://support.microsoft.com/en-us/office/
# irr-function-64925eaa-9988-495b-b290-3ad0c163c1bc

# currently guess is not used
return npf.irr(list(flatten(values)))


def pmt(rate, nper, pv, fv=0, when=0):
# Excel reference: https://support.microsoft.com/en-us/office/
# pmt-function-0214da64-9a63-4996-bc20-214433fa6441
return npf.pmt(rate, nper, pv, fv=fv, when=when)


def ppmt(rate, per, nper, pv, fv=0, when=0):
# Excel reference: https://support.microsoft.com/en-us/office/
# ppmt-function-c370d9e3-7749-4ca4-beea-b06c6ac95e1b
return npf.ppmt(rate, per, nper, pv, fv=fv, when=when)
46 changes: 42 additions & 4 deletions src/pycel/lib/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from bisect import bisect_right

import numpy as np
from openpyxl.utils import get_column_letter

from pycel.excelutil import (
AddressCell,
Expand Down Expand Up @@ -141,9 +142,19 @@ def compare(idx, val):
return result[0]


# def address(value):
def address(row_num, column_num, abs_num=1, style=None, sheet_text=''):
# Excel reference: https://support.microsoft.com/en-us/office/
# address-function-d0c26c0d-3991-446b-8de4-ab46431d4f89
sheet_text = "'" + sheet_text + "'!" if sheet_text else sheet_text
if style == 0:
r = str(row_num) if abs_num in [1, 2] else str([row_num])
c = str(column_num) if abs_num in [1, 3] else str([column_num])
return f'{sheet_text}R{r}C{c}'
else:
abs_row = '$' if abs_num in [1, 2] else ''
abs_col = '$' if abs_num in [1, 3] else ''
return f'{sheet_text}{abs_col}{get_column_letter(column_num)}' \
f'{abs_row}{str(row_num)}'


# def areas(value):
Expand Down Expand Up @@ -174,14 +185,38 @@ def column(ref):
return ref.col_idx


# def columns(value):
def columns(values):
# Excel reference: https://support.microsoft.com/en-us/office/
# columns-function-4e8e7b4e-e603-43e8-b177-956088fa48ca
if list_like(values):
return len(values[0])
return 1


# def filter(value):
def _xlws_filter(values, include, if_empty=VALUE_ERROR):
# Excel reference: https://support.microsoft.com/en-us/office/
# filter-function-f4f7cb66-82eb-4767-8f7c-4877ad80c759
if not list_like(include):
if not isinstance(values, tuple) or len(values) == 1 or len(values[0]) == 1:
return values if include else if_empty
return if_empty

res = None
if len(values[0]) == len(include[0]) and not len(include) > 1:
transpose = tuple(col for col in zip(*values))
res = [transpose[i] for i in range(len(transpose))
if include[0][i]]
res = tuple([col for col in zip(*res)])

elif len(values) == len(include) and not len(include[0]) > 1:
res = tuple([values[i] for i in range(len(values))
if include[i][0]])

if res:
return res
if res is None:
return VALUE_ERROR
return if_empty


# def formulatext(value):
Expand Down Expand Up @@ -431,9 +466,12 @@ def row(ref):
return ref.row


# def rows(value):
def rows(values):
# Excel reference: https://support.microsoft.com/en-us/office/
# rows-function-b592593e-3fc2-47f2-bec1-bda493811597
if list_like(values):
return len(values)
return 1


# def rtd(value):
Expand Down
21 changes: 19 additions & 2 deletions src/pycel/lib/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,31 @@ def count(*args):
if isinstance(x, (int, float)) and not isinstance(x, bool))


# def counta(value):
def counta(*args):
# Excel reference: https://support.microsoft.com/en-us/office/
# counta-function-7dc98875-d5c1-46f1-9a82-53f3219e2509
res = 0
for arg in args:
if list_like(arg):
for row in arg:
for cell in row:
res = res + 1 if cell is not None else res
else:
res = res + 1 if arg else res
return res


# def countblank(value):
def countblank(values):
# Excel reference: https://support.microsoft.com/en-us/office/
# countblank-function-6a92d772-675c-4bee-b346-24af6bd3ac22
res = 0
if list_like(values):
for row in values:
for cell in row:
res = res + 1 if cell in [None, ''] else res
else:
res = 1 if values in [None, ''] else res
return res


def countif(rng, criteria):
Expand Down
Binary file added tests/fixtures/filter.xlsx
Binary file not shown.
61 changes: 61 additions & 0 deletions tests/lib/test_financial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: UTF-8 -*-
#
# Copyright 2011-2021 by Dirk Gorissen, Stephen Rauch and Contributors
# All rights reserved.
# This file is part of the Pycel Library, Licensed under GPLv3 (the 'License')
# You may not use this work except in compliance with the License.
# You may obtain a copy of the Licence at:
# https://www.gnu.org/licenses/gpl-3.0.en.html

import math

import pytest

from pycel.lib.financial import (
irr,
pmt,
ppmt
)


@pytest.mark.parametrize(
'values, guess, expected',
(
((-100, -50, 100, 200, 400), None, 0.671269),
((-70000, 12000, 15000, 18000, 21000, 26000),
None, 0.086631),
((-70000, 12000, 15000, 18000, 21000),
None, -0.021245),
((-70000, 12000, 15000), 0.10, -0.443507)
)
)
def test_irr(values, guess, expected):
assert math.isclose(irr(values, guess=guess),
expected, abs_tol=1e-4)


@pytest.mark.parametrize(
'rate, nper, pv, fv, when, expected',
(
(0.05, 12, 100, 400, 0, -36.412705),
(0.00667, 10, 10000, 0, 0, -1037.050788),
(0.00667, 10, 10000, 0, 1, -1030.179490),
(0.005, 216, 0, 50000, 0, -129.0811609)
)
)
def test_pmt(rate, nper, pv, fv, when, expected):
assert math.isclose(pmt(rate, nper, pv, fv, when),
expected, abs_tol=1e-4)


@pytest.mark.parametrize(
'rate, per, nper, pv, fv, when, expected',
(
(0.05, 12, 100, 400, 0, 0, -0.262118),
(0.00833, 1, 24, 2000, 0, 0, -75.626160),
(0.08, 10, 10, 200000, 0, 0, -27598.053460)
)
)
def test_ppmt(rate, per, nper, pv, fv, when, expected):
assert math.isclose(ppmt(rate, per, nper, pv, fv, when),
expected, abs_tol=1e-4)
54 changes: 54 additions & 0 deletions tests/lib/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@
from pycel.lib.function_helpers import error_string_wrapper, load_to_test_module
from pycel.lib.lookup import (
_match,
address,
choose,
column,
columns,
hlookup,
index,
indirect,
lookup,
match,
offset,
row,
rows,
vlookup,
)

Expand Down Expand Up @@ -72,6 +75,24 @@ def test_lookup_ws(fixture_xls_copy):
assert indirect == 8


@pytest.mark.parametrize(
'row_num, col_num, abs_num, style, sheet_text, expected',
(
(2, 3, 1, None, '', '$C$2'),
(2, 3, 3, None, '', '$C2'),
(2, 3, 2, None, '', 'C$2'),
(2, 3, 2, False, '', 'R2C[3]'),
(2, 3, 2, True, '', 'C$2'),
(5, 4, 4, True, 'Sheet1', '\'Sheet1\'!D5'),
(5, 4, 1, True, 'Sheet1', '\'Sheet1\'!$D$5'),
(5, 4, 1, None, 'Sheet1', '\'Sheet1\'!$D$5'),
(5, 4, 1, False, 'Sheet1', '\'Sheet1\'!R5C4'),
)
)
def test_address(row_num, col_num, abs_num, style, sheet_text, expected):
assert address(row_num, col_num, abs_num, style, sheet_text) == expected


@pytest.mark.parametrize(
'index, data, expected', (
(-1, 'ABCDEFG', VALUE_ERROR),
Expand Down Expand Up @@ -122,6 +143,25 @@ def test_column(address, expected):
assert expected == result


@pytest.mark.parametrize(
'values, expected', (
(((1, None, None), (1, 2, None)), 3),
(1, 1),
("s", 1),
(((1.2, 3.4), (0.4, 5)), 2),
(((None, None, None, None,), ), 4)
)
)
def test_columns(values, expected):
assert columns(values) == expected


def test_xlws_filter(fixture_xls_copy):
compiler = ExcelCompiler(fixture_xls_copy('filter.xlsx'))
result = compiler.validate_serialized()
assert result == {}


@pytest.mark.parametrize(
'lkup, row_idx, result, approx', (
('A', 0, VALUE_ERROR, True),
Expand Down Expand Up @@ -566,6 +606,20 @@ def test_row(address, expected):
assert expected == result


@pytest.mark.parametrize(
'values, expected', (
(((1, None, None), (1, 2, None)), 2),
(1, 1),
("s", 1),
(((1.2, 3.4), (0.4, 5)), 2),
(((None, None,), ), 1),
(((1,), (2,), (3,)), 3)
)
)
def test_rows(values, expected):
assert rows(values) == expected


@pytest.mark.parametrize(
'lkup, col_idx, result, approx', (
('A', 0, VALUE_ERROR, True),
Expand Down
29 changes: 29 additions & 0 deletions tests/lib/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
averageif,
averageifs,
count,
counta,
countblank,
countif,
countifs,
forecast,
Expand Down Expand Up @@ -124,6 +126,33 @@ def test_count():
assert count(data, data[3], data[5], data[7])


@pytest.mark.parametrize(
'values, expected', (
((((7, 0, 1, None), ), ), 3),
((((True, False, 0, ''), ), ), 4),
((True, ), 1),
((((False, NA_ERROR, VALUE_ERROR), ), ), 3),
(((('', None, 4), (NAME_ERROR, True, 0)), ), 5),
(((('', None, 4), ), 7, ((4, ), (5, )), 10), 6),
)
)
def test_counta(values, expected):
assert counta(*values) == expected


@pytest.mark.parametrize(
'values, expected', (
(((7, 0, 1, None), ), 1),
(((True, False, 0, ''), ), 1),
(False, 0),
(((False, NA_ERROR, None), ), 1),
((('', None, 4), (NAME_ERROR, True, 0)), 2),
)
)
def test_countblank(values, expected):
assert countblank(values) == expected


@pytest.mark.parametrize(
'value, criteria, expected', (
(((7, 25, 13, 25), ), '>10', 3),
Expand Down