Page MenuHomePhabricator

Add ability to add/subtract months to/from pywikibot.Timestamp
Closed, ResolvedPublic

Description

mw is able to calculate with parser variable #time e.g. to add or subtract months to the given time. python does not support it with standard libs and dateutils is not part of the framework. On the other hand it would be great to have such a feature calculating months with Timestamps.


Version: core-(2.0)
Severity: enhancement

Event Timeline

bzimport raised the priority of this task from to Needs Triage.Nov 22 2014, 3:45 AM
bzimport set Reference to bz71124.
bzimport added a subscriber: Unknown Object (????).

Timestamp is a subclass of datetime.datetime. Can't we just use timedelta?

https://docs.python.org/2/library/datetime.html#timedelta-objects

This is what I did in newitem.py or do you want to do something else?

The problem is that months have different days between 28 an 31. Adding 1 month to 7th September should result in 7th October. Adding 7th October should give 7th November as result. dateutils does it. The highest timedelta resolution is "days" not "months". We could use constants from calendar module to implement it. I need it to calculate log entries. mw is able to calculate time stamps by adding months and years, whereas datetime objects aren't able to.

If we want to be independent of other packages we could add 28 days to a datetime and then add one day as long as the day number is the same as the original day number.

This obviously only works with the Gregorian/Julian calendar. If we don't skip 28 days by default and just iterate by one day we could support any calendar (in theory).

Okay here is one flexible solution which does work with any month length and any number of months (> 1) in a year:

def month_delta(date, month_delta=1):                                           
    if int(month_delta) != month_delta:                                         
        raise ValueError('Month delta must be an integer')                      
    delta = -1 if month_delta < 0 else +1                                       
    month = date.month                                                          
    target_day = date.day                                                       
    while date.day != target_day or month_delta != 0:                           
        date += datetime.timedelta(days=delta)                                  
        if date.month != month:                                                 
            # month number has changed                                          
            month_delta -= delta                                                
            month = date.month                                                  
    return date

It's basically adding or removing one day and checks if the month number changes. If that is the case it reduces the number of months by one until no delta is left. Then it checks if the day number is equal to the original day number and stops if that is the case.

You could speed that up if you rely on the fact a month has at least N days and a year has always M months:

min_day_per_month = 28                                                          
months_per_year = 12                                                            
def month_delta2(date, month_delta=1):                                          
    if int(month_delta) != month_delta:                                         
        raise ValueError('Month delta must be an integer')                      
    delta = -1 if month_delta < 0 else +1                                       
    new_date = date + datetime.timedelta(days=min_day_per_month * month_delta)  
    # figure out how many months have been skipped                              
    month_delta -= (new_date.year - date.year) * months_per_year - date.month + new_date.month
    month = new_date.month                                                      
    while new_date.day != date.day or month_delta != 0:                         
        new_date += datetime.timedelta(days=delta)                              
        if new_date.month != month:                                             
            # month number has changed                                          
            month_delta -= delta                                                
            month = new_date.month                                              
        import time; time.sleep(2)                                              
    return new_date

This already skips "month_delta * N" days, so you only need to add a few (<= |4*month_delta|).

Change 176349 had a related patch set uploaded (by XZise):
[FEAT] Date: Add/subtract months from a date

https://gerrit.wikimedia.org/r/176349

Patch-For-Review

There is also the very useful https://labix.org/python-dateutil

If we want our own implementation, the core problem is getting the number of days in the month, and there are a few ways to achieve that in python

http://stackoverflow.com/questions/42950/get-last-day-of-the-month-in-python

Once that algorithm is in core, the ability to do month operations is simple.

Hmm, https://docs.python.org/2/library/calendar.html#calendar.monthrange should do the trick then.

import datetime                                                                 
import calendar                                                                 
                                                                                
def month_delta(date, month_delta=1):                                           
    if int(month_delta) != month_delta:                                         
        raise ValueError('Month delta must be an integer')                      
    while month_delta > 0:                                                      
        date += datetime.timedelta(days=calendar.monthrange(
            date.year, date.month)[1])
        month_delta -= 1                                                        
    return date

Though this just supports non-negative deltas but without any external library or pywikibot specific code.

script_wui currently uses https://de.wikipedia.org/wiki/Benutzer:DrTrigon/DrTrigonBot/script_wui-crontab.css as a list of operations to perform on a schedule. It uses crontab-parse to parse the entry and includes methods _month_incr and _year_incr.

@Xqt, why do you need it to calculate log entries; In which script do you intend to use this, and how would it be used?

@jayvdb I have a script which removes signed entries on voting pages after a delay of "6 months". This may be 181-184 days.

Another needed feature is to calculate months from a Timestamp difference, which is the inverse of the above. This should be the same as mw does when displaying blocking durations.

Okay I've got a question: What would be the 31st March + 1 month? Would it be the last day in April or the 1 day in May? Or like PHP which selects the day in the month after that (so 31 May).

This doesn't affect the calculation of the difference however. That would be probably easier than the calculation as you could simply add the month difference and years difference multiplied by 12. So something like:

delta = (date2.month - date1.month) % 12 + (date2.year - date1.year) * 12
return -delta if date2 < date1 else delta

I personally need the first variant 31.03. + 1 month => 30.4. but it could be a good idea to have both possibilities for rounding up and down.

Well my newest version of the patch does by default what you need, but it also could add the missing months if you want it.

Change 176349 merged by jenkins-bot:
[FEAT] Date: Add/subtract months from a date

https://gerrit.wikimedia.org/r/176349

jayvdb claimed this task.

The code exists. New tasks can be created for integrating this. e.g. using it for Pywikibot-Wikidata newitem.py