Originally titled, a more snappy, "Half a Hacky Fix...", but whilst writing the post I was inspired to add another quarter of a fix

With the recent end of British Summer Time I discovered lots of my tasks in Taskwarrior had shifted an hour earlier, and since the default time for a task is 00:00:00 hours this meant they had all shifted to the previous day. This really screwed up my pseudo-Teuxdeux report implementation.

I found out this is a known issue for recurring tasks (I hadn't realised initially it was limited to recurring tasks).

I came up with a quick-ish fix for this:

#!/bin/sh
#Line below for testing:
#for task in 370; do
#Use task ids to find max number and enter that below
for task in `jot  374`; do
    #Get only the Due date line and not modification lines
    wrongdate=$(task $task info | grep ^Due.*23:00:00$ | awk '{print $2}');
    #Only modify if task due contains 23:00:00
    if ! [ -z "$wrongdate" ]; then 
        #echo $wrongdate
        correctdate=$(date -d "$wrongdate + 1 day" +%F)
        #echo $correctdate
        #echo $task
        #As per my cron routine I need to feed in a file containing only the letter "a" to modify all
        $(task $task rc.confirmation:no rc.bulk:100 modify due:$correctdate < ~/.taskcronin)
    fi
done

But then I realised this only fixed the already existing task instances created from the initial recurring parent and that any additional instances created would suffer the same (since Taskwarrior only creates a set number ahead; I use recurrence.limit=7). So it is necessary to fix the parents and shift them an hour forwards. A quick way to do this in taskwarrior 2.4.0 is:

task `task status:recurring ids` modify due:'due + 1hour'

Ideally though I should use a date range on this to target only recurring task parents created during Daylight Savings Time, rather than all of them. But I don't think it will really matter if this means some recurring tasks will alternate between 12am and 1am and the others between 1am and 2am, just so long as they don't shift over a day boundary.

But then I realised this will at some point screw up my reports as they are firstly sorted on due date. For my requirements I only want that to sort tasks to the right day, within each day I use a user defined attribute to sort, but now some of my tasks will have due dates differing by an hour or so.

So then I resigned myself to looking at the source code and figuring out a better fix.

In recurr.cpp there is a section of code that calculates the recurrence period, if it's a non-special one, as follows:

// Copyright 2006 - 2014, Paul Beckingham, Federico Hernandez.

// If the period is an 'easy' one, add it to current, and we're done.
// If it throws an error, the duration was not recognized.
int secs = 0;
std::string::size_type idx = 0;
Duration du;
if (du.parse (period, idx))
{
    secs = du;
}
else
{
    idx = 0;
    ISO8601p p;
    if (p.parse (period, idx))
    {
        secs = p;
    }
    else
        throw std::string (format (STRING_TASK_VALID_RECUR, period));
}
return current + secs;

I.e. it parses the period ("daily", "2days", "weekly", etc) and converts this into seconds and then adds that to the original datetime, called "current". So if a daylight saving change occurs in-between then it won't add the correct number of seconds; In the case I came across because the clocks had moved back an hour adding a day's worth of seconds to a task that occurred at midnight would take it only to 11pm.

I came up with the ingenious idea (ok, I didn't) to shift the current datetime to midday before adding the appropriate period (the shift to midday should make it well safe of any daylight saving adjustments - I don't think anywhere in the world is crazy enough to subject their inhabitants to daylight saving adjustments of over 12 hours) which gets us the correct day, and then read the actual hour, minute and second from the current time and use those those to set the time for the new date:

// Shift current to midday to be safe of any DST changes, before calculating future date
recurrence_date = (Date (m, d, y) + 43200) + secs;
// Then set back to correct hour
return Date (recurrence_date.month (), recurrence_date.day (), recurrence_date.year ()) + 3600 * current.hour () + 60 * current.minute() + current.second ();

There is a little bit more to it that this (but not much), such as I'm only doing it on periods of whole days that are also greater than or equal to a whole day, and also "fixing" the "weekdays" period similarly.

This "works for me" as tested on my daily and weekly recurring tasks with their default time of 00:00:00, but is only a three-quarters fix as there are weird edge cases I can't figure out:

task add due:20141023T040028Z recur:daily Testy

Adding this retrospectively will create the task on the 23rd of October at 05:28am, but it will switch to 04:28am after the daylight time saving change occurs on Sunday 26th October at 2am. Which is weird, as a task at midnight gets created at midnight and stays at midnight. Even one at 1-something AM works ok. I suspect it is something to do with being after 2am and that's when the clocks change, but exactly why it happens I don't know.

As such, I don't think this is a good enough fix to get included into Taskwarrior (especially because I have no idea how to write a portable test that would test changes over a DST change for wherever the test was being run), but if someone elses wants to patch their own, feel free.