DateTime处理算法

项目开发种需要将RTC模块中的年月日时分秒信息(DateTime),转换为从1970-01-01到当前的秒数信息(Seconds),查找了些处理算法,整理记录下。

参考算法链接:http://howardhinnant.github.io/date_algorithms.html

一、基本知识

  • 计算机计时通常使用秒数表示,从1970年1月1日00时00分00秒*(格林威治时间,与北京时间相差8小时)*开始到现在所经历的秒数。
  • 1年365天或366天,1天24小时,1小时60分,1分60秒,将时间信息转换为秒数较简单,复杂的是将日期信息转换为对应的天数。
  • 闰年:能够被4且不能被100整除的年份,或者能够被400整除的年份。
  • 考虑到闰年的存在,日期天数以400年为周期变化(称为时代,era),400年的天数为146097
  • 从公元(0000-01-01)开始到格林威治时间(1970-01-01)的天数为:719468
  • 1970年1月1日,周四,所以指定天数的周信息为:(day + 4) % 7
  • 根据国际标准ISO 8601规定:每个日历星期从周一开始,周日为第7天。我国采用的国家标准与国际标准相同。

二、算法分析

简单来说,算法的重点在于对闰年的处理,该算法实现有两点关键处理:

  • 根据闰年的定义,将400年作为一个周期,可以称为年代(era)。
  • 对月份进行映射转换,将3月1日作为每年的第一天。
erastart dateend date
-2-0800-03-01-0400-02-29
-1-0400-03-010000-02-29
00000-03-010400-02-29
10400-03-010800-02-29
20800-03-011200-02-29
31200-03-011600-02-29
41600-03-012000-02-29
52000-03-012400-02-29

首先,对于年代,根据闰年的定义(能够被4且不能被100整除的年份,或者能够被400整除的年份),可知400年为周期的天数是固定的。
一个年代400年的天数为:days = 400 * 365 + 400 / 4 - 400 / 100 + 400 / 400 = 146097
当前年份所经过的年代为:era = (y >= 0 ? y : y - 399) / 400

那么我们现在需要思考以下几个问题:

  • What is the year of the era (yoe)? This is always in the range [0, 399].
  • What is the day of the era (doe)? This is always in the range [0, 146096].
  • What is the day of the year(doy)? This is always in the range [0, 365/366]

对于yoe求取较为简单:yoe = y - era * 400
对应的doe的求取为:doe = yoe * 365 + yoe / 4 - yoe / 400 + yoe / 400 + doy

最后剩下一个问题:对doy的求取。
对于闰年,受影响的是2月:28天或29天,但对于我们求取doy来说,也会收到影响。但倘如每年的第一天是3月1日,那么2月29日将只会影响到当前年的总天数,对doy无影响。

mmpdoy
300
4131
5261
6392
74122
85153
96184
107214
118245
129275
110306
211337

通过对月份进行映射,可以看出每个月份对应的doy是固定的。为了便于程序实现,对mp和doy的关系进行公式化表达,实现可以根据月份直接获取doy信息。
mp与doy转换:doy = (153 * mp + 2)/ 5。(这里公式并没有搞懂可以怎么便捷的获取,但的确可以实现所需功能。)

但是这里的mp是映射后的月份,不是实际的月份,我们需要一个m到mp的转换公式:mp = m + m > 2 ? -3 : 9

综上可知:doy = (153 * (m + m > 2 ? -3 : 9) + 2) / 5 + d - 1
而所求取的天数为:z = era * 146097 + doe,可得z。

反之由z求取y、m、d类似。
首先当前天数经过的年代为:era = (z >= 0 ? z : z - 146096) / 146097
那么:doe = z - era * 146097
对于闰年相关的4年、100年、400年周期,对于的天数为1460,36524,146096。
(1460 = 365 * 4,36524 = 365 * 100 + (100 / 4) - (100/100)),146096 = 365 * 400 + (400 / 4) - (400 / 100) + (400 / 400) - 1)
则:
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
mp = (5 * doy + 2) / 153。(同样不清楚怎么便捷获取该公式。)
所以:
y = yoe + era * 400
m = mp + (mp < 10 ? 3 : -9)
d = doy - (153 * mp + 2) / 5 + 1

