两个 UNIX 时间戳之间的人类可读持续时间

Human-readable duration between two UNIX timestamps

我正在尝试计算两个 UNIX 时间戳之间的差异(即自 1970 年以来的秒数)。我想说例如“3 年、4 个月、6 天等”,但我不知道如何计算不同持续时间的闰年和月份。这肯定是一个已解决的问题..?

这与其他问题不同,其他问题想要在具有 fixed/homogeneous 持续时间(例如 3 小时或 7 周等)的一个单位中表达大致持续时间。 1 月 1 日到 2 月 1 日的结果将是“1 个月”,2 月 1 日到 3 月 1 日的结果也将是“1 个月”,即使天数不同。

我想精确地表达完整的持续时间,但在 years/months/days/hours/minutes 中。 C++ 中的解决方案将不胜感激!

回复:“确定这是一个已解决的问题吗?

如果 Wolfram Alpha 正确,此问题似乎已解决。 WA 似乎没有公开提供他们的方法,他们的网站故意让屏幕抓取变得困难,但它 一种方法,可以在线使用 "black box"。

要查看运行中的黑框,请转至 Wolfram Alpha,然后输入两个日期,以 "to" 分隔,例如:

7/12/1900 to 2/11/2000

输出:

99 years 6 months 30 days

一天中的时间还有:

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

输出:

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

请注意,Wolfram 假定默认的 EST 时区。也可以输入位置,这里是后一个示例的相同数字范围,但在不同的位置和时区:

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

输出与之前的答案相差 13 小时:

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

对于较旧的日期,使用闰年公式开始计算从 Unix 开始日期(1970 年 1 月 1 日)到您的第一个时间戳的天数

(对于闰秒,不知道您是否需要如此精确,希望会超出范围?)

通过将日期限制在公元 1600 年之后计算的闰年和 公历的算法来自: http://en.wikipedia.org/wiki/Leap_year

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

如果年份是闰年,那么2月有29天,否则28天

现在您知道第一个变量的月份,day_of_month,年份

接下来,使用闰年公式计算第二个时间戳的另一组天数。

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;
}

