Human-readable duration between two UNIX timestamps

佐手、 提交于 2019-12-11 02:25:42

问题


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!


回答1:


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.

typedef struct {
  int year;
  int month;
  int dayOfMonth;
} date_struct;

static int days_in_month[2][13] = {
  {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
  {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
};

int isLeapYear(int year) {
  int value;
  value = year;

  //if year modulo 400 is 0
  //   then is_leap_year
  value = year % 400;
  if(value == 0) return 1;

  //else if year modulo 100 is 0
  //   then not_leap_year
  value = year % 100;
  if(value == 0) return 0;

  //else if year modulo 4 is 0
  //   then is_leap_year
  value = year % 4;
  if(value == 0) return 1;

  //else
  //   not_leap_year
  return 0;
}

date_struct addOneDay(date_struct ds, int isLeapYear){
  int daysInMonth;

  ds.dayOfMonth++;

  //If the month is February test for leap year and adjust daysInMonth
  if(ds.month == 2) {
    daysInMonth = days_in_month[isLeapYear][ds.month];
  } else {
    daysInMonth = days_in_month[0][ds.month];
  }

  if(ds.dayOfMonth > daysInMonth) {
    ds.month++;
    ds.dayOfMonth = 1;
    if(ds.month > 12) {
      ds.year += 1;
      ds.month = 1;
    }
  }
  return ds;
}

long daysBetween(date_struct date1, date_struct date2){
  long result = 0l;
  date_struct minDate = min(date1, date2);
  date_struct maxDate = max(date1, date2);

  date_struct countingDate;
  countingDate.year = minDate.year;
  countingDate.month = minDate.month;
  countingDate.dayOfMonth = minDate.dayOfMonth;

  int leapYear = isLeapYear(countingDate.year);
  int countingYear = countingDate.year;

  while(isLeftDateSmaller(countingDate,maxDate)) {
    countingDate = addOneDay(countingDate,leapYear);
    //if the year changes while counting, check to see if
    //it is a new year
    if(countingYear != countingDate.year) {
      countingYear = countingDate.year;
      leapYear = isLeapYear(countingDate.year);
    }

    result++;
  }

  return result;
}

(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/ )




回答2:


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:

#include "date.h"
#include <iostream>

int
main()
{
    using namespace date;
    using namespace std::chrono;

And it also helps to have some example UNIX timestamps to work with.

auto t0 = sys_days{1970_y/7/28} + 8h + 0min + 0s;
auto t1 = sys_days{2016_y/4/2} + 2h + 34min + 43s;
std::cout << t0.time_since_epoch().count() << '\n';
std::cout << t1.time_since_epoch().count() << '\n';

This will print out:

18000000
1459564483

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:

auto dp0 = floor<days>(t0);
auto dp1 = floor<days>(t1);

dp0 and dp1 have type std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int, std::ratio<86400>>>, which is a whopping mouth-full! Isn't auto nice! There is a typedef called date:: sys_days that is a handy shortcut for the type of dp0 and dp1, so you don't ever have to type out the ugly form.

Next it is handy to convert dp0 and dp1 to {year, month, day} structures. There is such a structure with the type date::year_month_day which will implicitly convert from sys_days:

year_month_day ymd0 = dp0;
year_month_day ymd1 = dp1;

This is a very simple structure with year(), month() and day() 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-resolution time_point:

auto time0 = t0 - dp0;
auto time1 = t1 - dp1;

time0 and time1 have type std::chrono::seconds and represent the seconds past the start of the day for t0 and t1.

To verify where we are, it is handy to output what we have so far:

std::cout << ymd0 << ' ' << make_time(time0) << '\n';
std::cout << ymd1 << ' ' << make_time(time1) << '\n';

which outputs:

1970-07-28 08:00:00
2016-04-02 02:34:43

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 takes seconds 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:

auto dy = ymd1.year() - ymd0.year();
ymd0 += dy;
dp0 = ymd0;
t0 = dp0 + time0;
if (t0 > t1)
{
    --dy;
    ymd0 -= years{1};
    dp0 = ymd0;
    t0 = dp0 + time0;
}
std::cout << dy.count() << " years\n";
std::cout << ymd0 << ' ' << make_time(time0) << '\n';
std::cout << ymd1 << ' ' << make_time(time1) << '\n';

This outputs:

45 years
2015-07-28 08:00:00
2016-04-02 02:34:43

First we take the difference between the year fields of ymd1 and ymd0 and store this in the variable dy which has type date::years. Then we add dy back to ymd0 and recompute the serial time_points dp0 and t0. If it turns out that t0 > t1 then we have added one too many years (because the month/day of ymd0 occurs later in the year than that of ymd1). 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:

auto dm =  ymd1.year()/ymd1.month() - ymd0.year()/ymd0.month();
ymd0 += dm;
dp0 = ymd0;
t0 = dp0 + time0;
if (t0 > t1)
{
    --dm;
    ymd0 -= months{1};
    dp0 = ymd0;
    t0 = dp0 + time0;
}
std::cout << dm.count() << " months\n";
std::cout << ymd0 << ' ' << make_time(time0) << '\n';
std::cout << ymd1 << ' ' << make_time(time1) << '\n';

The first line of this example deserves extra attention because a lot is going on here. We need to find the difference between ymd1 and ymd0 in units of months. Just subtracting ymd0.month() from ymd1.month() only works if ymd1.month() >= ymd0.month(). But ymd1.year()/ymd1.month() creates a date::year_month type. These types are "time points", but with a precision of a month. One can subtract these types and get months as a result.

Now the same formula is followed: Add the difference of months back to ymd0, recompute dp0 and t0, and discover if you've added one too many months. If so, add one less month. The above code outputs:

8 months
2016-03-28 08:00:00
2016-04-02 02:34:43

Now we're down to finding the difference of {days, hours, minutes, seconds} between two dates.

auto dd = dp1 - dp0;
dp0 += dd;
ymd0 = dp0;
t0 = dp0 + time0;
if (t0 > t1)
{
    --dd;
    dp0 -= days{1};
    ymd0 = dp0;
    t0 = dp0 + time0;
}
std::cout << dd.count() << " days\n";
std::cout << ymd0 << ' ' << make_time(time0) << '\n';
std::cout << ymd1 << ' ' << make_time(time1) << '\n';

Now the interesting thing about sys_dayss is that they are really good at day-oriented arithmetic. So instead of dealing with field types like year_month_day or year_month, we work with sys_days at this level. We just subtract dp1 - dp0 to get the difference in days. Then we add that to dp0, and recreate ymd0 and t0. Check if t0 > t1 and if so, we've added one too many days so we back off by one day. This code outputs:

4 days
2016-04-01 08:00:00
2016-04-02 02:34:43

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.

auto delta_time = time1 - time0;
if (time0 > time1)
    delta_time += days{1};
auto dt = make_time(delta_time);
std::cout << dt.hours().count() << " hours\n";
std::cout << dt.minutes().count() << " minutes\n";
std::cout << dt.seconds().count() << " seconds\n";
t0 += delta_time;
dp0 = floor<days>(t0);
ymd0 = dp0;
time0 = t0 - dp0;
std::cout << ymd0 << ' ' << make_time(time0) << '\n';
std::cout << ymd1 << ' ' << make_time(time1) << '\n';

We can just subtract time0 from time1. Both time1 and time0 have type std::chrono::seconds and their difference has the same type. If it turns out that time0 > time1 (as in this example), we need to add a day. Then we can add the difference back and recompute time0, dp0 and ymd0 to check out our work. We should get the same timestamp as t1 now. This code outputs:

18 hours
34 minutes
43 seconds
2016-04-02 02:34:43
2016-04-02 02:34:43

This is all a very long-winded explanation for this code:

#include "date.h"
#include <iostream>

struct ymdhms
{
    date::years          y;
    date::months         m;
    date::days           d;
    std::chrono::hours   h;
    std::chrono::minutes min;
    std::chrono::seconds s;
};

std::ostream&
operator<<(std::ostream& os, const ymdhms& x)
{
    os << x.y.count()   << " years "
       << x.m.count()   << " months "
       << x.d.count()   << " days "
       << x.h.count()   << " hours "
       << x.min.count() << " minutes "
       << x.s.count()   << " seconds";
    return os;
}

using second_point =
    std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;

ymdhms
human_readable_difference(second_point t1, second_point t0)
{
    using namespace date;
    auto dp0 = floor<days>(t0);
    auto dp1 = floor<days>(t1);
    year_month_day ymd0 = dp0;
    year_month_day ymd1 = dp1;
    auto time0 = t0 - dp0;
    auto time1 = t1 - dp1;
    auto dy = ymd1.year() - ymd0.year();
    ymd0 += dy;
    dp0 = ymd0;
    t0 = dp0 + time0;
    if (t0 > t1)
    {
        --dy;
        ymd0 -= years{1};
        dp0 = ymd0;
        t0 = dp0 + time0;
    }
    auto dm =  ymd1.year()/ymd1.month() - ymd0.year()/ymd0.month();
    ymd0 += dm;
    dp0 = ymd0;
    t0 = dp0 + time0;
    if (t0 > t1)
    {
        --dm;
        ymd0 -= months{1};
        dp0 = ymd0;
        t0 = dp0 + time0;
    }
    auto dd = dp1 - dp0;
    dp0 += dd;
    t0 = dp0 + time0;
    if (t0 > t1)
    {
        --dd;
        dp0 -= days{1};
        t0 = dp0 + time0;
    }
    auto delta_time = time1 - time0;
    if (time0 > time1)
        delta_time += days{1};
    auto dt = make_time(delta_time);
    return {dy, dm, dd, dt.hours(), dt.minutes(), dt.seconds()};
}

Which can be exercised like this:

int
main()
{
    std::cout << human_readable_difference(second_point{1459564483s},
                                           second_point{18000000s}) << '\n';
}

and outputs:

45 years 8 months 4 days 18 hours 34 minutes 43 seconds

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:

std::cout << human_readable_difference(sys_days{feb/11/2000} + 9h + 21min + 6s,
                                       sys_days{jul/12/1900} + 15h + 24min + 7s) << '\n';

Outputs:

99 years 6 months 29 days 17 hours 56 minutes 59 seconds

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.




回答3:


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:

7/12/1900 to 2/11/2000

Outputs:

99 years 6 months 30 days

Times of day also:

7/12/1900 3:24:07pm  to 2/11/2000 9:21:06am

Outputs:

99 years 6 months 29 days 17 hours 56 minutes 59 seconds

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:

7/12/1900 3:24:07pm in Boston to 2/11/2000 9:21:06am in Hong Kong

The output is different from the previous answer by 13 hours:

99 years 6 months 29 days 4 hours 56 minutes 59 seconds



来源:https://stackoverflow.com/questions/34916758/human-readable-duration-between-two-unix-timestamps

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!