手动计算时间戳转日期

rust标准库没有时间戳转日期的方法,为了区区一个功能引入三方库又觉得划不来,于是准备自己实现

以前看到过转换时间戳的方法,但是这回网上找了半天都没找到想要的代码,全是各种语言调标准库或者三方库的,找来找去只有这个,但是评论区又说有bug,我看过代码好像闰年部分有点问题

思路

首先时间戳定义是1970年1月1日到指定时间的秒数,正数往后,负数往前,这里不需要考虑负数,一般采用32位存储,所以最多只能存储到2038年,又叫千年虫问题

时间戳计算的难度就在闰年,闰年导致每年的秒数不一致,从而不便于定位到年。顺便一提,除了闰年还有闰秒,每隔不确定的时间,将现在时间进行减一秒或者加一秒操作,闰秒也会导致一些大公司的系统出现bug,而且闰秒是国际协会根据地球运动情况确定的,不像闰年这样规律,所以不太好弄,不过时间戳里没有闰秒概念

为了确定过去到底有多少秒,最简单的办法就是把1970年到2038年每一年的秒数都累加出来,看时间戳小于哪一年,这个时间戳就是上一年的,再减去过去经过的秒数,剩下的就是在今年的秒数,确定日期和时间就很简单了。我看jdk里似乎就是这样干的,代码里把每一年的都硬编码了

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//
// 判断是否是闰年
//
#[inline]
fn is_leap(year: u64) -> bool {
return year % 4 == 0 && ((year % 100) != 0 || year % 400 == 0);
}

fn do_time_format(value: u64) -> String {
// 获取当前时间戳
let mut time = value;
let per_year_sec = 365 * 24 * 60 * 60; // 平年的秒数

// 平年的月份天数
let mut day_of_year: [u64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

let mut all_sec = 0;
// 直接算到 2038年,把每一年的秒数加起来看哪年合适
for year in 1970..2038 {
let is_leap = is_leap(year);

let before_sec = all_sec;
all_sec += per_year_sec;
if is_leap {
all_sec += 86400;
}
// println!("all={all_sec} before_sec={before_sec} year={year}");
// 具体是哪一年应该是 当 小于这一年的秒数
if time < all_sec {
// 减去到上一年年底的秒数 剩下的才是这一年内的秒数
time = value - before_sec;
// 找到了 计算日期
let sec = time % 60;
time /= 60;
let min = time % 60;
time /= 60;
let hour = time % 24;
time /= 24;

// 计算是哪天,因为每个月不一样多,所以需要修改
if is_leap {
day_of_year[1] += 1;
}
let mut month = 0;
for (index, ele) in day_of_year.iter().enumerate() {
if &time < ele {
month = index + 1;
time += 1; // 日期必须加一,否则 每年的 第 1 秒就成了第0天了
break;
}
time -= ele;
}

return format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, time, hour, min, sec
);
}
}

String::new()
}

改进

再思索下上面的程序,可以发现一个问题,那就是即使使用了64位存储,千年虫问题依然存在,因为只计算到了2038年

那怎么去掉这个上限呢?

因为闰年的存在,没法简单的直接确定年份,但是如果假设只有平年,那么年份就可以做个除法获得,这个粗略值只会比精确值更晚,比如粗略值可能是1988-01-01,因为中间有闰年,所以实际年份应该是1987,而且不会出现往前走两年的情况,因为这样需要中间有365个闰年,但是粗略值本身就是除以365的结果,这里我也不知道该怎样描述更清晰明了一点。

总之现在有个年份的粗略值,以及剩余的在年内的秒数,只需要循环判断一下1970到粗略值有多少个闰年,再用时间戳减去按平年计算的秒数和多出来的闰年的秒数,不直接用剩余秒数去减主要是考虑两种情况

一是中间没有闰年的情况,比如1971年;二是时间戳除以平年刚好能整除,剩余秒数就是0。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
fn do_time_format2(value: u64) -> String {
// 先粗略定位到哪一年
// 以 365 来计算,年通常只会相比正确值更晚,剩下的秒数也就更多,并且有可能出现需要往前一年的情况

let per_year_sec = 365 * 24 * 60 * 60; // 平年的秒数

let mut year = value / per_year_sec;
// if year * per_year_sec == value {
// // 刚好是个整数倍
// year -= 1;
// }
// 剩下的秒数,如果这些秒数 不够填补闰年,比如粗略计算是 2024年,还有 86300秒,不足一天,那么中间有很多闰年,所以 年应该-1,只有-1,因为-2甚至更多 需要 last_sec > 365 * 86400,然而这是不可能的
let mut last_sec = value - (year) * per_year_sec;
year += 1970;

let mut leap_year_sec = 0;
// 计算中间有多少闰年,当前年是否是闰年不影响回退,只会影响后续具体月份计算
for y in 1970..year {
if is_leap(y) {
// 出现了闰年
leap_year_sec += 86400;
}
}
if last_sec < leap_year_sec {
// 不够填补闰年,年份应该-1
year -= 1;
// 上一年是闰年,所以需要补一天
if is_leap(year) {
leap_year_sec -= 86400;
}
}
// 剩下的秒数
let mut time = value - leap_year_sec - (year - 1970) * per_year_sec;

// 平年的月份天数
let mut day_of_year: [u64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

// 找到了 计算日期
let sec = time % 60;
time /= 60;
let min = time % 60;
time /= 60;
let hour = time % 24;
time /= 24;

// 计算是哪天,因为每个月不一样多,所以需要修改
if is_leap(year) {
day_of_year[1] += 1;
}
let mut month = 0;
for (index, ele) in day_of_year.iter().enumerate() {
if &time < ele {
month = index + 1;
time += 1; // 日期必须加一,否则 每年的 第 1 秒就成了第0天了
break;
}
time -= ele;
}

return format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, time, hour, min, sec
);
}

验证

我用java直接生成2038为止每一天,随机时间的时间戳数据,共计两万多天。用了assert_eq!宏来做断言,然后编译总是出错,还没有错误原因,只有一个kill 9。最后把宏换成了方法调用就可以了,怀疑就是太多宏影响了编译,毕竟两万多个宏。


手动计算时间戳转日期
http://blog.inkroom.cn/2024/08/06/3DR7BNH.html
作者
inkbox
发布于
2024年8月6日
许可协议