(我在 C/C++ 中写了一个开源程序,它给出了两个日历日期之间的差异。它的源代码,我在上面提出的一些,可能会帮助你启发自己的解决方案,或者你也可以改编其中的一些 http://mrflash818.geophile.net/software/timediff/ )

使用这个free, open-source C++11/14 date/time library,这个问题可以用非常高级的语法非常简单地解决。它利用了 C++11 <chrono> 库。

由于相对较少的人熟悉我的 date library 是如何工作的,我将逐一详细介绍如何做到这一点。然后最后我把它们打包成一个简洁的函数。

为了演示它,我将假设一些有用的使用声明来减少冗长:

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

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

还有一些示例 UNIX timestamps 可以使用。

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';

这将打印出:

18000000
1459564483

这表示 1970-07-28 08:00:00 UTC 是纪元后 18000000 秒,2016-04-02 02:34:43 UTC 是纪元后 1459564483 秒。这都忽略了闰秒。即与UNIX timestamps的工作方式一致,也与std::chrono::system_clock::now().

的操作一致

接下来,方便的是 "coarsen" 这些精度为秒的时间戳到精度为天的时间戳(自纪元以来的天数)。这是通过以下代码完成的:

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

dp0dp1 的类型是 std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int, std::ratio<86400>>>,真是满口!是不是auto不错!有一个名为 date:: sys_daystypedef,它是 dp0dp1 类型的便捷快捷方式,因此您永远不必输入丑陋的形式。

接下来可以方便地将dp0dp1转换为{year, month, day}结构。有这样一个类型为 date::year_month_day 的结构,它将从 sys_days:

隐式转换
year_month_day ymd0 = dp0;
year_month_day ymd1 = dp1;

这是一个非常简单的结构,包含 year()month()day() 吸气剂。

对于这些UNIX timestamps,从午夜开始的时间也很方便。这很容易通过从秒分辨率 time_point:

中减去天分辨率 time_point 得到
auto time0 = t0 - dp0;
auto time1 = t1 - dp1;

time0time1 的类型为 std::chrono::seconds,代表 t0t1.[=119= 当天开始后的秒数]

为了验证我们在哪里,输出我们到目前为止的内容很方便:

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

输出:

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

到目前为止一切顺利。我们有两个 UNIX timestamps 分为人类可读的部分(至少年、月和日)。上面 print 语句中显示的函数 make_time 接受 seconds 并将其转换为 {hours, minutes, seconds} 结构。

好的,但到目前为止我们所做的只是采用 UNIX timestamps 并将它们转换为字段类型。现在是差异部分...

为了获得人类可读的差异,我们从大单位开始并减去它们。然后,如果下一个最小单位不可减去(如果减法会产生负结果),则更大单位的减法太大了。跟着我,代码+例子比人类语言更清晰:

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';

这输出:

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

首先,我们取 ymd1ymd0year 字段之间的差异,并将其存储在类型为 date::years 的变量 dy 中。然后我们将 dy 添加回 ymd0 并重新计算序列 time_points dp0t0。如果结果是 t0 > t1 那么我们加了太多年(因为 ymd0 的 month/day 出现在比 ymd1 更晚的年份。所以我们减去一年并重新计算。

现在我们有了年差,我们已经将问题简化为找到 {months, days, hours, minutes, seconds} 的差值,并且这个差值保证小于 1 年。

这就是整道题的基本公式!现在我们只需要冲洗并用较小的单元重复:

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';

这个例子的第一行值得特别注意,因为这里发生了很多事情。我们需要以月为单位找出 ymd1ymd0 之间的差异。从 ymd1.month() 中减去 ymd0.month() 只有在 ymd1.month() >= ymd0.month() 时才有效。但是 ymd1.year()/ymd1.month() 创建了一个 date::year_month 类型。这些类型是 "time points",但精度为一个月。可以减去这些类型并得到 months 作为结果。

现在遵循相同的公式:将月份差加回到 ymd0,重新计算 dp0t0,然后发现您添加的月份是否过多。如果是这样,请少加一个月。以上代码输出:

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

现在我们要找出两个日期之间 {days, hours, minutes, seconds} 的差异。

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';

现在关于 sys_dayss 的有趣之处在于他们非常擅长面向天的算术。因此,我们在此级别处理 sys_days,而不是处理 year_month_dayyear_month 等字段类型。我们只需减去 dp1 - dp0 即可得到 days 的差值。然后我们将其添加到 dp0,并重新创建 ymd0t0。检查 t0 > t1 是否存在,如果是,我们添加的 days 太多了,所以我们推迟了一天。此代码输出:

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

现在我们要找出两个时间戳之间 {hours, minutes, seconds} 的区别。这真的很简单,<chrono> 的亮点。

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';

我们可以从 time1 中减去 time0time1time0 都具有类型 std::chrono::seconds 并且它们的差异具有相同的类型。如果结果是time0 > time1(如本例),我们需要添加一个day。然后我们可以将差异加回去并重新计算 time0dp0ymd0 以检查我们的工作。我们现在应该得到与 t1 相同的时间戳。此代码输出:

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

这是对这段代码的冗长解释:

#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()};
}

可以这样练习:

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

并输出:

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

所有这些背后的算法都是 public 域,并在此处整齐地收集和解释:

http://howardhinnant.github.io/date_algorithms.html

我想强调一下,这道题是一道很好但复杂的题,因为年、月等单位不统一。处理此类问题的最有效方法是拥有能够使用高级语法抽象出复杂的低级算术的低级工具。

作为补充验证,这:

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

输出:

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

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 中报告的输出相同,使用 "America/New_York" 时区报告输出,并且对于这两个时间戳,UTC 偏移量相同(因此时区偏移量不影响).

如果时区确实很重要,则需要在此之上添加 additional software layer