C语言应用(1)——Unix时间戳和北京时间的相互转换

一、时间戳

准确的说,应该是unix时间戳,是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。一个小时表示为UNIX时间戳格式为:3600秒;一天表示为UNIX时间戳为86400秒,闰秒不计算。在很多API接口中,数据的更新时间并不是一个字符串,而是一个长整形数据,如1593003485,表示是北京时间2020-06-24 20:58:05。注意这里直接换算出的是北京时间,如果用时间戳直接转换的话,得到的时间UTC/GMT时间,和北京时间相差8个小时,在原始时间戳加上8个小时再进行转换就是北京时间了。大部分时间戳是以秒为单位的,有的时间戳是以毫秒为单位的。

在大多数的UNIX系统中UNIX时间戳存储为32位,这样会引发2038年问题或Y2038。

在线转换工具:北京时间和UNIX时间戳在线转换

二、使用C库函数进行转换

2.1 头文件time.h

如果使用C库函数进行转换,使用之前先要包含对应的头文件:

#include 

头文件中有一个比较重要的结构体:

/* 时间戳类型,单位为秒,与uint32_t类型一样 */
typedef unsigned int time_t;     

struct tm {
    int tm_sec;   /* 秒钟,范围0-60,偶尔的闰秒 */
    int tm_min;   /* 分钟,范围0-59 */
    int tm_hour;  /* 小时,范围0-23*/
    int tm_mday;  /* 日,范围1-31 */
    int tm_mon;   /* 月份,范围0-11 */
    int tm_year;  /* 年份,自从1900年 */
    int tm_wday;  /* 星期,范围0-6 */
    int tm_yday;  /* 一年的第几天,范围0-365 */
    int tm_isdst; /* 夏令时标志 */
};

这里,我们要注意几个时间的修正:

年份自1900算起,转换为实际年份,要+1900
月份范围0-11,转换为实际月份,要+1
星期范围0-6,转换为实际星期,要+1

2.1.1 localtime

struct tm * localtime(const time_t *);
给定一个毫秒级时间戳,返回时间结构体

2.1.2 mktime

time_t mktime(struct tm *);
给定一个初始化完成的时间结构体,返回一个毫秒级时间戳,
转换时不考虑tm结构的tm_wday和tm_yday,仅用tm_mday来决定日期。

2.1.3 strftime

size_t strftime(char *strDest, size_t maxsize, const char *format, const struct tm *timeptr);
给定一个时间结构体,格式化输出字符串,格式化字符可参考
https://baike.baidu.com/item/strftime

示例:
char str[100];
strftime(str, 100, "%F %T", time);  /* 2020-07-01 02:16:51 */
strftime(str, 100, "%m-%d %H:%M", time);  /* 06-30 22:16 */
printf("%s\n", str);

2.2 Unix时间戳转北京时间

输入毫秒级时间戳,调用系统函数,把时间戳转换为UTC时间,为了得到北京时间,在转换之前要先加上8个小时的补偿时间:

#include "time.h"
.....
int main(void)
{
    char str[100];
    struct tm *time;
    uint16_t year, yday;
    uint8_t month, day, week, hour, minute, second;
    time_t timestamp = 1592932611;  /*北京时间2020-06-24 01:16:51*/
/*
    几个用于测试的时间戳和北京时间对应
    1592932611 = 2020-06-24 01:16:51(北京时间) 
    1593541011 = 2020-07-01 02:16:51
    1593526611 = 2020-06-30 22:16:51
*/    

    /* 北京时间补偿 */
    timestamp += 8*60*60;
    /* 调用系统函数 */
    time = localtime(×tamp);
    
    year = time->tm_year;   /* 自1900年算起 */
    month = time->tm_mon;   /* 从1月算起,范围0-11 */
    week = time->tm_wday;   /* 从周末算起,范围0-6 */
    yday = time->tm_yday;  /* 从1月1日算起,范围0-365 */
    day = time->tm_mday;    /* 日: 1-31 */
    hour = time->tm_hour;   /* 小时:0-23点,UTC+0时间 */
    minute = time->tm_min;  /* 分钟:0-59 */
    second = time->tm_sec;  /* 0-60,偶尔出现的闰秒 */
    
    /* 时间校正 */
    year += 1900;
    month += 1;
    week += 1;
    
    printf("UNIX时间戳:%d\r\n", timestamp);
    printf("日期:%d-%d-%d 第%d天 星期%d 时间:%d:%d:%d\r\n",
        year, month, day, yday, week, hour, minute, second);
    
    /* 格式化时间字符串 */
    strftime(str, 100, "%F %T", time);  /* 2020-07-01 02:16:51 */
//    strftime(str, 100, "%m-%d %H:%M", time);  /* 06-30 22:16 */
    printf("%s\r\n", str);

    while(1)
    {
        ;
    }
}

