时间戳是一种用来表示日期和时间的数字格式,在不同的编程语言里时间戳的长度和单位都不一样:
C:以秒为单位,目前的时间戳是10位数。
Python:以秒为单位并且有精确到7位小数的毫秒,目前的时间戳整数部分是10位数,毫秒是7位小数。
JavaScript:以毫秒为单位,目前的时间戳是13位数。
虽然时间戳在计算机内部处理时间非常方便,但对于人类来说显得不直观。在日常编程工作中经常遇到需要将时间戳转换为日期时间格式方便,有以下好处:
1. 可读性:将时间戳转换为日期时间格式后,时间数据变得更容易理解。这对于用户界面、日志记录和数据可视化非常重要。
2. 数据处理:日期时间格式允许我们执行各种时间相关的操作,如排序、筛选和计算时间间隔。
3. 报告和分析:日期时间格式更容易传达时间信息,使数据更易于解释。
使用C语言的 time.h 库
在C语言中使用 time.h 库来执行时间戳字符串到日期时间格式的转换。下面是演示代码,使用localtime函数将时间戳转换为 tm 结构,然后使用 sprintf 将其格式化为日期时间字符串。
char* timestamp_to_datetime(long long timestamp){struct tm *tm;time_t time_seconds;tm = localtime(&time_seconds);// 容错处理if (tm == NULL) {perror("localtime");return NULL;}char* result=malloc(25);sprintf(result, "%04d-%02d-%02d %02d:%02d:%02d",tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec);return result;
}
我的设想是:对于像JavaScript产生的13位数时间戳,先处理前10位数转换日期时间,然后把末尾的3位数作为毫秒数添加到日期时间里。小于13位数的时间戳则直接转换。因此需要判断时间戳字符串长度,该函数修改如下:
char* timestamp_to_datetime(long long timestamp){int timestamp_length = snprintf(NULL, 0, "%lld", timestamp);struct tm *tm;time_t time_seconds;// 如果时间戳长度为13,则取前10位数为时间戳time_seconds = timestamp_length == 13 ? (time_t)(timestamp / 1000) : (time_t)(timestamp);tm = localtime(&time_seconds);// 容错处理if (tm == NULL) {perror("localtime");return NULL;}// 时间戳的前10位数转换为日期和时间char* result=malloc(25);sprintf(result, "%04d-%02d-%02d %02d:%02d:%02d",tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec);// 如果时间戳长度为13,则将11~13位数作为毫秒数if (timestamp_length == 13) {char ms[5]; // Make sure to have space for the null terminatorsprintf(ms, ".%03lld", timestamp % 1000);strcat(result, ms);}return result;
}
接下来是主程序的代码,接收命令行输入的第一个参数作为时间戳字符串:
int main(int argc, char *argv[]) {long long timestamp;char* result=malloc(25);if (argc != 2) {printf("\n时间戳转换成日期时间。\n\n使用方法:%s <时间戳>\n\n注:时间戳长度不应该超过13位数字。\n", argv[0]);// 获取当前时间time(×tamp);printf("\n当前时间戳:%lld\n", timestamp);result = timestamp_to_datetime(timestamp);printf("时间戳转换为日期时间是:%s\n", result);free(result);return 1;}// 从命令行参数获取第一个参数作为时间戳字符串char *timestamp_str = argv[1];int timestamp_length = strlen(timestamp_str);timestamp = atoll(timestamp_str); printf("你输入的时间戳是:%lld\n长度为%d位数。\n", timestamp, timestamp_length);result = timestamp_to_datetime(timestamp);if (result != NULL){printf("时间戳转换为日期时间是:%s\n", result);free(result);}return 0;
}
程序基本上写好了。但这个代码存在多出bug:
Bug #1:输入参数的合法性
首先没有检查输入参数是否合法,如输入的字符串必须是数字才行,如果输入的参数是混有字母或其它符号,localtime 函数转换不了会直接报错退出程序。
在Python里有检查一个字符串是否全是数字的方法:string.isdigit()。该方法简单有效。C语言虽然也有 isdigit(),但是它只负责检查一个字符是否数字而不是判断字符串,而C语言的库里没有现成的函数检查一个字符串是否数字,所以只好手写一个:
// 检查字符串是否全是数字
int isStringAllDigits(const char *str) {for (int i = 0; str[i] != '\0'; i++) {if (!isdigit((unsigned char)str[i])) {return 0; // 只要检测到非数字的字符串就直接返回0}}return 1; // 全是数字的话返回1
}
或者写得高级一些,使用指针和while循环,更简练高效:
// 检查字符串是否全是数字,高级写法:
int isStringAllDigits(const char *str) {while (*str)if (!isdigit((unsigned char)(*str++)))return 0; return 1;
}
Bug #2:检查输入参数边界的有效性。
输入的时间戳字符串必须限制不超过13位数。
if (timestamp_length > 13) {printf("长度不正确!注:时间戳长度不应该超过13位数字。\n\n");return 1;}
编译后运行测试,发现当时间戳是11、12位数大数字情况下,会返回 localtime 报错信息,数值越过边界了。
查阅相关文档(localtime、_localtime32、_localtime64 | Microsoft Learn),得知:localtime 返回的最大时间是 3000年12月31日 23:59:59,那么对应的时间戳是32535158399。
但是经过我调试,时间戳大于32535158399仍可以输出日期时间:
只要时间戳长度小于13,时间戳的最大值不能超过 32536799999,日期最大值可以到达3001年1月19日15:59:59。
因此需要加一段检查参数是否越过边界的代码:
if (timestamp>32536799999 && timestamp<1000000000000){printf("注:%d位数的数值不能大于32536799999。\n你可以尝试输入13位数的时间戳。", timestamp_length);return 1;}
Bug #3:输入参数的确是数字,但却是以0开头的长串数字。
这个情况很容易被忽视,假如输入的是0开头的数字:001、0123456789,就需要先出去多余的0。
要先转换字符串,从头开始检查非零的位置,然后截取到末尾,最后转成数字吗?
没这么麻烦,C语言已提供一系列灵活的转换函数当中就有:atoll(),把字符串格式化成长整数类型变量:
timestamp = atoll(timestamp_str);
这下就把前面多余的0清除掉了。但有个问题:
假如输入的是很长的0开头的数字,如:00000000000009876543210,
timestamp变量经过 atoll(timestamp_str) 转换后是9876543210。
timestamp_str 字符串储存了“00000000000009876543210”,长度为23。
如果判断timestamp_str的长度是否不超过13,那按照上面代码的判断,将直接输出:长度超过13,程序退出。
所以应该判断 timestamp 的长度。
由于timestamp的类型是long long长整数型,不能直接使用 strlen(timestamp) 来获取其长度,只能另外声明一个临时字符串,比如 formatted_str 来存储 timestamp 转换成字符串,最后判断 formatted_str 的长度:
// 如果输入值为0开头,如:0000123456789,则必须先用atoll转成长数值,去除前面所有0timestamp = atoll(timestamp_str); // 去除前面多余的0之后,用snprintf格式化并存入formatted_strchar formatted_str[20];snprintf(formatted_str, sizeof(formatted_str), "%llu", timestamp);// 判断有效的时间戳字符串长度,不要拿timestamp_str而应该拿formatted_str来判断;int timestamp_length = strlen(formatted_str);printf("你输入的时间戳是:%lld\n长度为%d位数。\n", timestamp, timestamp_length);if (timestamp_length > 13) {printf("长度不正确!注:时间戳长度不应该超过13位数字。\n\n");return 1;}
这样一来,即使输入长长的数字也能妥善处理,不至于返回 localtime 的错误提示。
Bug #4:输出的代码页问题
C代码编译运行的.exe程序,默认以UTF-8格式输出文字。UTF-8对应的代码页为65001。
而Windows系统的命令行的默认格式是GBK,代码页为936。
上述的代码编译运行在Windows的命令行里,所有中文会显示成乱码。为了适应Windows的命令行,C代码里应该在主程序加一行设定代码页为UTF-8,以确保输出的文字正确显示。
#include <windows.h>。。。int main(int argc, char *argv[]) {// 切换至UTF-8(65001)环境输出if (GetConsoleOutputCP() != CP_UTF8)SetConsoleOutputCP(CP_UTF8); 。。。
}
好了,经过上面检查输入合法性、检查变量边界,代码的bug基本上修复好了,下面是完整的代码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <windows.h>// 检查字符串是否全是数字
int isStringAllDigits(const char *str) {while (*str)if (!isdigit((unsigned char)(*str++)))return 0; return 1;
}char* timestamp_to_datetime(long long timestamp){int timestamp_length = snprintf(NULL, 0, "%lld", timestamp);struct tm *tm;time_t time_seconds;// 如果时间戳长度为13,则取前10位数为时间戳time_seconds = timestamp_length == 13 ? (time_t)(timestamp / 1000) : (time_t)(timestamp);tm = localtime(&time_seconds);// 容错处理if (tm == NULL) {perror("localtime");return NULL;}// 时间戳的前10位数转换为日期和时间char* result=malloc(25);sprintf(result, "%04d-%02d-%02d %02d:%02d:%02d",tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec);// 如果时间戳长度为13,则将11~13位数作为毫秒数if (timestamp_length == 13) {char ms[5];sprintf(ms, ".%03lld", timestamp % 1000);strcat(result, ms);}return result;
}int main(int argc, char *argv[]) {// 切换至UTF-8(65001)环境输出if (GetConsoleOutputCP() != CP_UTF8) SetConsoleOutputCP(CP_UTF8); long long timestamp;char* result=malloc(25);if (argc != 2) {printf("\n时间戳转换成日期时间。\n\n使用方法:%s <时间戳>\n\n注:时间戳长度不应该超过13位数字。\n", argv[0]);// 获取当前时间time(×tamp);printf("\n当前时间戳:%lld\n", timestamp);result = timestamp_to_datetime(timestamp);printf("时间戳转换为日期时间是:%s\n", result);free(result);return 1;}// 从命令行参数获取第一个参数作为时间戳字符串char *timestamp_str = argv[1];// 判断输入参数是否全是数字if (!isStringAllDigits(timestamp_str)) {printf("输入不合法。请输入由数字组成的时间戳。\n");return 1;}// 如果输入值为0开头,如:0000123456789,则必须先用atoll转成长数值,去除前面所有0timestamp = atoll(timestamp_str); // 去除前面多余的0之后,用snprintf格式化并存入formatted_strchar formatted_str[20];snprintf(formatted_str, sizeof(formatted_str), "%llu", timestamp);// 判断有效的时间戳字符串长度,不要拿timestamp_str而应该拿formatted_str来判断;int timestamp_length = strlen(formatted_str);printf("你输入的时间戳是:%lld\n长度为%d位数。\n", timestamp, timestamp_length);if (timestamp_length > 13) {printf("长度不正确!注:时间戳长度不应该超过13位数字。\n\n");return 1;}if (timestamp>32536799999 && timestamp<1000000000000){printf("注:%d位数的数值不能大于32536799999。\n你可以尝试输入13位数的时间戳。\n", timestamp_length);return 1;}result = timestamp_to_datetime(timestamp);if (result != NULL){printf("时间戳转换为日期时间是:%s\n", result);free(result);}return 0;
}
运行结果截图: