Ah, time. Yes, that gets quite complicated quite fast. The issue is mostly to do with leap days, actually. I am using an abstraction I call the GDN, or Gregorian Day Number (similar to Julian Day Number), which is the number of days since January 1, year 1 (which is a hypothetical date; it never happened; but it is a good abstraction).
In the Gregorian calendar, there are 365 days in a normal year, 366 days in a leap year. With the basis being year 1, the nice thing is that a leap year, if it occurs, will always be at the
end of a four-year cycle. And a normal four-year cycle will have 1461 days. In the Gregorian calendar, of course, all centuries that are not divisible by 4 end in a non-leap cycle. But we can deal with that one layer further up. A normal century has 76 normal years and 24 leap years, making a total of 36,524 days. A leap century of course has 36,525 days. And a cycle, a group of four centuries, has 146,097 days.
With these definitions out of the way, converting a calendar year to a GDN is quite simple. I am assuming the normal C struct tm here, because it is the standard type for this. This means you may have to convert the RTC time into this standard type before hand. That likely means subtracting 1900 from the year and 1 from the month. I am further assuming that you have a signed 64-bit type for time_t, because otherwise you get into trouble in January of 2038.
Code: Select all
/* returns the GDN of January 1 of a given year */
int year_to_gdn(int year, int *is_leap) {
/* year is the Gregorian calendar year */
assert(year >= 1); /* C integer math with negative numbers is weird. And you don't really work with Roman times, right? */
assert(year < INT_MAX/146097); /* otherwise this overflows the calculation down there */
year--; /* makes the upcoming calculations easier */
int cycle = year / 400;
int year_in_cycle = year % 400;
int century = year_in_cycle / 100;
int year_in_century = year_in_cycle % 100;
int group = year_in_century / 4;
int year_in_group = year_in_century % 4;
if (is_leap) *is_leap = year_in_group == 3 && (year_in_century != 99 || year_in_cycle == 399);
return 146097 * cycle + 36524 * century + 1461 * group + 365* year_in_group;
}
Now you only have to calculate the number of days since January 1, and you have the GDN of the day you wanted.
Code: Select all
int tm_to_gdn(const struct tm *tm) {
int is_leap;
static const unsigned short month_to_gdn[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int gdn = year_to_gdn(tm->tm_year + 1900, &is_leap);
gdn += month_to_gdn[tm->tm_mon] + (is_leap && tm->tm_mon > 1);
gdn += tm->tm_mday - 1;
return gdn;
}
And now with all of that out of the way, we do have to calculate the GDN of January 1, 1970. Well, 1970 is 4 cycles, 3 centuries, 17 groups and 1 year after the start of the calendar, so:
And with all of that out of the way, the remaining steps for converting the GDN to UNIX time and adding the time component as well are actually very simple:
Code: Select all
time_t tm_to_time(const struct tm *tm) {
int gdn = tm_to_gdn(tm);
return (gdn - GDN_EPOCH) * 86400LL + tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec;
}
Leap seconds you don't really need to care about. Your computer likely stores time as UTC (there are very very few machines out there storing time as TAI), which ignores leap seconds. Indeed, the standard UNIX time APIs cannot show a leap second at all. Time stamps are kept in UTC, which has no leap seconds, and converted to struct tm with no additional info.
As for time zones, I was assuming the struct tm was in UTC. If not, then you have to find the timezone offset somehow and subtract it from the result to get UTC, because time_t is supposed to contain the time as UTC.