距1970的天数与年月日的转换算法

背景

在日志系统中调用localtime来转换为用户可见的时间(即距1970-01-01的秒数转换为年月日时分秒),会低概率的出现异常。原因是localtime()这个函数是线程不安全的,函数返回一个指向struct tm结构体的指针,这个指针指向一个静态变量。所以在多线程下,会概率性导致踩内存问题。
另外,虽然localtime_r()可以处理这个问题,但是localtime_r()的底层实现上,会对资源进行lock,在log系统里面使用该函数会造成一定程度上性能的损耗。

因此,需要一个可以快速将距1970-01-01的秒数转换为年月日时分秒的算法

yyyy-mm-dd转换为距1970年的天数

1、将3月1号当做每年的第一天。如下表,mm是实际的月份,mp是转换之后的月份,doy是days-of-year,即距离每年的第一天的天数。

mm 3 4 5 6 7 8 9 10 11 12 1 2
mp 0 1 2 3 4 5 6 7 8 9 10 11
doy 0 31 61 92 122 153 184 214 245 275 306 337

通过上表,可以推导出mp和doy的公式,如下:
doy = (153 * mp + 2) / 5 + dd - 1 // 这个式子是通过 (b1 * mp + b0) / a0这样一个公式假设出来的。
mp = (mm + 9) % 12 = mm + (mm > 2 ? -3 : 9)

2、由于每400年的总天数是相同的,因此以400年为单位进行计算。era指有多少个400年,yoe(years-of-era)是400年内的第几年,yp是转换后的年数(由于每年的第一天换成的3月1号,所以1月和2月被算到了上一年)。于是:
yp = year - (month <= 2)
era = (yp >= 0 ? yp : yp - 399) / 400 (yp为负数说明是公元前)
yoe = yp - era * 400

3、步骤1得到了doy,步骤2得到了yoe,因此可以推算出来doe(days-of-era),即距离400年内的第一天的天数:
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy。
解释下上面的公式,先复习下闰年的定义:被4整除的年份,以及被400整除的年份是闰年,整百但不能被400整除的年份不是闰年。所以 yoe * 365 + yoe / 4 - yoe / 100是400年内到yoe这一年的总天数,再加上一个doy就是400年内的第一天的天数。

4、得到最终结果——总天数Ans。
Ans = era * 146097 + doe - 719162 - 306
解释下这个公式146097是400年的总天数,719162是1970-01-01到0001-01-01的总天数,306是换算后的第10个月到第一天的天数。于是:
era * 146097 + doe就是yyyy-mm-dd到0000-10-01的天数。
719161是1970-01-01到0001-01-01的天数。
306是0001-01-01到0000-10-01的天数。


// Returns number of days since civil 1970-01-01.  Negative values indicate
//    days prior to 1970-01-01.
// Preconditions:  y-m-d represents a date in the civil (Gregorian) calendar
//                 m is in [1, 12]
//                 d is in [1, last_day_of_month(y, m)]
//                 y is "approximately" in
//                   [numeric_limits::min()/366, numeric_limits::max()/366]
//                 Exact range of validity is:
//                 [civil_from_days(numeric_limits::min()),
//                  civil_from_days(numeric_limits::max()-719468)]
template 
constexpr
Int
days_from_civil(Int y, unsigned m, unsigned d) noexcept
{
    static_assert(std::numeric_limits::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    y -= m <= 2;
    const Int era = (y >= 0 ? y : y-399) / 400;
    const unsigned yoe = static_cast(y - era * 400);      // [0, 399]
    const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d-1;  // [0, 365]
    const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;         // [0, 146096]
    return era * 146097 + static_cast(doe) - 719468;
}

距1970的天数days转换为yyyy-mm-dd

1、计算1970-01-01到0000-03-1的天数,era、doe
days += 719162 + 306
era = days / 146097
doe = days - era * 146097

2、推导出yoe和doy。
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
推导方式如下:
1460 = 365 * 4, 第4年会多出1天(闰年)
35624 = 365 * 100 + 100 / 4 - 1,第100年不是闰年
146096 = 365 * 400 + 400 / 4 - 400 / 100

doy = doe - ( 365 * yoe + yoe / 4 - yoe / 100)

3、得到yp、mp、dp(即基于03-01是第一天的值)
yp = yoe + era * 400
mp = ( 5 * doy + 2) / 153 // PS: 这里不能直接使用上一章节的逆向推导,因为那个公式是假设出来的,并不完全准确,因此需要稍微修改下。
dp = doy - (153 * mp + 2) / 5 + 1

4、得到yyyy、mm、dd
yyyy = yp + (m <=2)
mm = mp + (mp < 10 ? 3 : -9)
dd = dp

// Returns year/month/day triple in civil calendar
// Preconditions:  z is number of days since 1970-01-01 and is in the range:
//                   [numeric_limits::min(), numeric_limits::max()-719468].
template 
constexpr
std::tuple
civil_from_days(Int z) noexcept
{
    static_assert(std::numeric_limits::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    z += 719468;
    const Int era = (z >= 0 ? z : z - 146096) / 146097;
    const unsigned doe = static_cast(z - era * 146097);          // [0, 146096]
    const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
    const Int y = static_cast(yoe) + era * 400;
    const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
    const unsigned mp = (5*doy + 2)/153;                                   // [0, 11]
    const unsigned d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
    const unsigned m = mp + (mp < 10 ? 3 : -9);                            // [1, 12]
    return std::tuple(y + (m <= 2), m, d);
}

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

其它类似文章:
https://blog.csdn.net/wang93IT/article/details/79744711?utm_source=blogxgwz0
https://www.cnblogs.com/westfly/p/5139645.html
https://sites.google.com/site/malfoxcn/一个精巧的日期差算法赏析

你可能感兴趣的:(杂七杂八,C++,算法,date转换)