From 4bad095851e710df3d6c831a887c864f7e67885a Mon Sep 17 00:00:00 2001 From: danielsjf Date: Tue, 9 Apr 2019 22:42:30 +0200 Subject: [PATCH 1/7] Add YEAR(), MONTH() and EOMONTH() Also import datetime to avoid conflicts with the redefined date function --- koala/excellib.py | 60 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/koala/excellib.py b/koala/excellib.py index 7ca17317..4049bfbf 100644 --- a/koala/excellib.py +++ b/koala/excellib.py @@ -9,9 +9,11 @@ from __future__ import absolute_import, division import numpy as np -from datetime import datetime, date +import datetime from math import log, ceil from decimal import Decimal, ROUND_UP, ROUND_HALF_UP +from calendar import monthrange +from dateutil.relativedelta import relativedelta from openpyxl.compat import unicode @@ -99,11 +101,13 @@ "ROUNDUP", "POWER", "SQRT", - "TODAY" + "TODAY", + "YEAR", + "MONTH", ] CELL_CHARACTER_LIMIT = 32767 -EXCEL_EPOCH = datetime.strptime("1900-01-01", '%Y-%m-%d').date() +EXCEL_EPOCH = datetime.datetime.strptime("1900-01-01", '%Y-%m-%d').date() ###################################################################################### # List of excel equivalent functions @@ -419,6 +423,48 @@ def mod(nb, q): # Excel Reference: https://support.office.com/en-us/article/MOD- return nb % q +def eomonth(start_date, months): # Excel reference: https://support.office.com/en-us/article/eomonth-function-7314ffa1-2bc9-4005-9d66-f49db127d628 + if not is_number(start_date): + return ExcelError('#VALUE!', 'start_date %s must be a number' % str(start_date)) + if start_date < 0: + return ExcelError('#VALUE!', 'start_date %s must be positive' % str(start_date)) + + if not is_number(months): + return ExcelError('#VALUE!', 'months %s must be a number' % str(months)) + + y1, m1, d1 = date_from_int(start_date) + start_date_d = datetime.date(year=y1, month=m1, day=d1) + end_date_d = start_date_d + relativedelta(months=months) + y2 = end_date_d.year + m2 = end_date_d.month + d2 = monthrange(y2, m2)[1] + res = int(excel_date(datetime.date(y2, m2, d2))) + + return res + + +def year(serial_number): # Excel reference: https://support.office.com/en-us/article/year-function-c64f017a-1354-490d-981f-578e8ec8d3b9 + if not is_number(serial_number): + return ExcelError('#VALUE!', 'start_date %s must be a number' % str(serial_number)) + if serial_number < 0: + return ExcelError('#VALUE!', 'start_date %s must be positive' % str(serial_number)) + + y1, m1, d1 = date_from_int(serial_number) + + return y1 + + +def month(serial_number): # Excel reference: https://support.office.com/en-us/article/month-function-579a2881-199b-48b2-ab90-ddba0eba86e8 + if not is_number(serial_number): + return ExcelError('#VALUE!', 'start_date %s must be a number' % str(serial_number)) + if serial_number < 0: + return ExcelError('#VALUE!', 'start_date %s must be positive' % str(serial_number)) + + y1, m1, d1 = date_from_int(serial_number) + + return m1 + + def count(*args): # Excel reference: https://support.office.com/en-us/article/COUNT-function-a59cd7fc-b623-4d93-87a4-d23bf411294c l = list(args) @@ -590,10 +636,10 @@ def date(year, month, day): # Excel reference: https://support.office.com/en-us/ year, month, day = normalize_year(year, month, day) # taking into account negative month and day values - date_0 = datetime(1900, 1, 1) - date = datetime(year, month, day) + date_0 = datetime.datetime(1900, 1, 1) + date = datetime.datetime(year, month, day) - result = (datetime(year, month, day) - date_0).days + 2 + result = (datetime.datetime(year, month, day) - date_0).days + 2 if result <= 0: return ExcelError('#VALUE!', 'Date result is negative') @@ -980,7 +1026,7 @@ def sqrt(number): # https://support.office.com/en-ie/article/today-function-5eb3078d-a82c-4736-8930-2f51a028fdd9 def today(): - reference_date = datetime.today().date() + reference_date = datetime.datetime.today().date() days_since_epoch = reference_date - EXCEL_EPOCH # why +2 ? # 1 based from 1900-01-01 From 4472e7972675427ce6ab9978100f1023d62fdff2 Mon Sep 17 00:00:00 2001 From: danielsjf Date: Tue, 9 Apr 2019 22:43:35 +0200 Subject: [PATCH 2/7] Add helper function for the excel date --- koala/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/koala/utils.py b/koala/utils.py index b53e08e7..d6a21b66 100644 --- a/koala/utils.py +++ b/koala/utils.py @@ -5,6 +5,7 @@ import collections import numbers import re +import datetime as dt from six import string_types from openpyxl.compat import unicode @@ -131,7 +132,7 @@ def resolve_range(rng, should_flatten = False, sheet=''): start_row = start end_col = "XFD" end_row = end - else: + else: sh, start_col, start_row = split_address(start) sh, end_col, end_row = split_address(end) @@ -407,11 +408,17 @@ def date_from_int(nb): if nb > max_days: nb -= max_days else: - current_day = nb + current_day = int(nb) nb = 0 return (current_year, current_month, current_day) +def excel_date(date): + temp = dt.date(1899, 12, 30) # Note, not 31st Dec but 30th! + delta = date - temp + + return float(delta.days) + (float(delta.seconds) / 86400) + def criteria_parser(criteria): if is_number(criteria): From 3f18e46cf4a0cd695b85074c854f4a3946058a02 Mon Sep 17 00:00:00 2001 From: danielsjf Date: Tue, 9 Apr 2019 22:44:19 +0200 Subject: [PATCH 3/7] Add requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f82bea12..7d1b27ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ numpy==1.14.2 Cython==0.28.2 lxml==4.1.1 six==1.11.0 +python-dateutil==2.8.0 \ No newline at end of file From da51e21a34989385956a0da7b2c422e535d05179 Mon Sep 17 00:00:00 2001 From: danielsjf Date: Tue, 9 Apr 2019 22:45:46 +0200 Subject: [PATCH 4/7] Rename function --- koala/excellib.py | 2 +- koala/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/koala/excellib.py b/koala/excellib.py index 4049bfbf..2afd27ae 100644 --- a/koala/excellib.py +++ b/koala/excellib.py @@ -438,7 +438,7 @@ def eomonth(start_date, months): # Excel reference: https://support.office.com/e y2 = end_date_d.year m2 = end_date_d.month d2 = monthrange(y2, m2)[1] - res = int(excel_date(datetime.date(y2, m2, d2))) + res = int(int_from_date(datetime.date(y2, m2, d2))) return res diff --git a/koala/utils.py b/koala/utils.py index d6a21b66..3137f7fe 100644 --- a/koala/utils.py +++ b/koala/utils.py @@ -413,7 +413,7 @@ def date_from_int(nb): return (current_year, current_month, current_day) -def excel_date(date): +def int_from_date(date): temp = dt.date(1899, 12, 30) # Note, not 31st Dec but 30th! delta = date - temp From e368a783b02b3052c01ec17c34d7d2dcabf9e50f Mon Sep 17 00:00:00 2001 From: danielsjf Date: Wed, 10 Apr 2019 00:01:58 +0200 Subject: [PATCH 5/7] Fix test Import changed --- tests/excel/test_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/excel/test_functions.py b/tests/excel/test_functions.py index 0f573f7f..b718c52d 100644 --- a/tests/excel/test_functions.py +++ b/tests/excel/test_functions.py @@ -722,8 +722,8 @@ def test_positive_integers(self): class Test_Today(unittest.TestCase): - EXCEL_EPOCH = datetime.strptime("1900-01-01", '%Y-%m-%d').date() - reference_date = datetime.today().date() + EXCEL_EPOCH = datetime.datetime.strptime("1900-01-01", '%Y-%m-%d').date() + reference_date = datetime.datetime.today().date() days_since_epoch = reference_date - EXCEL_EPOCH todays_ordinal = days_since_epoch.days + 2 From e235108af41949774d839e5393bcab85984e5eaf Mon Sep 17 00:00:00 2001 From: danielsjf Date: Thu, 11 Apr 2019 09:28:43 +0200 Subject: [PATCH 6/7] Add tests --- tests/excel/test_functions.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/excel/test_functions.py b/tests/excel/test_functions.py index b718c52d..e0d8cb27 100644 --- a/tests/excel/test_functions.py +++ b/tests/excel/test_functions.py @@ -719,6 +719,9 @@ def test_first_argument_validity(self): def test_positive_integers(self): self.assertEqual(sqrt(16), 4) + def test_float(self): + self.assertEqual(sqrt(.25), .5) + class Test_Today(unittest.TestCase): @@ -740,3 +743,29 @@ def test_first_argument_validity(self): def test_concatenate(self): self.assertEqual(concatenate("Hello", " ", "World!"), "Hello World!") + + +class Test_Year(unittest.TestCase): + + def test_results(self): + self.assertEqual(year(43566), 2019) # 11/04/2019 + self.assertEqual(year(43831), 2020) # 01/01/2020 + self.assertEqual(year(36525), 1999) # 31/12/1999 + + +class Test_Month(unittest.TestCase): + + def test_results(self): + self.assertEqual(month(43566), 4) # 11/04/2019 + self.assertEqual(month(43831), 1) # 01/01/2020 + self.assertEqual(month(36525), 12) # 31/12/1999 + + +class Test_Eomonth(unittest.TestCase): + + def test_results(self): + self.assertEqual(eomonth(43566, 2), 43646) # 11/04/2019, add 2 months + self.assertEqual(eomonth(43831, 5), 44012) # 01/01/2020, add 5 months + self.assertEqual(eomonth(36525, 1), 36556) # 31/12/1999, add 1 month + self.assertEqual(eomonth(36525, 15), 36981) # 31/12/1999, add 15 month + self.assertNotEqual(eomonth(36525, 15), 36980) # 31/12/1999, add 15 month From 40ec42f615191dfb808de6903269d61fde761ed3 Mon Sep 17 00:00:00 2001 From: danielsjf Date: Fri, 12 Apr 2019 09:34:51 +0200 Subject: [PATCH 7/7] Add Eomonth --- koala/excellib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/koala/excellib.py b/koala/excellib.py index 2afd27ae..6b8350e3 100644 --- a/koala/excellib.py +++ b/koala/excellib.py @@ -104,6 +104,7 @@ "TODAY", "YEAR", "MONTH", + "EOMONTH", ] CELL_CHARACTER_LIMIT = 32767