运行结果:


2.3 北京时间转Unix时间戳

给定北京时间:2020-06-24 01:16:51,输出时间戳1592932611,北京时间先转为UTC8时间戳,再去掉8个小时,转为标准的UNIX时间戳。

#include "time.h"
.....

int main(void)
{
    struct tm time;
    time_t timestamp;
    
    /* 2020-6-25 19:11:50 */
    uint16_t str[6] = {2020, 6, 25, 19, 11, 50};
    
    time.tm_year = str[0] - 1900;   /* 年份修正 */
    time.tm_mon = str[1] - 1;       /* 月份修正 */
    time.tm_mday = str[2];
    time.tm_hour = str[3];
    time.tm_min = str[4];
    time.tm_sec = str[5];
    
    /* 去掉北京时间8个小时 */
    timestamp = mktime(&time) - 8*60*60;    
    /*1593083510 = 2020-6-25 19:11:50*/
    printf("%d\r\n", timestamp);    
  
    while(1)
    {
        ;
    }
}

2.4 写成函数和调用示例

#include "usart.h"
#include "time.h"

/* 定义结构体,时间为北京时间格式 */
typedef struct{
    uint16_t year;
    uint8_t month;
    uint8_t day;
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
}bj_time;

bj_time timestamp_to_bj_time(time_t timestamp);
time_t bj_time_to_timestamp(bj_time time);

int main(void)
{
    time_t timestamp;
    bj_time time;
    
    timestamp = 1593083510;
    printf("%d\r\n", timestamp);
    
    /* 时间戳转北京时间 */
    time = timestamp_to_bj_time(timestamp);
    /* 2020-6-25 19:11:50 */
    printf("%d-%d-%d %d:%d:%d\r\n",
        time.year, time.month, time.day, time.hour, time.minute, time.second);
    
    /* 北京时间转时间戳 */
    timestamp = bj_time_to_timestamp(time);
    printf("%d\r\n", timestamp);
    
    while(1)
    {
        ;
    }
}


bj_time timestamp_to_bj_time(time_t timestamp)
{
    bj_time time;
    
    struct tm *t;
    
    /* 加上8个小时 */
    timestamp += 8*60*60;
    t = localtime(×tamp);
    
    /* 日期修正 */
    time.year = t->tm_year + 1900;
    time.month = t->tm_mon + 1;
    time.day = t->tm_mday;
    time.hour = t->tm_hour;
    time.minute = t->tm_min;
    time.second = t->tm_sec;
    
    return time;
}

time_t bj_time_to_timestamp(bj_time time)
{
    struct tm t;
    time_t timestamp = 0;
    
    /* 日期修正 */
    t.tm_year = time.year - 1900;
    t.tm_mon = time.month - 1;
    t.tm_mday = time.day;
    t.tm_hour = time.hour;
    t.tm_min = time.minute;
    t.tm_sec = time.second;

    timestamp = mktime(&t) - 8*60*60;
    return timestamp;
}

运行结果:


三、使用算法进行转换

3.1 Unix时间戳转北京时间

#include
#include
#include

typedef unsigned int time_t;
 
