Last active
August 28, 2025 21:52
-
-
Save skull-squadron/861d33ddb19b1781b0bd24864888d922 to your computer and use it in GitHub Desktop.
C++ O(1) epoch <-> datetime (requires floating point math)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/testcxx | |
/testc |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// FEATURES | |
// | |
// - Valid C++-14 and C99 | |
// - Only uses integer arithmetic | |
// - Essentially O(1) performance for all functions | |
// - (C++ only) constexpr functions for compile-time evaluation when compiling as C++ | |
// - Exhaustive testing of all functions | |
// | |
// BUGS | |
// | |
// - Doesn't handle leap seconds | |
// - Doesn't handle dates before 1970-01-01 00:00:00 | |
// - uint32_t epoch doesn't handle dates after 2106-02-07 06:28:15 | |
// - Doesn't check input dates are valid | |
// | |
#ifndef EPOCH_H | |
#define EPOCH_H | |
#ifdef __cplusplus | |
#define EPOCH_CONSTEXPR constexpr | |
#else | |
#define EPOCH_CONSTEXPR | |
#endif | |
#include <stdbool.h> | |
#include <stdint.h> | |
typedef enum DayOfWeek { | |
SUNDAY = 1, | |
MONDAY, | |
TUESDAY, | |
WEDNESDAY, | |
THURSDAY, | |
FRIDAY, | |
SATURDAY | |
} DayOfWeek; | |
// Result is not in half-day form | |
// y 1970..2106 | |
// m 1..12 | |
// d 1..31 | |
EPOCH_CONSTEXPR uint32_t | |
julianDate(uint16_t y, uint8_t m, uint8_t d) { | |
if (m < 3) --y; | |
m += (m < 3) ? 13 : 1; | |
return (36525*y)/100 - y/100 + y/400 + (306001*m)/10000 + d + 1720997; | |
} | |
// y 1970..2106 | |
// m 1..12 | |
// d 1..31 | |
// h 0..23 | |
// min 0..59 | |
// s 0..59 | |
EPOCH_CONSTEXPR uint32_t | |
dateTimeToEpoch(uint16_t y, uint8_t m, uint8_t d, uint8_t h, uint8_t min, uint8_t s) { | |
return 60 * (60 * (24 * (julianDate(y, m, d) - 2440588) + h) + min) + s; | |
} | |
// jd is the Julian Date | |
// y is the **year** part (NULL to not store) | |
// m is the **month** part (NULL to not store) | |
// d is the **day** part (NULL to not store) | |
EPOCH_CONSTEXPR void | |
dateFromJulian(uint32_t jd, uint16_t *y, uint8_t *m, uint8_t *d) { | |
uint8_t q = (4*jd - 17918)/146097; | |
uint32_t r = jd + q + (-q+3)/4 + 1486; | |
uint16_t s = (20*r - 2442)/7305; | |
uint16_t t = r - 365*s - s/4; | |
uint8_t u = (10000*t)/306001; | |
if (d) *d = t - (306001*u)/10000; | |
uint8_t mm = u - ((u > 13) ? 13 : 1); | |
if (m) *m = mm; | |
if (y) *y = s - ((mm > 2) ? 4716 : 4715); | |
} | |
// epoch is seconds since 1970-01-01 00:00:00 TA1/UTC | |
// y is the **year** part (NULL to not store) | |
// m is the **month** part (NULL to not store) | |
// d is the **day** part (NULL to not store) | |
// h is the **hour** part (NULL to not store) | |
// min is the **minute** part (NULL to not store) | |
// s is the **second** part (NULL to not store) | |
EPOCH_CONSTEXPR void | |
dateTimeFromEpoch(uint32_t epoch, uint16_t *y, uint8_t *m, uint8_t *d, uint8_t *h, uint8_t *min, uint8_t *s) { | |
if (s) *s = epoch % 60; epoch /= 60; | |
if (min) *min = epoch % 60; epoch /= 60; | |
if (h) *h = epoch % 24; epoch /= 24; | |
dateFromJulian(epoch + 2440588, y, m, d); | |
} | |
EPOCH_CONSTEXPR bool | |
isLeapYear(uint16_t y) { return ((y & 3) == 0 && y % 100 != 0) || (y % 400 == 0); } | |
// m 1..12 | |
// d 1..31 | |
EPOCH_CONSTEXPR DayOfWeek | |
toDayOfWeek(uint16_t y, uint8_t m, uint16_t d) { | |
const uint8_t t[12] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; | |
if (m < 3) --y; | |
return (DayOfWeek)((y + (y>>2) - y/100 + y/400 + t[m-1] + d) % 7 + 1); | |
} | |
// m 1..12 | |
EPOCH_CONSTEXPR uint8_t | |
daysInMonth(uint16_t m, bool leap_y) { | |
const uint8_t dm[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; | |
return dm[m-1] + ((m == 2 && leap_y) ? 1 : 0); | |
} | |
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CXX ?= c++ | |
CC ?= cc | |
TARGETS = testcxx testc | |
all: $(TARGETS) | |
testcxx: test.cpp epoch.h Makefile | |
$(CXX) -O3 -std=c++14 $(CXXFLAGS) -o $@ $< | |
./$@ | |
testc: test.cpp epoch.h Makefile | |
$(CC) -O3 -x c -std=c99 $(CFLAGS) -o $@ $< | |
./$@ | |
clean: | |
rm -f $(TARGETS) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "epoch.h" | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#define EXPECT(cond, message, ...) \ | |
if (!(cond)) { \ | |
fprintf(stderr, "ERROR: (%s == false) " message, #cond, ##__VA_ARGS__); \ | |
exit(1); \ | |
} | |
#define EXPECT_EQ(actual, expected, message, ...) \ | |
do { \ | |
int _a = (actual); \ | |
int _e = (expected); \ | |
if (_a != _e) { \ | |
fprintf(stderr, "ERROR: (%s (%d) != %s (%d))" message, #actual, _a, #expected, _e, ##__VA_ARGS__); \ | |
exit(1); \ | |
} \ | |
} while(0) | |
EPOCH_CONSTEXPR DayOfWeek nextDay(DayOfWeek d) { return (d == SATURDAY) ? SUNDAY : (DayOfWeek)(d+1); } | |
void testToDayOfWeek() { | |
printf("testToDayOfWeek\n"); | |
DayOfWeek expected = THURSDAY; // 1970-01-01 12:00:00 UTC | |
bool leap = isLeapYear(1970); | |
for (uint16_t y = 1970; y < 2400; ++y) { | |
leap = leap ? false : isLeapYear(y); | |
for (uint8_t m = 1; m <= 12; ++m) | |
for (uint8_t d = 1; d <= daysInMonth(m, leap); expected = nextDay(expected), ++d) | |
EXPECT_EQ(toDayOfWeek(y, m, d), expected, "%04u-%02u-%02u", y, m, d); | |
} | |
} | |
void testIsLeapYear() { | |
printf("testIsLeapYear\n"); | |
for (uint16_t y = 1970; y < 2400; ++y) { | |
bool expected = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)); | |
EXPECT_EQ(isLeapYear(y), expected, "y: %u", y); | |
} | |
} | |
void testJulianDate() { | |
printf("testJulianDate\n"); | |
uint32_t expected = 2440588; // 1970-01-01 12:00:00 UTC | |
bool leap = isLeapYear(1970); | |
for (uint16_t y = 1970; y < 2400; ++y) { | |
leap = leap ? false : isLeapYear(y); | |
for (uint8_t m = 1; m <= 12; ++m) | |
for (uint8_t d = 1; d <= daysInMonth(m, leap); ++d, ++expected) | |
EXPECT_EQ(julianDate(y, m, d), expected, "%04u-%02u-%02u", y, m, d); | |
} | |
} | |
void testDateFromJulian() { | |
printf("testDateFromJulian\n"); | |
uint16_t y; | |
uint8_t m, d; | |
for (uint32_t expected = 2440588; expected <= 2490625; ++expected) { | |
dateFromJulian(expected, &y, &m, &d); | |
uint32_t actual = julianDate(y, m, d); | |
EXPECT_EQ(actual, expected, "jd: %u -> %04u-%02u-%02u -> %u", expected, y, m, d, actual); | |
} | |
} | |
void testDateTimeToEpoch() { | |
printf("testDateTimeToEpoch\n"); | |
uint32_t expected = 0; | |
bool leap = isLeapYear(1970); | |
for (uint16_t y = 1970; y < 2400; ++y) { | |
leap = leap ? false : isLeapYear(y); | |
for (uint8_t m = 1; m <= 12; ++m) | |
for (uint8_t d = 1; d <= daysInMonth(m, leap); ++d) | |
for (uint8_t h = 0; h < 24; ++h) | |
for (uint8_t min = 0; min < 60; ++min) | |
for (uint8_t s = 0; s < 60; ++s, ++expected) | |
EXPECT_EQ(dateTimeToEpoch(y, m, d, h, min, s), expected, ""); | |
} | |
} | |
void testDateTimeFromEpoch() { | |
printf("testDateTimeFromEpoch"); | |
uint32_t epoch = 0; | |
struct { | |
uint16_t y; | |
uint8_t m, d, h, min, s; | |
} a, e; | |
bool leap = isLeapYear(1970); | |
for (e.y = 1970; e.y <= 2105; ++e.y) { | |
printf("\rtestDateTimeFromEpoch: year %u", e.y); | |
leap = leap ? false : isLeapYear(e.y); | |
for (e.m = 1; e.m <= 12; ++e.m) | |
for (e.d = 1; e.d <= daysInMonth(e.m, leap); ++e.d) | |
for (e.h = 0; e.h < 24; ++e.h) | |
for (e.min = 0; e.min < 60; ++e.min) | |
for (e.s = 0; e.s < 60; ++e.s, ++epoch) { | |
dateTimeFromEpoch(epoch, &a.y, &a.m, &a.d, &a.h, &a.min, &a.s); | |
EXPECT(memcmp(&a, &e, sizeof(a)) == 0, "epoch %u: expected: %04u-%02u-%02u %02u:%02u:%02u , actual: %04u-%02u-%02u %02u:%02u:%02u", epoch, | |
e.y, e.m, e.d, e.h, e.min, e.s, | |
a.y, a.m, a.d, a.h, a.min, a.s); | |
} | |
} | |
printf("\n"); | |
} | |
void testDaysInMonth() { | |
printf("testDaysInMonth\n"); | |
struct { | |
uint8_t month; | |
bool leap; | |
uint8_t expected; | |
} tests[] = { | |
{1, false, 31}, {1, true, 31}, | |
{2, false, 28}, {2, true, 29}, | |
{3, false, 31}, {3, true, 31}, | |
{4, false, 30}, {4, true, 30}, | |
{5, false, 31}, {5, true, 31}, | |
{6, false, 30}, {6, true, 30}, | |
{7, false, 31}, {7, true, 31}, | |
{8, false, 31}, {8, true, 31}, | |
{9, false, 30}, {9, true, 30}, | |
{10, false, 31}, {10, true, 31}, | |
{11, false, 30}, {11, true, 30}, | |
{12, false, 31}, {12, true, 31}, | |
}; | |
for (size_t i = 0; i < sizeof(tests)/sizeof(*tests); ++i) | |
EXPECT_EQ(daysInMonth(tests[i].month, tests[i].leap), tests[i].expected, "month: %u leap: %d", tests[i].month, tests[i].leap); | |
} | |
int main() { | |
setbuf(stdout, NULL); // stop buffering stdout | |
testJulianDate(); | |
testDateTimeToEpoch(); | |
testDateFromJulian(); | |
testDateTimeFromEpoch(); | |
testIsLeapYear(); | |
testToDayOfWeek(); | |
testDaysInMonth(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment