# -*- coding: utf-8 -*-
# businessdate
# ------------
# A fast, efficient Python library for generating business dates inherited
# from float for fast date operations. Typical banking business methods
# are provided like business holidays adjustment, day count fractions.
# Beside dates generic business periods offer to create time periods like
# '10Y', '3 Months' or '2b'. Periods can easily added to business dates.
#
# Author: pbrisk <pbrisk@icloud.com>
# Copyright: 2016, 2017 Deutsche Postbank AG
# Website: https://github.com/pbrisk/businessdate
# License: APACHE Version 2 License (see LICENSE file)
from datetime import date, timedelta
from math import floor
#: float: basis for diff_in_years method
DAYS_IN_YEAR = 365.25
#: list(int): non-leap year number of days per month
_days_per_month = \
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
#: list(int): non-leap year cumulative number of days per month
_cum_month_days = \
[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
[docs]def is_leap_year(year):
"""
returns True for leap year and False otherwise
:param int year: calendar year
:return bool:
"""
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
[docs]def days_in_year(year):
"""
returns number of days in the given calendar year
:param int year: calendar year
:return int:
"""
return 366 if is_leap_year(year) else 365
[docs]def days_in_month(year, month):
"""
returns number of days for the given year and month
:param int year: calendar year
:param int month: calendar month
:return int:
"""
eom = _days_per_month[month - 1]
if is_leap_year(year) and month == 2:
eom += 1
return eom
[docs]def is_valid_ymd(year, month, day):
"""
return True if (year,month, day) can be represented in Excel-notation
(number of days since 30.12.1899) for calendar days, otherwise False
:param int year: calendar year
:param int month: calendar month
:param int day: calendar day
:return bool:
"""
return 1 <= month <= 12 and 1 <= day <= days_in_month(year, month) and year >= 1899
[docs]def from_excel_to_ymd(excel_int):
"""
converts date in Microsoft Excel representation style and returns `(year, month, day)` tuple
:param int excel_int: date as int (days since 1899-12-31)
:return tuple(int, int, int):
"""
int_date = int(floor(excel_int))
int_date -= 1 # count from 31.12.1899 instead of from 30.12.1899
year = (int_date - 1) // 365
rest_days = int_date - 365 * year - (year + 3) // 4 + (year + 99) // 100 - (year + 299) // 400
year += 1900
while rest_days <= 0:
year -= 1
rest_days += days_in_year(year)
month = 1
if is_leap_year(year) and rest_days == 60:
month = 2
day = 29
else:
if is_leap_year(year) and rest_days > 60:
rest_days -= 1
while rest_days > _cum_month_days[month]:
month += 1
day = rest_days - _cum_month_days[month - 1]
return year, month, day
[docs]def from_ymd_to_excel(year, month, day):
"""
converts date as `(year, month, day)` tuple into Microsoft Excel representation style
:param tuple(int, int, int): int tuple `year, month, day`
:return int:
"""
if not is_valid_ymd(year, month, day):
raise ValueError("Invalid date {0}.{1}.{2}".format(year, month, day))
days = _cum_month_days[month - 1] + day
days += 1 if (is_leap_year(year) and month > 2) else 0
years_distance = year - 1900
days += years_distance * 365 + \
(years_distance + 3) // 4 - (years_distance + 99) // 100 + (years_distance + 299) // 400
# count days since 30.12.1899 (excluding 30.12.1899) (workaround for excel bug)
days += 1
return days
[docs]class BaseDateFloat(float):
# --- property methods ---------------------------------------------------
@property
def day(self):
"""
day of date
:return int:
"""
return BaseDateFloat.to_ymd(self)[2]
@property
def month(self):
"""
month of date
:return int:
"""
return BaseDateFloat.to_ymd(self)[1]
@property
def year(self):
"""
year of date
:return int:
"""
return BaseDateFloat.to_ymd(self)[0]
# --- constructor method -------------------------------------------------
@staticmethod
[docs] def from_ymd(year, month, day):
"""
creates date for year, month and day
:param int year:
:param int month:
:param int day:
:return BaseDate:
"""
return BaseDate(from_ymd_to_excel(year, month, day))
# --- cast method --------------------------------------------------------
@staticmethod
[docs] def to_ymd(d):
"""
returns date represented as tuple `year, month, day`
:param BaseDateFloat d:
:return tuple(int, int, int):
"""
return from_excel_to_ymd(d)
# --- calculation methods ------------------------------------------------
@staticmethod
[docs] def add_days(d, days_int):
"""
adds number of days to a date
:param BaseDateFloat d: date to add days to
:param int days_int: number of days to add
:return BaseDate: resulting date
"""
"""
addition of a number of days
:param BusinessDate d:
:param int days_int:
:return bankdate:
"""
return BaseDate(super(BaseDate, d).__add__(days_int))
@staticmethod
[docs] def add_years(d, years_int):
"""
adds number of years to a date
:param BaseDateFloat d: date to add years to
:param int years_int: number of years to add
:return BaseDate: resulting date
"""
"""
addition of a number of years
:param BusinessDate d:
:param int years_int:
:return bankdate:
"""
y, m, d = BaseDate.to_ymd(d)
if not is_leap_year(years_int) and m == 2:
d = min(28, d)
return BaseDate.from_ymd(y + years_int, m, d)
@staticmethod
[docs] def diff_in_days(start, end):
"""
returns distance of two dates as number of days
:param BaseDateFloat start: start date
:param BaseDateFloat end: end date
:return float: difference between end date and start date in days
"""
return super(BaseDateFloat, end).__sub__(start)
@staticmethod
[docs] def diff_in_years(start, end):
"""
calculate difference between given dates in years. The difference corresponds to Act/365.25 year fraction
:param BaseDateFloat start: state date
:param BaseDateFloat end: end date
:return float: difference between end date and start date in years
"""
return BaseDateFloat.diff_in_days(start, end) / DAYS_IN_YEAR
[docs]class BaseDatetimeDate(date):
# --- property methods ---------------------------------------------------
# day
# month
# year
# --- constructor method -------------------------------------------------
@staticmethod
[docs] def from_ymd(year, month, day):
"""
converts date as `(year, month, day)` tuple into Microsoft Excel representation style
:param tuple(int, int, int): int tuple `year, month, day`
:return BaseDatetimeDate:
"""
return BaseDatetimeDate(year, month, day)
# --- cast method --------------------------------------------------------
@staticmethod
[docs] def to_ymd(d):
"""
returns date represented as tuple `year, month, day`
:param BaseDatetimeDate d:
:return tuple(int, int, int):
"""
return d.year, d.month, d.day
# --- calculation methods ------------------------------------------------
@staticmethod
[docs] def add_days(d, days_int):
"""
addition of a number of days
:param BaseDatetimeDate d:
:param int days_int:
:return BaseDatetimeDate:
"""
n = date(d.year, d.month, d.day) + timedelta(days_int)
return BaseDatetimeDate(n.year, n.month, n.day)
@staticmethod
[docs] def add_years(d, years_int):
"""
addition of a number of years
:param BaseDatetimeDate d:
:param int years_int:
:return BaseDatetimeDate:
"""
y, m, d = BaseDatetimeDate.to_ymd(d)
y += years_int
if not is_leap_year(y) and m == 2:
d = min(28, d)
return BaseDatetimeDate.from_ymd(y, m, d)
@staticmethod
[docs] def diff_in_days(start, end):
"""
calculate difference between given dates in days
:param BaseDatetimeDate start: state date
:param BaseDatetimeDate end: end date
:return float: difference between end date and start date in days
"""
diff = date(end.year, end.month, end.day) - date(start.year, start.month, start.day)
return float(diff.days)
@staticmethod
[docs] def diff_in_years(start, end):
"""
calculate difference between given dates in years. The difference corresponds to Act/365.25 year fraction
:param BaseDatetimeDate start: state date
:param BaseDatetimeDate end: end date
:return float: difference between end date and start date in years
"""
return BaseDatetimeDate.diff_in_days(start, end) / DAYS_IN_YEAR
[docs]class BaseDate(BaseDateFloat):
"""
base class for BusinessDate
"""
pass