注1:0000-01-01到1970-01-01的天数可计算获得为719468。
注2:对于以上的z为从0000-01-01到当前的天数,计算1970-01-01到当前的天数注意处理719468。
注3:算法中每年的第一天为3月1日,当月份为1,2时,注意对年做处理。

下面我们理解下日期与天数转换算法实现。

1、将当前日期转换为天数
// 获取1970-01-01至当前日期的获取天数
void days_from_civil(int32_t y, uint8_t m, uint8_t d, int32_t *z)
{
    int32_t era, yoe;
    int32_t doe, doy;

    y -= m <= 2 ? 1 : 0;

    era = (y >= 0 ? y : y - 399) / 400;
    yoe = y - era * 400;
    doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
    doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;

    *z = era * 146097 + doe - 719468;
}
2、将当前天数转换为日期
// 根据1970-01-01至当前的天数获取日期
void civil_from_days(int32_t z, int32_t *y, uint8_t *m, uint8_t *d)
{
    int32_t era, yoe;
    int32_t doe, doy, mp;

    z += 719468;

    era = (z >= 0 ? z : z - 146096) / 146097;
    doe = z - era * 146097;
    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    mp = (5 * doy + 2) / 153;

    *y = yoe + era * 400;
    *m = mp + (mp < 10 ? 3 : -9);
    *d = doy - (153 * mp + 2) / 5 + 1;

    *y += *m <= 2 ? 1 : 0;
}

对于参考算法中提到的其他几种小算法,理解和实现较为简单了,代码实现分别如下。