struct tm {
    int tm_sec; /* 秒 – 取值区间为[0,59] */
    int tm_min; /* 分 - 取值区间为[0,59] */
    int tm_hour; /* 时 - 取值区间为[0,23] */
    int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
    int tm_mon; /* 月份(从一月开始,0代表一月) - 取值区间为[0,11] */
    int tm_year; /* 年份,其值等于实际年份减去1900 */
};

const char Days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

void localtime(time_t time,struct tm *t)
{
    unsigned int Pass4year;
    int hours_per_year;
 
    if(time < 0)
    {
        time = 0;
    }
    //取秒时间
    t->tm_sec=(int)(time % 60);
    time /= 60;
    //取分钟时间
    t->tm_min=(int)(time % 60);
    time /= 60;
    //取过去多少个四年,每四年有 1461*24 小时
    Pass4year=((unsigned int)time / (1461L * 24L));
    //计算年份
    t->tm_year=(Pass4year << 2) + 1970;
    //四年中剩下的小时数
    time %= 1461L * 24L;
    //校正闰年影响的年份,计算一年中剩下的小时数
    for (;;)
    {
        //一年的小时数
        hours_per_year = 365 * 24;
        //判断闰年
        if((t->tm_year & 3) == 0)
        {
            //是闰年,一年则多24小时,即一天
            hours_per_year += 24;
        }
        if(time < hours_per_year)
        {
            break;
        }
        t->tm_year++;
        time -= hours_per_year;
    }
    //小时数
    t->tm_hour=(int)(time % 24);
    //一年中剩下的天数
    time /= 24;
    //假定为闰年
    time++;
    //校正闰年的误差,计算月份,日期
    if((t->tm_year & 3) == 0)
    {
        if(time > 60)
        {
            time--;
        }
        else
        {
            if(time == 60)
            {
                t->tm_mon = 1;
                t->tm_mday = 29;
                return ;
            }
        }
    }
    //计算月日
    for(t->tm_mon = 0; Days[t->tm_mon] < time; t->tm_mon++)
    {
        time -= Days[t->tm_mon];
    }
 
    t->tm_mday = (int)(time);
 
    return;
}

int main(void)
{
    char str[100];
    struct tm time;
    uint16_t year, yday;
    uint8_t month, day, week, hour, minute, second;
    time_t timestamp = 1592932611;  /*北京时间2020-06-24 01:16:51*/
/*
    几个用于测试的时间戳和北京时间对应
    1592932611 = 2020-06-24 01:16:51(北京时间) 
    1593541011 = 2020-07-01 02:16:51
    1593526611 = 2020-06-30 22:16:51
*/    

    /* 北京时间补偿 */
    timestamp += 8*60*60;
    /* 调用系统函数 */
    localtime(timestamp, &time);
    
    year = time.tm_year;   /* 自1900年算起 */
    month = time.tm_mon;   /* 从1月算起,范围0-11 */
    day = time.tm_mday;    /* 日: 1-31 */
    hour = time.tm_hour;   /* 小时:0-23点,UTC+0时间 */
    minute = time.tm_min;  /* 分钟:0-59 */
    second = time.tm_sec;  /* 0-60,偶尔出现的闰秒 */
    
    /* 时间校正 */
    month += 1;
    week += 1;
    
    printf("UNIX时间戳:%d\r\n", timestamp);
    printf("日期:%d-%d-%d 时间:%d:%d:%d\r\n",
        year, month, day, hour, minute, second);
    
    return 0;
}

运行结果:


3.2 北京时间转Unix时间戳

#include
#include
#include

typedef unsigned int time_t;
 
struct tm {
    int tm_sec; /* 秒 – 取值区间为[0,59] */
    int tm_min; /* 分 - 取值区间为[0,59] */
    int tm_hour; /* 时 - 取值区间为[0,23] */
    int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
    int tm_mon; /* 月份(从一月开始,0代表一月) - 取值区间为[0,11] */
    int tm_year; /* 年份,其值等于实际年份减去1900 */
};

