我很难从一年中的某一天和自午夜以来的秒数获取日期/时间组件

Posted

技术标签:

【中文标题】我很难从一年中的某一天和自午夜以来的秒数获取日期/时间组件【英文标题】:I'm having a hard time getting date/time components from day of year and seconds since midnight 【发布时间】:2016-04-21 08:09:51 【问题描述】:

在我的数据流中,我从午夜开始有秒数,从 1 月 1 日开始有几天...我将手动指定年份,因此我需要能够将这三个值转换为正确的日期/时间以输出到另一个程序。这是我的代码:

int currentDay = XPLMGetDatai(gLocalDate); // days since Jan 1st
float currentTime = XPLMGetDataf(gZuluTime); // seconds since midnight
int currentYear = 2015;

struct tm t;
struct tm * ct;
time_t t_of_day;

t.tm_year = currentYear - 1900;
t.tm_yday = currentDay;
t.tm_hour = (int)(currentTime / 3600);
t.tm_min = (int)(currentTime - (t.tm_hour * 3600)) / 60;
t.tm_sec = (int)currentTime - ((t.tm_hour * 3600) + (t.tm_min * 60));

t_of_day = mktime(&t); // should convert t into a valid time_t
ct = gmtime(&t_of_day); // should give me a valid UTC from t

// Send data to Celestial Control
CCelC.SetDay(ct->tm_mday);
CCelC.SetHour(ct->tm_hour);
CCelC.SetMinute(ct->tm_min);
CCelC.SetMonth(ct->tm_mon);
CCelC.SetYear(currentYear);

我似乎遇到的问题是,当调用 mktime(&t) 时,插入 tm_yday 的 currentDay 会被删除。所以我最终得到 ct->tm_mon 为 0,这在我当前 90 日(4 月 1 日)的测试运行中是不正确的。

因此,给定任何年份、自午夜以来的任何秒数以及自 1 月 1 日以来的任何日子,我如何生成正确的日期(1-31)、小时(0-23)、分钟(0-59)、星期一(1- 12)、年份值?

【问题讨论】:

为什么不使用<chrono> 你想要当年的Jan 1还是epoch time? 我不相信 <chrono> 对日期、闰年、月份、一个月中的天数等一无所知。我建议您查看 Boost 的 datetime 库。 您不能使用mktime 来执行此操作。来自these docs“timeptr 的成员 tm_wday 和 tm_yday 的值被忽略......如果 timeptr 的成员超出范围或 - 在 tm_wday 和tm_yday- 如果它们的值与其他成员描述的日期不匹配。" 有一种方法——我现在把它写下来作为答案。 【参考方案1】:

这是使用<chrono> 的参数。这种说法并非没有缺点。不过我相信,从长远来看,迁移到这个系统将在类型安全(正确性)、性能和可读性方面受益。

缺点包括需要第 3 方开源免费仅标头(一个标头)库(目前):

https://github.com/HowardHinnant/date/blob/master/date.h

这也需要 C++11 或更高版本(自然是因为它建立在 <chrono> 之上)。

我将分阶段介绍这个解决方案:

    第一阶段只使用<chrono>进行转换,在类型安全方面收获甚微。这个阶段的输入和输出是一体的。

    当输入阶段为自己的接口采用<chrono> 类型时,第二阶段开始显示优势。

    当输入和输出都采用<chrono>时,第三阶段显示出显着的优势。

基础设施

假设struct CCelC 像这样:

#include <iomanip>
#include <iostream>

struct CCelC

    int year_;
    unsigned month_;
    unsigned day_;
    int hour_;
    int min_;
    int sec_;

    friend
    std::ostream&
    operator<<(std::ostream& os, const CCelC& x)
    
        using namespace std;
        auto f = os.fill();
        os.fill('0');
        os << setw(4) << x.year_ << '-'
           << setw(2) << x.month_ << '-'
           << setw(2) << x.day_  << ' '
           << setw(2) << x.hour_ << ':'
           << setw(2) << x.min_ << ':'
           << setw(2) << x.sec_;
        os.fill(f);
        return os;
    
;

还有一个像这样的测试驱动程序:

int
main()

    auto t = convert(90, 12*3600 + 52*60 + 31, 2015);
    std::cout << t << '\n';

第一阶段

第一阶段构建一个CCelC convert(int currentDay, float s, int y) 转换函数,它接受标量输入并输出一个CCelC,它本身接受标量输入。 &lt;chrono&gt; 在这里的唯一用途是输入标量、进行日期计算和输出标量:

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

CCelC
convert(int currentDay, float s, int y)

    using namespace date;
    using namespace std::chrono;
    auto tp = sys_daysyeary/jan/1 + dayscurrentDay + secondsstatic_cast<int>(s);
    auto dp = floor<days>(tp);
    auto time = make_time(tp - dp);
    auto ymd = year_month_daydp;
    return intymd.year(), unsignedymd.month(), unsignedymd.day(),
            int(time.hours().count()), int(time.minutes().count()),
            int(time.seconds().count());

需要这个辅助(free, open-source one-header) library 以方便日期计算。它只是将输入的年/日/秒字段类型转换为std::chrono::time_point,然后将std::chrono::time_point 转换回年/月/日时:分:秒标量。

这个解决方案大致相当于当前接受的(和好的)答案。两种解决方案都不需要用户方面的日历算术。此解决方案的此驱动程序输出:

2015-04-01 12:52:31

第 2 阶段

假设convert 的输入代码决定转换为&lt;chrono&gt;。这具有显着的类型安全优势。编译器现在可以帮助您正确转换单位,并防止您将 minutes 与其他与时间单位无关的整数类型混淆。这将有效地将潜在的运行时错误转化为编译时错误(在编译时捕获错误总是更好)。

