This post is based on a discussion about Progress Bars of Life, where I was foolish enough to claim that printing a text string representing the difference between two times could not be that hard in C. It is not hard, but turned out not to be entirely trivial either.
The problem we will consider is; given two
tm structs, compute the difference in time between them, in such a way that we can easily format a string that gives a textual representation of it. We want years, months, days, hours, minutes, seconds.
The first idea you might get is to use
difftime() to get the difference in seconds between the two times, and then compute the quantities we want by simple arithmetic. So,
You often see something like this in timing code – it works great for showing elapsed time in seconds, minutes, even hours. Do you see any problems with this approach?
How many seconds are there in a month? That depends on which month of course. And even worse, it also depends on which year, due to leap years.
For instance, the difference between Jan 31st and Mar 1st is sometimes 29 days, sometimes 30 days, but always 1 month 1 day. The difference between Jul 2nd and Aug 1st is 30 days, but not a month.
So once the difference in time exceeds hours, we somehow need to use the additional information about where this period of time starts and ends, in order to get answers that make sense to humans. Luckily this information is already available in the
If we accept the convention that the time between the 1st of a month and the 1st of the following is a “month” regardless of how many days are between, then one approach is to use elementary school subtraction on the
tm structs. We subtract one field at a time, starting at the lowest, borrowing from the next one if required.
For instance, we get the difference in seconds by subtracting the
tm_sec fields. If the result is negative, we have to borrow a minute.
But how do we borrow a month? Since we already know the number of days in the end month (
end.tm_mday), and all months in between are full calendar months, we only need to figure out how many days are in the start month.
So, if we borrow a month, then the difference in the days of the month is the number of days left in the start month, plus the number of days in the end month, minus any borrow from the calculation of hours of the day. This is the same as the total number of days in the start month plus the negative difference we already computed.
This method gives the desired results on examples like the ones given above – months are what we perceive as months on the calendar.
There is one more detail we may have to consider – how many hours are there in a day? Usually 24, but due to daylight saving time (summer time) it might be 23 or 25.
As an example, consider the difference between 01:45 and 03:15, which is 1 hour 30 minutes in most cases. But the night DST starts, it will be only 30 minutes (from 01:45 STD to 03:15 DST). Even worse, the night DST ends, it will first be 2 hours 30 minutes (from 01:45 DST to 03:15 STD), then 1 hour 30 minutes (from 01:45 STD to 03:15 STD) as the clock runs through the extra hour.
Much like the relationship between months and days, I think it makes sense to interpret the difference between the same time on consecutive dates as a “day”, no matter if it takes 23, 24, or 25 actual hours. Once the difference goes below 1 day however, it is more logical to use the actual difference in clock time.
So we would like the difference between 12:00 STD the day before DST starts, and 12:30 DST the day DST starts to be 1 day 30 minutes even though it only takes 23 hours 30 minutes. And we want the difference between 12:30 DST the day before DST ends, and 12:00 STD the day DST ends to be 24 hours 30 minutes but not a day.
Handling of DST is implementation defined in C, so to keep things simple, we
mktime() handle this
for us. We just have to adjust the hours of the day in case the difference is
below one day.
In many cases you can safely ignore these details, and I am sure there are better ways to handle it – but I hope this has given some indication of how it may not be quite as simple as it appears. The code I wrote while playing around with this problem is available here.