项目开发种需要将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日作为每年的第一天。
era | start date | end date |
---|---|---|
-2 | -0800-03-01 | -0400-02-29 |
-1 | -0400-03-01 | 0000-02-29 |
0 | 0000-03-01 | 0400-02-29 |
1 | 0400-03-01 | 0800-02-29 |
2 | 0800-03-01 | 1200-02-29 |
3 | 1200-03-01 | 1600-02-29 |
4 | 1600-03-01 | 2000-02-29 |
5 | 2000-03-01 | 2400-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无影响。
m | mp | doy |
---|---|---|
3 | 0 | 0 |
4 | 1 | 31 |
5 | 2 | 61 |
6 | 3 | 92 |
7 | 4 | 122 |
8 | 5 | 153 |
9 | 6 | 184 |
10 | 7 | 214 |
11 | 8 | 245 |
12 | 9 | 275 |
1 | 10 | 306 |
2 | 11 | 337 |
通过对月份进行映射,可以看出每个月份对应的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;
}