Skip to content Skip to sidebar Skip to footer

Measuring Months Between Two Dates : Legislative Definition Of Months

I'm looking to build code that represents the definition of months in a piece of Australian legislation - the Interpretations Act (1987). Please note that I am still a relative no

Solution 1:

This can be calculated without converting to date types except for the edge case where dates are the last day of the month (where they actually correspond to day zero of the next month).

from datetime import date

def isLastDay(y,m,d):
    return date.fromordinal(date(y,m,d).toordinal()+1).month != m

def legalMonthDif(date1,date2):
    y1,m1,d1 = map(int,date1.split("-"))
    y2,m2,d2 = map(int,date2.split("-"))
    if isLastDay(y1,m1,d1): m1,d1 = m1+1,0
    if isLastDay(y2,m2,d2): m2,d2 = m2+1,0
    return y2*12+m2 -y1*12-m1 -(d2<d1)

output:

legalMonthDif('2019-08-15','2020-02-05') #5
legalMonthDif('2019-08-31','2019-11-30') #3
legalMonthDif('2019-08-25','2019-09-10') #0
legalMonthDif('2019-08-25','2019-09-25') #1
legalMonthDif('2019-08-31','2019-11-30') #3
legalMonthDif('2019-08-01','2019-12-01') #4 
legalMonthDif('2019-08-31','2019-12-01') #3
legalMonthDif('2019-08-15','2019-12-01') #3

You could also do it completely without the datetime library by implementing a daysOfMonth function to compute the number of days in any month:

