cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
civil_time.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
10
11/******************************************************************
12 * 1. Included files
13 ******************************************************************/
14
15#include "civil_time.h"
16
17#include <stdio.h> /* sscanf */
18#include <string.h> /* memcmp, strcmp */
19
20/******************************************************************
21 * 2. Internal helpers
22 ******************************************************************/
23
36static int64_t days_from_civil(int y, int m, int d)
37{
38 y -= (m <= 2) ? 1 : 0;
39
40 const int64_t era = ((y >= 0) ? y : (y - 399)) / 400;
41 const int64_t yoe = static_cast<int64_t>(y) - (era * 400); /* [0, 399] */
42 const int64_t doy = (((153 * (m + ((m > 2) ? -3 : 9))) + 2) / 5)
43 + d - 1; /* [0, 365] */
44 const int64_t doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy; /* [0, 146096] */
45
46 return (era * 146097) + doe - 719468;
47}
48
66static bool fields_sane(int year, int mon, int day, int hour, int min, int sec)
67{
68 return ((year >= 1970) && (year <= 9999) &&
69 (mon >= 1) && (mon <= 12) &&
70 (day >= 1) && (day <= 31) &&
71 (hour >= 0) && (hour <= 23) &&
72 (min >= 0) && (min <= 59) &&
73 (sec >= 0) && (sec <= 60)); /* 60: leap second */
74}
75
76/******************************************************************
77 * 3. Public API
78 ******************************************************************/
79
80int64_t civil_to_epoch(int year, int mon, int day, int hour, int min, int sec)
81{
82 const int64_t days = days_from_civil(year, mon, day);
83
84 return (days * 86400) + (static_cast<int64_t>(hour) * 3600) +
85 (static_cast<int64_t>(min) * 60) + sec;
86}
87
88int civil_month_from_abbrev(const char *mmm)
89{
90 static const char *const MONTHS = "JanFebMarAprMayJunJulAugSepOctNovDec";
91 int i;
92
93 if (mmm == NULL) {
94 return 0;
95 }
96
97 for (i = 0; i < 12; i++) {
98 if (memcmp(mmm, MONTHS + (i * 3), 3U) == 0) {
99 return i + 1;
100 }
101 }
102 return 0;
103}
104
105bool civil_parse_build_stamp(const char *date, const char *time_str,
106 int64_t *out)
107{
108 char mon_abbrev[4] = { 0 };
109 int day = 0;
110 int year = 0;
111 int hour = 0;
112 int min = 0;
113 int sec = 0;
114 int mon;
115
116 if ((date == NULL) || (time_str == NULL) || (out == NULL)) {
117 return false;
118 }
119
120 /* "%3s %d %d" absorbs the space-padded single-digit day ("Jul 9 2026")
121 * because a space in the format matches any run of whitespace. */
122 if (sscanf(date, "%3s %d %d", mon_abbrev, &day, &year) != 3) {
123 return false;
124 }
125 if (sscanf(time_str, "%d:%d:%d", &hour, &min, &sec) != 3) {
126 return false;
127 }
128
129 mon = civil_month_from_abbrev(mon_abbrev);
130 if ((mon == 0) || !fields_sane(year, mon, day, hour, min, sec)) {
131 return false;
132 }
133
134 *out = civil_to_epoch(year, mon, day, hour, min, sec);
135 return true;
136}
137
138bool civil_parse_http_date(const char *hdr, int64_t *out)
139{
140 char mon_abbrev[4] = { 0 };
141 char zone[8] = { 0 };
142 int day = 0;
143 int year = 0;
144 int hour = 0;
145 int min = 0;
146 int sec = 0;
147 int mon;
148
149 if ((hdr == NULL) || (out == NULL)) {
150 return false;
151 }
152
153 /* "Wed, 29 Jul 2026 15:14:09 GMT" — the day-of-week is skipped (%*3s),
154 * the zone is captured so that a non-conforming numeric offset is
155 * rejected instead of being silently treated as UTC. */
156 if (sscanf(hdr, "%*3s, %2d %3s %4d %2d:%2d:%2d %7s",
157 &day, mon_abbrev, &year, &hour, &min, &sec, zone) != 7) {
158 return false;
159 }
160 if (strcmp(zone, "GMT") != 0) {
161 return false;
162 }
163
164 mon = civil_month_from_abbrev(mon_abbrev);
165 if ((mon == 0) || !fields_sane(year, mon, day, hour, min, sec)) {
166 return false;
167 }
168
169 *out = civil_to_epoch(year, mon, day, hour, min, sec);
170 return true;
171}
172
174static int64_t weekday(int64_t days)
175{
176 const int64_t w = (days + 4) % 7; /* 1970-01-01 was a Thursday */
177 return (w < 0) ? (w + 7) : w;
178}
179
181static int64_t nth_sunday(int y, int m, int n)
182{
183 const int64_t d1 = days_from_civil(y, m, 1);
184 return d1 + ((7 - weekday(d1)) % 7) + (7 * (n - 1));
185}
186
188static int64_t last_sunday(int y, int m)
189{
190 const int64_t dl = days_from_civil(y, m + 1, 1) - 1;
191 return dl - weekday(dl);
192}
193
194int civil_local_offset_min(int64_t utc, int std_off_min, int rule)
195{
196 /* The year the local clock is in. Days from 1970 / 365.2425 can be a year
197 * out near New Year, so step it until Jan 1 of it brackets the day. */
198 const int64_t std_s = utc + (static_cast<int64_t>(std_off_min) * 60);
199 const int64_t day = (std_s >= 0) ? (std_s / 86400) : (((std_s + 1) / 86400) - 1);
200 int y = 1970 + static_cast<int>((day * 400) / 146097);
201 while (days_from_civil(y, 1, 1) > day) { y--; }
202 while (days_from_civil(y + 1, 1, 1) <= day) { y++; }
203
204 /* Transition instants in UTC seconds. The local ones are at 02:00 standard
205 * time, which is also 03:00 daylight time for the way back. */
206 const int64_t loc = (2 * 3600) - (static_cast<int64_t>(std_off_min) * 60);
207 int64_t on = 0;
208 int64_t off = 0;
209 switch (rule) {
210 case CIVIL_DST_EU:
211 on = (last_sunday(y, 3) * 86400) + 3600;
212 off = (last_sunday(y, 10) * 86400) + 3600;
213 break;
214 case CIVIL_DST_US:
215 on = (nth_sunday(y, 3, 2) * 86400) + loc;
216 off = (nth_sunday(y, 11, 1) * 86400) + loc - 3600;
217 break;
218 case CIVIL_DST_AU: /* southern: DST spans New Year, so on > off */
219 on = (nth_sunday(y, 10, 1) * 86400) + loc;
220 off = (nth_sunday(y, 4, 1) * 86400) + loc;
221 break;
222 case CIVIL_DST_NZ:
223 on = (last_sunday(y, 9) * 86400) + loc;
224 off = (nth_sunday(y, 4, 1) * 86400) + loc;
225 break;
226 default:
227 return std_off_min;
228 }
229 const bool dst = (on < off) ? ((utc >= on) && (utc < off))
230 : ((utc >= on) || (utc < off));
231 return std_off_min + (dst ? 60 : 0);
232}
bool civil_parse_http_date(const char *hdr, int64_t *out)
Parse an HTTP-date in IMF-fixdate form into an epoch value.
int civil_month_from_abbrev(const char *mmm)
Map a three-letter English month abbreviation to 1–12.
bool civil_parse_build_stamp(const char *date, const char *time_str, int64_t *out)
Parse the compiler's DATE / TIME pair into an epoch value.
int64_t civil_to_epoch(int year, int mon, int day, int hour, int min, int sec)
Convert a UTC civil date/time to seconds since the Unix epoch.
static int64_t nth_sunday(int y, int m, int n)
Day count of the n th Sunday of a month (n >= 1).
static bool fields_sane(int year, int mon, int day, int hour, int min, int sec)
Range-check a parsed civil date/time.
static int64_t days_from_civil(int y, int m, int d)
Days from 1970-01-01 to the given proleptic-Gregorian civil date.
static int64_t weekday(int64_t days)
Day of week of a day count from days_from_civil(), 0 = Sunday.
int civil_local_offset_min(int64_t utc, int std_off_min, int rule)
Offset from UTC in force at utc, DST included.
static int64_t last_sunday(int y, int m)
Day count of the last Sunday of a month (m <= 11).
TZ-independent calendar arithmetic and date-string parsing.
@ CIVIL_DST_NZ
Definition civil_time.h:104
@ CIVIL_DST_US
Definition civil_time.h:102
@ CIVIL_DST_EU
Definition civil_time.h:101
@ CIVIL_DST_AU
Definition civil_time.h:103