Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various functions #139

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions binder/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
matplotlib
networkx>=2.0,<2.7
numpy
numpy-financial
openpyxl>=2.6.2
python-dateutil
pydot
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def changes():
install_requires=[
'networkx>=2.0,<2.7',
'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