def daysOfMonth(y,m):
    return30+(m+m//8)%2-(m==2)*(2-(y%4==0andnoty%100==0ory%400==0))

def legalMonthDif(date1,date2):
    y1,m1,d1 = map(int,date1.split("-"))
    y2,m2,d2 = map(int,date2.split("-"))
    if daysOfMonth(y1,m1) == d1: m1,d1 = m1+1,0if daysOfMonth(y2,m2) == d2: m2,d2 = m2+1,0return y2*12+m2 -y1*12-m1 -(d2<d1)

Solution 2:

dates = [('2019-07-16','2019-08-15'),('2019-08-31','2019-09-30'),
         ('2019-08-15','2020-02-05'),('2019-08-31','2019-11-30'),
         ('2019-08-25','2019-09-10'),('2019-08-25','2019-09-25'),
         ('2019-08-31','2019-12-01'),('2019-08-15' , '2019-12-01'),
         ('2019-08-01', '2019-11-30'),('2019-08-01', '2019-12-01')]

Using Pandas date-time functionality. This relies on the fact that adding months to a timestamp will truncate to the end of the month if the resulting date doesn't exist - providing a means to test for the (b)(ii) part of the spec.

import pandas as pd

def f(a,b):
    earlier,later = sorted((a,b))
    rel_months = later.month - earlier.month
    delta_months = rel_months + (later.year - earlier.year) *12
    period_end = earlier + pd.DateOffset(months=delta_months)

    # sentinals for implementing logic of (b)(ii) of the definition
    period_end_isEOM = period_end + pd.tseries.offsets.MonthEnd(0)
    later_isEOM = later == later + pd.tseries.offsets.MonthEnd(0)
    next_month = period_end + pd.tseries.offsets.MonthBegin(0)

    # beginwith the delta - period_end == later -then adjust
    months = delta_months
    # this is straightforward
    if period_end > later:
        months -=1

    # did period_end get truncated to the endof a month
    if period_end_isEOM and (period_end.day < earlier.day):
        # actual endofperiod would be beginning of next month
        if later < next_month:    # probably also means later_isEOM or later == period_end
            months -=1return months 

for a,b in dates:
   a, b = map(pd.Timestamp, (a,b))
   c = f(a,b)
   print(f'{a.date()} - {b.date()} --> {c}')

>>>2019-07-16-2019-08-15--> 02019-08-31-2019-09-30--> 02019-08-15-2020-02-05--> 52019-08-31-2019-11-30--> 22019-08-25-2019-09-10--> 02019-08-25-2019-09-25--> 12019-08-31-2019-12-01--> 32019-08-15-2019-12-01--> 32019-08-01-2019-11-30--> 32019-08-01-2019-12-01--> 4>>>

pd.TimeStamp is an instance of datetime.datetime

This appears to work - only the OP can judge - but I can't help thinking that there is some builtin functionality I'm still not utilizing. Should be able to subclass pandas.DateOffset and customize it to make the calcs easier.


Solutions using a subclass of Pandas.DateOffset.

from pandas import DateOffset, Timestamp
from pandas.tseries.offsets import MonthBegin

classLegislativeMonth(DateOffset):
    def__init__(self, n=1, normalize=False, months=1):
        # restricted to months
        kwds = {'months':months}
        super().__init__(n=1, normalize=False, **kwds)
    defapply(self,other):
        end_date = super().apply(other)
        if end_date.day < other.day:
            # truncated to month end
            end_date = end_date + MonthBegin(1)
        return end_date

for a,b in dates:
   earlier,later = sorted(map(Timestamp, (a,b)))
   delta_months = later.month - earlier.month
   delta_months += (later.year - earlier.year) * 12
   end_of_period = earlier + LegislativeMonth(months=delta_months)
   if end_of_period > later:
       delta_months -= 1print(f'{earlier.date()} - {later.date()} --> {delta_months}')

# another

one_month = LegislativeMonth(months=1)
for a,b in dates:
   earlier,later = sorted(map(Timestamp, (a,b)))
   end_period = earlier
   months = 0while later >= end_period + one_month:
       months += 1
       end_period += one_month
   print(f'{earlier.date()} - {later.date()} --> {months}')

Finally it looks like relativedelta will do what you want if you ensure that it is called with the earlier date as the first item - (earlier,later)

from datetime import datetime
from dateutil.relativedelta import relativedelta

for a,b in dates:
##   earlier,later = sorted(map(Timestamp, (a,b)))
    earlier,later = sorted((datetime.strptime(a, '%Y-%m-%d'),
                            datetime.strptime(b, '%Y-%m-%d')))
    rd = relativedelta(earlier,later)
    print(f'{earlier.date()} - {later.date()} --> {abs(rd.months)}')

Using the dates at the top of this post all print the following:

2019-07-16-2019-08-15-->02019-08-31-2019-09-30-->02019-08-15-2020-02-05-->52019-08-31-2019-11-30-->22019-08-25-2019-09-10-->02019-08-25-2019-09-25-->12019-08-31-2019-12-01-->32019-08-15-2019-12-01-->32019-08-01-2019-11-30-->32019-08-01-2019-12-01-->4

Solution 3:

I ended up writing the below functions which capture the intended functionality of this legislation:

def find_corresponding_date(start_date):
day= start_date.day
month= start_date.month
year= start_date.year
next_month =month+1
next_year =year

if month==12:
    next_month =1
    next_year =year+1
try:
    new_date = py_datetime(year=next_year, month=next_month, day=day)
except ValueError:
    next_month = next_month +1
    if next_month ==13:
        next_month =1
        next_year = next_year +1
    new_date = py_datetime(year=next_year, month=next_month, day=1)
    return new_date

else:
    return new_date


def toPyDateTime(numpyDate):
    return py_datetime.strptime(str(numpyDate), "%Y-%m-%d")


def count_months(sdate, edate):
    start_date = toPyDateTime(sdate)
    end_date = toPyDateTime(edate)
    count =0
    corres_date = start_date
    while(True):
        corres_date = find_corresponding_date(corres_date)
        if(corres_date > end_date):
            return count
            break
        else:
            count = count +1

Post a Comment for "Measuring Months Between Two Dates : Legislative Definition Of Months"