3、判断是否为闰年
bool is_leap(int32_t year)
{
	return ((y % 4) == 0) && (((y % 100) != 0) || ((y % 400) == 0));
}
4、获取非闰年指定月的天数
uint8_t last_day_of_month_common_year(uint8_t m)
{
    uint8_t a[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    return a[m - 1];
}
5、获取闰年指定月的天数
uint8_t last_day_of_month_leap_year(uint8_t m)
{
    uint8_t a[] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    return a[m - 1];
}
6、根据当前年、月信息获取当天数
uint8_t last_day_of_month(int32_t y, uint8_t m)
{
    return (m != 2) || (!is_leap(y)) ? last_day_of_month_common_year(m) : 29u;
}
7、根据输入天数,获取当前周信息

对于1970-01-01,对应的是周四,对应获取周信息的算法也比较巧妙。

// z is number of days since 1970-01-01
uint8_t weekday_from_days(int32_t z)
{
	return z >= -4 ? ((z + 4) % 7) : (((z + 5) % 7 + 6));
}
8、获取周之间相差的天数
// The number of days from the weekday y to the weekday x.
uint8_t weekday_difference(uint8_t x, uint8_t y)
{
	int32_t temp;
	
	temp = x - y;

	return temp >= 0 ? temp : temp + 7;
}

注1:参考算法中,这个算法的代码实现感觉笔误敲错了,可以按参考算法中的单元测试方法验证下看。
注2:周一到周日,分别用数字1、2、3、4、5、6、0表示。
注3:根据国际标准ISO 8601,周一为每周的第一天,周日为最后一天。

9、获取指定周的下一天
uint8_t next_weekday(uint8_t wd)
{
	return wd < 6 ? wd + 1 : 0;
}
10、获取指定周的前一天
uint8_t prev_weekday(uint8_t wd)
{
	return wd > 0 ? wd - 1 : 6;
}

三、代码实现

DateTime数据结构定义。

static uint16_t yearH = 2000;

typedef struct
{
    uint8_t year;
    uint8_t month;
    uint8_t day;
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
    uint8_t week;
}DateTime;

注1:week使用0-6分别表示周日、周一、周二、周三、周四、周五、周六。
注2:BCD格式时,year能够表示的范围为0-99,yearH用于表示年份信息的高位信息。

由于从RTC模块中拿到的DateTime信息为BCD格式,而在计算机运行处理时使用HEX格式较为方便,因此通常我们也会对RTC获取的数据进行BCD与HEX格式进行转换。

bool RtcBcdToHex(DateTime *dateTime)
{
    uint8_t *hex = (uint8_t *)dateTime;
    uint8_t *bcd = (uint8_t *)dateTime;
    uint32_t i;

    for (i = 0; i < 7; i++)
    {
        if (((*bcd & 0x0f) > 0x09) ||
            ((*bcd & 0xf0) > 0x90))
        {
            return false;
        }

        *hex = ((*bcd & 0xf0) >> 4) * 10 + (*bcd & 0x0f);

        hex++;
        bcd++;
    }

    return true;
}

bool RtcHexToBcd(DateTime *dateTime)
{
    uint8_t *hex = (uint8_t *)dateTime;
    uint8_t *bcd = (uint8_t *)dateTime;
    uint32_t i;

    for (i = 0; i < 7; i++)
    {
        if (*hex > 99)
        {
            return 0;
        }

        *bcd = ((*hex / 10) << 4) + (*hex % 10);

        hex++;
        bcd++;
    }

    return true;
}

在使用DateTime前,我们需要做必要的合法性检查(DateTime数据为Hex格式)。

bool RtcCheck(DateTime *dateTime)
{
    uint16_t year;
    uint8_t day;

    // Year
    if (dateTime->year > 99)
    {
        return false;
    }

    // Month
    switch (dateTime->month)
    {
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12:
        day = 31;
        break;

    case 4:
    case 6:
    case 9:
    case 11:
        day = 30;
        break;

    case 2:
        year = yearH + dateTime->year;
        day = (year % ((year % 100) ? 4 : 400)) ? 28 : 29;
        break;

    default:
        return false;
    }

    // Day
    if (dateTime->day == 0 ||
       (dateTime->day > day))
    {
        return false;
    }

    // Hour
    if (dateTime->hour >= 24)
    {
        return false;
    }

    // Minute
    if (dateTime->minute >= 60)
    {
        return false;
    }

    // Second
    if (dateTime->second >= 60)
    {
        return false;
    }

    return true;
}

注:用三目运算符获取当前年份的2月天数:day = (year % ((year % 100) ? 4 : 400)) ? 28 : 29

最终DateTime与Seconds转换的代码实现(DateTime数据为Hex格式)。

// DateTime转换为Seconds
bool RtcDateTimeToSeconds(DateTime *dateTime, uint32_t *seconds)
{
    uint32_t year;
    uint32_t era, yoe;
    uint32_t doe, doy;
    uint32_t days, mp;

    year = yearH + dateTime->year - (dateTime->month <= 2 ? 1 : 0);

    era = (year >= 0 ? year : year - 399) / 400;
    yoe = year - era * 400;
    mp = dateTime->month + (dateTime->month > 2 ? -3 : 9);
    doy = (153 * mp + 2) / 5 + dateTime->day - 1;
    doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;

    days = era * 146097 + doe - 719468;

    *seconds = ((days * 24 + dateTime->hour) * 60 + dateTime->minute) * 60 + dateTime->second;

    return true;
}

// Seconds转换为DateTime
bool RtcSecondsToDateTime(uint32_t seconds, DateTime *dateTime)
{
    uint32_t year;
    uint32_t era, yoe;
    uint32_t doe, doy;
    uint32_t days, mp;

    days = seconds / 86400 + 719468;

    era = (days >= 0 ? days : days - 146096) / 146097;
    doe = days - era * 146097;
    yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    mp = (5 * doy + 2) / 153;

    year = yoe + era * 400 + (dateTime->month <= 2 ? 1 : 0);

    dateTime->day = doy - (153 * mp + 2) / 5 + 1;
    dateTime->month = mp + (mp < 10 ? 3 : -9);
    dateTime->year = year % 100;

    yearH = year / 100 * 100;

    return true;
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ftswsfb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值