时间戳是一种用来表示日期和时间的数字格式,在不同的编程语言里时间戳的长度和单位都不一样:

C:以秒为单位,目前的时间戳是10位数。

Python:以秒为单位并且有精确到7位小数的毫秒,目前的时间戳整数部分是10位数,毫秒是7位小数。

JavaScript:以毫秒为单位,目前的时间戳是13位数。

虽然时间戳在计算机内部处理时间非常方便,但对于人类来说显得不直观。在日常编程工作中经常遇到需要将时间戳转换为日期时间格式的情况,便于数据处理、分析和报告。

使用C语言的 time.h 库

在C语言中使用 time.h 库来执行时间戳字符串到日期时间格式的转换。下面是演示代码,使用localtime函数将时间戳转换为 tm 结构,然后使用 sprintf 将其格式化为日期时间字符串。

char* timestamp_to_datetime(long long timestamp){
    int timestamp_length = snprintf(NULL, 0, "%lld", timestamp);
    static char result[20];
    struct tm *tm;
    time_t time_seconds;
    tm = localtime(&time_seconds);
    // 容错处理
    if (tm == NULL) {
        perror("localtime");
        result[0] = '\0';
        return result;
    }

    // 时间戳的前10位数转换为日期和时间
    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);
    static char result[20];
    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");
        result[0] = '\0';
        return result;
    }

    // 时间戳的前10位数转换为日期和时间
    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[4];
        sprintf(ms, "%lld", timestamp % 1000);
        strcat(result, ms);
    }
    return result;
}

接下来是主程序的代码,接收命令行输入的第一个参数作为时间戳字符串:

int main(int argc, char *argv[]) {
    long long timestamp;
    char result[20];
    if (argc != 2) {
        printf("\n时间戳转换成日期时间。\n\n使用方法:%s <时间戳>\n\n注:时间戳长度不应该超过13位数字。\n", argv[0]);
        // 获取当前时间
        time(&timestamp);
        printf("\n当前时间戳:%lld\n", timestamp);
        sprintf(result, "%s", timestamp_to_datetime(timestamp));
        printf("时间戳转换为日期时间是:%s\n", result);
        return 1;
    }

    // 从命令行参数获取第一个参数作为时间戳字符串
    timestamp = atoll(argv[1]); 
    int timestamp_length = strlen(argv[1]);
    printf("你输入的时间戳是:%lld\n长度为%d位数。\n", timetamp, timestamp_length);

    result = timestamp_to_datetime(timestamp);
    if (result[0]){
        printf("时间戳转换为日期时间是:%s\n", 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) 来获取其长度,只能使用 snprintf 函数获取:

    // 如果输入值为0开头,如:0000123456789,则必须先用atoll转成长数值,去除前面所有0
    timestamp = atoll(argv[1]);   
    int timestamp_length = snprintf(NULL, 0, "%lld", timestamp);
    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)环境输出
    int codePage = GetConsoleOutputCP();
    SetConsoleOutputCP(CP_UTF8); 
    。。。
}

如果是 Linux 系统,则无需做这一步,毕竟 Linux 默认字符编码是 UTF-8,而且没有 windows.h 库文件。

好了,经过上面检查输入合法性、检查变量边界,代码的bug基本上修复好了,下面是完整的代码。

#include <stdio.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);
    static char result[20];
    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");
        result[0] = '\0';
        return result;
    }

    // 时间戳的前10位数转换为日期和时间
    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[4];
        sprintf(ms, "%lld", timestamp % 1000);
        strcat(result, ms);
    }
    return result;
}


int main(int argc, char *argv[]) {
    // 切换至UTF-8(65001)环境输出
    int codePage = GetConsoleOutputCP();
    SetConsoleOutputCP(CP_UTF8); 
    long long timestamp;
    char result[20];
    if (argc != 2) {
        printf("\n时间戳转换成日期时间。\n\n使用方法:%s <时间戳>\n\n注:时间戳长度不应该超过13位数字。\n", argv[0]);
        // 获取当前时间
        time(&timestamp);
        printf("\n当前时间戳:%lld\n", timestamp);
        sprintf(result, "%s", timestamp_to_datetime(timestamp));
        printf("时间戳转换为日期时间是:%s\n", result);
        SetConsoleOutputCP(codePage);
        return 1;
    }

    // 从命令行参数获取第一个参数作为时间戳字符串
    // 判断输入参数是否全是数字
    if (!isStringAllDigits(argv[1])) {
        printf("输入不合法。请输入由数字组成的时间戳。\n");
        SetConsoleOutputCP(codePage);
        return 1;
    }

    // 如果输入值为0开头,如:0000123456789,则必须先用atoll转成长数值,去除前面所有0
    timestamp = atoll(argv[1]);   
    int timestamp_length = snprintf(NULL, 0, "%lld", timestamp);
    printf("你输入的时间戳是:%lld\n长度为%d位数。\n", timestamp, timestamp_length);

    if (timestamp_length > 13) {
        printf("长度不正确!注:时间戳长度不应该超过13位数字。\n\n");
        SetConsoleOutputCP(codePage);
        return 1;
    }
        
    if (timestamp>32536799999 && timestamp<1000000000000){
        printf("注:%d位数的数值不能大于32536799999。\n你可以尝试输入13位数的时间戳。\n", timestamp_length);
        SetConsoleOutputCP(codePage);
        return 1;
    }
    sprintf(result, "%s", timestamp_to_datetime(timestamp));
    if (result[0]){
        printf("时间戳转换为日期时间是:%s\n", result);
        SetConsoleOutputCP(codePage);
    }
    return 0;
}

运行结果截图:

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