convert 函数现在被指定为采用计时类型:

CCelC
convert(date::days currentDay, std::chrono::duration<float> s, date::year y)

date::days 只是24 std::chrono::hours 的类型别名。 date::year 是一种新类型,但有助于消除 2015 与某个任意整数的歧义。现在2015_y 具有year 类型,编译器会为您传播该信息。

我们的驱动程序现在可以变得更具可读性(假设 C++14 用于计时持续时间文字):

int
main()

    using namespace date;
    using namespace std::chrono_literals;
    auto t = convert(days90, 12h + 52min + 31s, 2015_y);
    std::cout << t << '\n';

使用这个新 API 对 convert 的实现略有简化:

CCelC
convert(date::days currentDay, std::chrono::duration<float> s, date::year y)

    using namespace date;
    using namespace std::chrono;
    auto tp = sys_daysy/jan/1 + currentDay + duration_cast<seconds>(s);
    auto dp = floor<days>(tp);
    auto time = make_time(tp - dp);
    auto ymd = year_month_daydp;
    return intymd.year(), unsignedymd.month(), unsignedymd.day(),
            int(time.hours().count()), int(time.minutes().count()),
            int(time.seconds().count());

不再需要将标量输入转换为&lt;chrono&gt; 库的类型安全单元。 convert 的大部分工作仍然是采用 CCelC 的标量格式需求。

第 3 阶段

但是如果CCelC 采用&lt;chrono&gt; 呢?从逻辑上讲,如果它这样做了,它应该存储一个 std::chrono::time_point 而不是所有这些字段。它更节省空间,并且在必要时很容易(使用date.h)转换为字段类型。这可能看起来像:

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

struct CCelC

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

    friend
    std::ostream&
    operator<<(std::ostream& os, const CCelC& x)
    
        using namespace date;
        return os << x.tp_;
    
;

这里的功能根本没有改变。这个程序的输出仍然是2015-04-01 12:52:31。并且 sizeof 要求刚刚急剧下降。而涉及秒、分、小时和天的算术性能也刚刚飙升。

convert 函数也刚刚获得了性能和简化。它的输入根本没有改变,所以驱动程序还是一样的。但是现在convert 不需要再转回标量类型了:

CCelC
convert(date::days currentDay, std::chrono::duration<float> s, date::year y)

    using namespace date;
    using namespace std::chrono;
    return sys_daysy/jan/1 + currentDay + duration_cast<seconds>(s);

现在的代码已经大大简化,逻辑错误的机会大大减少了。这种简化包括类型安全,以便编译器帮助您捕获逻辑错误。代码中不再有单位转换,消除了另一类错误。如果你在这段代码周围加上计时器,你会发现它运行快速

Cppcon 2015 video presentation of date.h

也许&lt;chrono&gt; 不是您今天可以完全采用的东西。但是在小阶段,在代码的一小部分中采用它是有好处的。 date.h 可以提供帮助。在 2 或 3 年后,&lt;chrono&gt; 是您想要的目标。最终,这将是 C++ 社区普遍采用的。 &lt;ctime&gt;/&lt;time.h&gt; 死了。 &lt;chrono&gt; 的类型安全和性能优势太大了。此答案描述了如何逐步加入&lt;chrono&gt;,一次一小段代码。

【讨论】:

【参考方案2】:

您不能使用mktime 以您想要的方式执行此操作。来自these docs

timeptr的成员tm_wday和tm_yday的值被忽略

.....

调用此函数会自动调整 timeptr 成员的值(如果它们超出范围或 - 在 tm_wdaytm_yday 的情况下 - 如果它们的值与描述的日期不匹配)其他成员。”

但是,您可以利用此行为正确填充其他字段。如果您改为像这样设置struct tm 的字段

struct tm t = 0;

t.tm_mday = currentDay + 1;
t.tm_year = currentYear - 1900;

t.tm_sec = currentTime;

mktime(&t);

此方法之所以有效,是因为mktime 的自动调整行为

作为一个完整的工作示例

#include <ctime>
#include <iostream>

int main()

    int currentDay = 90;
    int currentTime = (12*3600 + 52*60 + 31);
    int currentYear = 2015;

    struct tm t = 0;

    t.tm_mday = currentDay + 1;
    t.tm_year = currentYear - 1900;

    t.tm_sec = currentTime;

    // mktime will now correctly populate
    // tm_sec, tm_min, tm_hour
    // tm_day, tm_mon, tm_year
    mktime(&t);

    // Print the time to make sure!
    std::cout << asctime(&t);

会输出

Wed Apr  1 12:52:31 2015

【讨论】:

这里唯一的问题是 currentDay 是从一月开始的天数......不是一个月中的一天。所以我相信这行不通。因此,就像在非闰年一样,当前 90 日将是 4 月 1 日。 好的,感谢您的清理,在这种情况下,您可以将 mday 设置为 currentDay + 1 我已经用一个示例程序更新了我的答案,显示它可以正常工作。 main() 中缺少需要的tm_isdst。最好从struct tm t = 0 开始,因为可能存在需要初始化的其他字段“tm 结构应包含至少以下成员......” @chux 感谢现场!

以上是关于我很难从一年中的某一天和自午夜以来的秒数获取日期/时间组件的主要内容,如果未能解决你的问题,请参考以下文章

如何获得自 1970 年 1 月 1 日以来 Python 日期时间对象的秒数?

在比较 jwt.iat(发布于)属性与当前日期时间时遇到问题 - (“自纪元以来的秒数”NumericDate)

在linux bash中将ISO日期转换为纪元以来的秒数

如何从 gmtime() 的时间 + 日期输出中获取纪元以来的秒数?

powershell 获取自Epoch以来的秒数

Visual C++ 如何将日期时间转换为自 1970 年以来的秒数