static time_t mon_yday[2][12] =
{
    {0,31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
    {0,31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335},
};
 
int isleap(int year)
{
    return (year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0);
}
 
time_t mktime(struct tm dt)
{
    time_t result;
    int i =0;
    // 以平年时间计算的秒数
    result = (dt.tm_year - 1970) * 365 * 24 * 3600 +
    (mon_yday[isleap(dt.tm_year)][dt.tm_mon-1] + dt.tm_mday - 1) * 24 * 3600 +
    dt.tm_hour * 3600 + dt.tm_min * 60 + dt.tm_sec;
    // 加上闰年的秒数
    for(i=1970; i < dt.tm_year; i++)
    {
        if(isleap(i))
        {
            result += 24 * 3600;
        }
    }
    return(result);
}

int main(void)
{
    struct tm time;
    uint16_t year, yday;
    uint8_t month, day, week, hour, minute, second;
    time_t timestamp = 0;
    /* 2020-6-24 01:16:51 */
    uint16_t str[6] = {2020, 6, 24, 1, 16, 51};
/*
    几个用于测试的时间戳和北京时间对应
    1592932611 = 2020-06-24 01:16:51(北京时间) 
    1593541011 = 2020-07-01 02:16:51
    1593526611 = 2020-06-30 22:16:51
*/    
    
    time.tm_year = str[0];   /* 自1900年算起 */
    time.tm_mon = str[1]; /* 从1月算起,范围0-11 */
    time.tm_mday = str[2];    /* 日: 1-31 */
    time.tm_hour = str[3];   /* 小时:0-23点,UTC+0时间 */
    time.tm_min = str[4];  /* 分钟:0-59 */
    time.tm_sec = str[5];  /* 0-60,偶尔出现的闰秒 */
    
    timestamp = mktime(time) - 8*60*60;  //将年月日时分秒再次转换成时间戳,验证算法是否正确
    printf("Beijing time:%d\r\n",timestamp);
    
    return 0;
}

运行结果:


四、用C实现转化,并根据根据蔡勒公式计算星期几

4.1 蔡勒公式

数学家蔡勒(Zeller)推算出了这个公式,使用这个公式随便给出一个日期,就可以计算出是星期几。
W = [C / 4] - 2C + y + [y / 4] + [13 * (M + 1) / 5] + d - 1
或者是:w = y + [y / 4] + [c / 4] - 2c + [26(m + 1) / 10] + d - 1

公式中的符号含义如下:
w:星期; w对7取模得:0-星期日,1-星期一,2-星期二,3-星期三,4-星期四,5-星期五,6-星期六
c:世纪-1(前两位数)
y:年(后两位数)
m:月(m大于等于3,小于等于14,即在蔡勒公式中,某年的1、2月要看作上一年的13、14月来计算,比如2003年1月1日要看作2002年的13月1日来计算)
d:日 [ ]代表取整,即只要整数部分。

下面以中华人民共和国成立100周年纪念日那天(2049年10月1日)来计算是星期几,过程如下:
w = y + [y / 4] + [c / 4] - 2c + [26(m + 1) / 10] + d - 1
= 49 + [49 / 4] + [20 / 4] - 2 × 20 + [26 × (10 + 1) / 10] + 1 - 1
= 49 + [12.25] + 5 - 40 + [28.6]
= 49 + 12 + 5 - 40 + 28
= 54 (除以7余5)

即2049年10月1日(100周年国庆)是星期五。

再比如计算2006年4月4日,过程如下:
w = y + [y / 4] + [c / 4] - 2c + [26(m + 1) / 10] + d - 1
= 6 + [6 / 4] + [20 / 4] - 2 * 20 + [26 * (4 + 1) / 10] + 4 - 1
= -12 (除以7余5,注意对负数的取模运算!实际上应该是星期二而不是星期五)

不过要注意的是,蔡勒公式只适合于1582年(明朝万历十年)10月15日之后的情形。

4.2 具体实现

#include 
#include 

#define UTC_BASE_YEAR 1970
#define MONTH_PER_YEAR 12
#define DAY_PER_YEAR 365
#define SEC_PER_DAY 86400
#define SEC_PER_HOUR 3600
#define SEC_PER_MIN 60

/* 每个月的天数 */
const unsigned char g_day_per_mon[MONTH_PER_YEAR] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

/* 自定义的时间结构体 */
typedef struct
{
    unsigned short nYear;
    unsigned char nMonth;
    unsigned char nDay;
    unsigned char nHour;
    unsigned char nMin;
    unsigned char nSec;
    unsigned char DayIndex; /* 0 = Sunday */
} mytime_struct;

/*
 * 功能:
 *     判断是否是闰年
 * 参数:
 *     year:需要判断的年份数
 *
 * 返回值:
 *     闰年返回1,否则返回0
 */
unsigned char applib_dt_is_leap_year(unsigned short year)
{
    /*----------------------------------------------------------------*/
    /* Local Variables                                                */
    /*----------------------------------------------------------------*/

    /*----------------------------------------------------------------*/
    /* Code Body                                                      */
    /*----------------------------------------------------------------*/
    if ((year % 400) == 0) {
        return 1;
    } else if ((year % 100) == 0) {
        return 0;
    } else if ((year % 4) == 0) {
        return 1;
    } else {
        return 0;
    }
}

/*
 * 功能:
 *     得到每个月有多少天
 * 参数:
 *     month:需要得到天数的月份数
 *     year:该月所对应的年份数
 *
 * 返回值:
 *     该月有多少天
 *
 */
unsigned char applib_dt_last_day_of_mon(unsigned char month, unsigned short year)
{
    /*----------------------------------------------------------------*/
    /* Local Variables                                                */
    /*----------------------------------------------------------------*/

    /*----------------------------------------------------------------*/
    /* Code Body                                                      */
    /*----------------------------------------------------------------*/
    if ((month == 0) || (month > 12)) {
        return g_day_per_mon[1] + applib_dt_is_leap_year(year);
    }

    if (month != 2) {
        return g_day_per_mon[month - 1];
    } else {
        return g_day_per_mon[1] + applib_dt_is_leap_year(year);
    }
}

/*
 * 功能:
 *     根据给定的日期得到对应的星期
 * 参数:
 *     year:给定的年份
 *     month:给定的月份
 *     day:给定的天数
 *
 * 返回值:
 *     对应的星期数,0 - 星期天 ... 6 - 星期六
 */
unsigned char applib_dt_dayindex(unsigned short year, unsigned char month, unsigned char day)
{
    char century_code, year_code, month_code, day_code;
    int week = 0;

    century_code = year_code = month_code = day_code = 0;

    if (month == 1 || month == 2) {
        century_code = (year - 1) / 100;
        year_code = (year - 1) % 100;
        month_code = month + 12;
        day_code = day;
    } else {
        century_code = year / 100;
        year_code = year % 100;
        month_code = month;
        day_code = day;
    }

    /* 根据蔡勒公式计算星期 */
    week = year_code + year_code / 4 + century_code / 4 - 2 * century_code + 26 * ( month_code + 1 ) / 10 + day_code - 1;
    week = week > 0 ? (week % 7) : ((week % 7) + 7);

    return week;
}

/*
 * 功能:
 *     根据UTC时间戳得到对应的日期
 * 参数:
 *     utc_sec:给定的UTC时间戳
 *     result:计算出的结果
 *     daylightSaving:是否是夏令时
 *
 * 返回值:
 *     无
 */
void utc_sec_2_mytime(unsigned int utc_sec, mytime_struct *result, bool daylightSaving)
{
    /*----------------------------------------------------------------*/
    /* Local Variables                                                */
    /*----------------------------------------------------------------*/
    int sec, day;
    unsigned short y;
    unsigned char m;
    unsigned short d;
    unsigned char dst;

    /*----------------------------------------------------------------*/
    /* Code Body                                                      */
    /*----------------------------------------------------------------*/

    if (daylightSaving) {
        utc_sec += SEC_PER_HOUR;
    }

    /* hour, min, sec */
    /* hour */
    sec = utc_sec % SEC_PER_DAY;
    result->nHour = sec / SEC_PER_HOUR;

    /* min */
    sec %= SEC_PER_HOUR;
    result->nMin = sec / SEC_PER_MIN;

    /* sec */
    result->nSec = sec % SEC_PER_MIN;

    /* year, month, day */
    /* year */
    /* year */
    day = utc_sec / SEC_PER_DAY;
    for (y = UTC_BASE_YEAR; day > 0; y++) {
        d = (DAY_PER_YEAR + applib_dt_is_leap_year(y));
        if (day >= d)
        {
            day -= d;
        }
        else
        {
            break;
        }
    }

    result->nYear = y;

    for (m = 1; m < MONTH_PER_YEAR; m++) {
        d = applib_dt_last_day_of_mon(m, y);
        if (day >= d) {
            day -= d;
        } else {
            break;
        }
    }

    result->nMonth = m;
    result->nDay = (unsigned char) (day + 1);
    /* 根据给定的日期得到对应的星期 */
    result->DayIndex = applib_dt_dayindex(result->nYear, result->nMonth, result->nDay);
}

/*
 * 功能:
 *     根据时间计算出UTC时间戳
 * 参数:
 *     currTime:给定的时间
 *     daylightSaving:是否是夏令时
 *
 * 返回值:
 *     UTC时间戳
 */
unsigned int mytime_2_utc_sec(mytime_struct *currTime, bool daylightSaving)
{
    /*----------------------------------------------------------------*/
    /* Local Variables                                                */
    /*----------------------------------------------------------------*/
    unsigned short i;
    unsigned int no_of_days = 0;
    int utc_time;
    unsigned char dst;

    /*----------------------------------------------------------------*/
    /* Code Body                                                      */
    /*----------------------------------------------------------------*/
    if (currTime->nYear < UTC_BASE_YEAR) {
        return 0;
    }

    /* year */
    for (i = UTC_BASE_YEAR; i < currTime->nYear; i++) {
        no_of_days += (DAY_PER_YEAR + applib_dt_is_leap_year(i));
    }

    /* month */
    for (i = 1; i < currTime->nMonth; i++) {
        no_of_days += applib_dt_last_day_of_mon((unsigned char) i, currTime->nYear);
    }

    /* day */
    no_of_days += (currTime->nDay - 1);

    /* sec */
    utc_time = (unsigned int) no_of_days * SEC_PER_DAY + (unsigned int) (currTime->nHour * SEC_PER_HOUR +
                                                                currTime->nMin * SEC_PER_MIN + currTime->nSec);

    if (dst && daylightSaving) {
        utc_time -= SEC_PER_HOUR;
    }

    return utc_time;
}

int main(int argc, char *argv[])
{
    mytime_struct my_time;
    unsigned int sec;
    char *DayIndex[] = {"Sun.", "Mon.", "Tues.", "Wed.", "Thur.", "Fri.", "Sat."};

    /* 这里根据UTC时间戳计算出来的时间是零时区的时间,所以如果要转化成北京时间就需要多加8小时 */
    utc_sec_2_mytime(1484537668 + 8 * SEC_PER_HOUR, &my_time, false);


    printf("%d-%d-%d %d:%d:%d %s\n", my_time.nYear, my_time.nMonth, my_time.nDay,
            my_time.nHour, my_time.nMin, my_time.nSec, DayIndex[my_time.DayIndex]);

    sec = mytime_2_utc_sec(&my_time, false);
    printf("sec = %d\n", sec);

    return 0;
}

查看打印:

2017-1-16 11:34:28 Mon.
sec = 1484566468

• 由 Leung 写于 2021 年 6 月 29 日

• 参考:UNIX时间戳和北京时间的相互转换
    C实现Unix时间戳和本地时间转化
    C语言实现时间戳转换_避免2038年时间溢出问题
    C语言实现将时间戳转换为年月日时分秒和将年月日时分秒转换为时间戳

你可能感兴趣的:(C语言应用(1)——Unix时间戳和北京时间的相互转换)