I'm trying to calculate the difference between two UNIX timestamps (i.e. seconds since 1970). I want to say e.g. "3 years, 4 months, 6 days, etc" but I don't know how to account for leap years and months of different durations. Surely this is a solved problem..?
This is different to other questions which want to express an approximate duration in one unit that has a fixed/homogeneous duration (e.g. 3 hours, or 7 weeks, etc.). Results for 1. Jan to 1. Feb would be "1 month" and results for 1. Feb to 1. Mar would also be "1 month" even though the number of days are different.
I want to express the complete duration precisely but in years/months/days/hours/minutes. Solutions in C++ would be appreciated!
For the older date, use the Leap Year formula to start counting the number of days from Unix start date, Jan1st 1970, to your first timestamp
(For leap seconds, dunno if you need to get that precise, hopefully will be out-of-scope?)
Leap year calculated by constraining date to after 1600AD and the algorithm for the Gregorian Calendar from: http://en.wikipedia.org/wiki/Leap_year of
if year modulo 400 is 0 then is_leap_year else if year modulo 100 is 0 then not_leap_year else if year modulo 4 is 0 then is_leap_year else not_leap_year
If a year is a Leap Year, then there are 29days in Feb, else 28days
Now you know the month, day_of_month, year for the 1st variable
Next, another set of counts of days to the 2nd timestamp, using the Leap Year formula till you get to the 2nd timestamp.
(I wrote an open source program that gives the difference between two calendar dates, in C/C++. Its source code, some that I posed above, might help give you inspiration for your own solution, or maybe you can adapt some of it, too http://mrflash818.geophile.net/software/timediff/ )
Using this free, open-source C++11/14 date/time library, this problem can be solved very simply, with very high-level syntax. It leverages the C++11
<chrono>
library.As relatively few people are familiar with how my date library works, I will go through how to do this in painstaking detail, piece by piece. Then at the end I put it all together packaged up in a neat function.
To demonstrate it, I will assume some helpful using declarations to cut down on the verbosity:
And it also helps to have some example UNIX timestamps to work with.
This will print out:
This indicates that 1970-07-28 08:00:00 UTC is 18000000 seconds after the epoch and 2016-04-02 02:34:43 UTC is 1459564483 seconds after the epoch. This all neglects leap seconds. That is consistent with the way UNIX timestamps work, and consistent with the operation of
std::chrono::system_clock::now()
.Next it is convenient to "coarsen" these timestamps with seconds precision down to timestamps with a precision of days (number of days since the epoch). This is done with this code:
dp0
anddp1
have typestd::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int, std::ratio<86400>>>
, which is a whopping mouth-full! Isn'tauto
nice! There is atypedef
calleddate:: sys_days
that is a handy shortcut for the type ofdp0
anddp1
, so you don't ever have to type out the ugly form.Next it is handy to convert
dp0
anddp1
to{year, month, day}
structures. There is such a structure with the typedate::year_month_day
which will implicitly convert fromsys_days
:This is a very simple structure with
year()
,month()
andday()
getters.It is also handy to have the time since midnight for each of these UNIX timestamps. That is easily obtained by subtracting the days-resolution
time_point
from seconds-resolutiontime_point
:time0
andtime1
have typestd::chrono::seconds
and represent the seconds past the start of the day fort0
andt1
.To verify where we are, it is handy to output what we have so far:
which outputs:
So far so good. We have two UNIX timestamps divided up into their human readable components (at least year, month and day). The function
make_time
shown in the print statement above takesseconds
and converts it into a{hours, minutes, seconds}
structure.Ok, but so far all we've done is take UNIX timestamps and convert them to field types. Now for the difference part...
To take the human-readable difference we start with the big units and subtract them. And then if the next smallest units are not subtractible (if the subtraction would produce a negative result), then the bigger-unit subtraction is too big by one. Stay with me, code+example is clearer than human language:
This outputs:
First we take the difference between the
year
fields ofymd1
andymd0
and store this in the variabledy
which has typedate::years
. Then we adddy
back toymd0
and recompute the serialtime_point
sdp0
andt0
. If it turns out thatt0 > t1
then we have added one too many years (because the month/day ofymd0
occurs later in the year than that ofymd1
). So we subtract a year and recompute.Now we have the difference in years, and we have reduced the problem to finding the difference in
{months, days, hours, minutes, seconds}
and this delta is guaranteed to be less than 1 year.This is the basic formula for the whole problem! Now we just need to rinse and repeat with the smaller units:
The first line of this example deserves extra attention because a lot is going on here. We need to find the difference between
ymd1
andymd0
in units of months. Just subtractingymd0.month()
fromymd1.month()
only works ifymd1.month() >= ymd0.month()
. Butymd1.year()/ymd1.month()
creates adate::year_month
type. These types are "time points", but with a precision of a month. One can subtract these types and getmonths
as a result.Now the same formula is followed: Add the difference of months back to
ymd0
, recomputedp0
andt0
, and discover if you've added one too many months. If so, add one less month. The above code outputs:Now we're down to finding the difference of
{days, hours, minutes, seconds}
between two dates.Now the interesting thing about
sys_days
s is that they are really good at day-oriented arithmetic. So instead of dealing with field types likeyear_month_day
oryear_month
, we work withsys_days
at this level. We just subtractdp1 - dp0
to get the difference indays
. Then we add that todp0
, and recreateymd0
andt0
. Check ift0 > t1
and if so, we've added one too manydays
so we back off by one day. This code outputs:Now we are just down to finding the difference between two time stamps in terms of
{hours, minutes, seconds}
. This is really simple, and where<chrono>
shines.We can just subtract
time0
fromtime1
. Bothtime1
andtime0
have typestd::chrono::seconds
and their difference has the same type. If it turns out thattime0 > time1
(as in this example), we need to add aday
. Then we can add the difference back and recomputetime0
,dp0
andymd0
to check out our work. We should get the same timestamp ast1
now. This code outputs:This is all a very long-winded explanation for this code:
Which can be exercised like this:
and outputs:
The algorithms behind all this are all public domain and neatly collected and explained here:
http://howardhinnant.github.io/date_algorithms.html
I would like to emphasize that this question is a good but complicated question because of the non-uniformity of units such as years and months. The most effective way to deal with a question like this is to have low-level tools capable of abstracting away the complicated low-level arithmetic with higher-level syntax.
As an added verification, this:
Outputs:
which is identical to the output reported in another answer that reports what Wolfram Alpha outputs. As a bonus, the syntax here does not -- and can not -- suffer from endian ambiguity (m/d/y vs d/m/y). Admittedly this involved a little luck in that Wolfram's output is reported using the "America/New_York" timezone, and for these two timestamps the UTC offset is the same (so the timezone offset does not factor in).
If timezone actually does matter, an additional software layer on top of this is required.
Re: "surely this is a solved problem?"
This problem appears to be be solved, if Wolfram Alpha is correct. It seems WA doesn't publicly provide their method, and their site deliberately makes screen-scraping difficult, but it has a method, which is available as an online "black box".
To see that black box in action, go to Wolfram Alpha, and eEnter two dates, separated by a "to", for example:
Outputs:
Times of day also:
Outputs:
Note that Wolfram assumes a default EST timezone. Locations too, can be input, here's the same numeric range the latter example but in different locations and timezones:
The output is different from the previous answer by 13 hours: