solidc
Robust collection of general-purpose cross-platform C libraries and data structures designed for rapid and safe development in C
Loading...
Searching...
No Matches
win_strptime.c
1#include <ctype.h>
2#include <stdbool.h>
3#include <string.h>
4
5#include "../include/win_strptime.h"
6
7// Windows does not have gmtime_r & localtime_r and completely lacks strptime.
8#if defined(_MSC_VER)
9static inline struct tm* gmtime_r(const time_t* timer, struct tm* buf) {
10 return (gmtime_s(buf, timer) == 0) ? buf : NULL;
11}
12
13static inline struct tm* localtime_r(const time_t* timer, struct tm* buf) {
14 return (localtime_s(buf, timer) == 0) ? buf : NULL;
15}
16
20static inline bool is_leap_year(int year) {
21 // year is tm_year (years since 1900)
22 int actual_year = year + 1900;
23 return (actual_year % 4 == 0 && actual_year % 100 != 0) || (actual_year % 400 == 0);
24}
25
29static inline int days_in_month(int month, int year) {
30 static const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
31 if (month < 0 || month > 11) {
32 return 0;
33 }
34 if (month == 1 && is_leap_year(year)) { // February in leap year
35 return 29;
36 }
37 return days[month];
38}
39
43static inline bool is_valid_date(int day, int month, int year) {
44 if (month < 0 || month > 11) {
45 return false;
46 }
47 if (day < 1) {
48 return false;
49 }
50 return day <= days_in_month(month, year);
51}
52
64char* strptime(const char* buf, const char* fmt, struct tm* tm) {
65 if (buf == NULL || fmt == NULL || tm == NULL) {
66 return NULL;
67 }
68
69 const char* s = buf;
70 const char* f = fmt;
71 bool is_pm = false;
72 bool has_ampm = false;
73 int century = -1; // For %C handling
74 bool need_date_validation = false;
75
76 // Initialize tm structure to safe defaults (POSIX requirement)
77 // Don't zero everything - preserve what caller may have set
78 // But ensure fields we might not set have safe values
79 if (tm->tm_wday == 0 && tm->tm_yday == 0 && tm->tm_isdst == 0) {
80 tm->tm_isdst = -1; // Unknown DST status
81 }
82
83 while (*f != '\0') {
84 // Handle whitespace: any whitespace in format matches zero or more in input
85 if (isspace((unsigned char)*f)) {
86 while (isspace((unsigned char)*f)) f++;
87 while (isspace((unsigned char)*s)) s++;
88 continue;
89 }
90
91 if (*f != '%') {
92 // Literal character match (case-sensitive)
93 if (*f != *s) {
94 return NULL;
95 }
96 f++;
97 s++;
98 continue;
99 }
100
101 // Format specifier
102 f++; // Skip '%'
103
104 if (*f == '\0') {
105 return NULL; // Trailing % is invalid
106 }
107
108 // Handle modifier flags (E and O)
109 bool has_modifier = false;
110 if (*f == 'E' || *f == 'O') {
111 has_modifier = true;
112 f++; // Skip modifier (we'll ignore it for basic implementation)
113 if (*f == '\0') {
114 return NULL;
115 }
116 }
117
118 switch (*f) {
119 case 'Y': { // 4-digit year
120 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1]) || !isdigit((unsigned char)s[2]) ||
121 !isdigit((unsigned char)s[3])) {
122 return NULL;
123 }
124 int year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
125 tm->tm_year = year - 1900;
126 s += 4;
127 break;
128 }
129
130 case 'y': { // 2-digit year (00-99)
131 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
132 return NULL;
133 }
134 int year = (s[0] - '0') * 10 + (s[1] - '0');
135 // POSIX: 69-99 -> 1969-1999, 00-68 -> 2000-2068
136 if (century >= 0) {
137 tm->tm_year = century * 100 + year - 1900;
138 } else {
139 tm->tm_year = (year >= 69) ? year : (year + 100);
140 }
141 s += 2;
142 break;
143 }
144
145 case 'C': { // Century (00-99)
146 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
147 return NULL;
148 }
149 century = (s[0] - '0') * 10 + (s[1] - '0');
150 s += 2;
151 break;
152 }
153
154 case 'm': { // Month (01-12) - zero-padded
155 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
156 return NULL;
157 }
158 int month = (s[0] - '0') * 10 + (s[1] - '0');
159 if (month < 1 || month > 12) {
160 return NULL;
161 }
162 tm->tm_mon = month - 1;
163 need_date_validation = true;
164 s += 2;
165 break;
166 }
167
168 case 'd': { // Day of month (01-31) - zero-padded
169 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
170 return NULL;
171 }
172 int day = (s[0] - '0') * 10 + (s[1] - '0');
173 if (day < 1 || day > 31) {
174 return NULL;
175 }
176 tm->tm_mday = day;
177 need_date_validation = true;
178 s += 2;
179 break;
180 }
181
182 case 'e': { // Day of month (1-31, space-padded)
183 // Skip leading whitespace/zero
184 if (*s == ' ' || *s == '0') {
185 s++;
186 }
187 if (!isdigit((unsigned char)s[0])) {
188 return NULL;
189 }
190 int day = (s[0] - '0');
191 s++;
192 if (isdigit((unsigned char)s[0])) {
193 day = day * 10 + (s[0] - '0');
194 s++;
195 }
196 if (day < 1 || day > 31) {
197 return NULL;
198 }
199 tm->tm_mday = day;
200 need_date_validation = true;
201 break;
202 }
203
204 case 'H': { // Hour (00-23) - zero-padded
205 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
206 return NULL;
207 }
208 int hour = (s[0] - '0') * 10 + (s[1] - '0');
209 if (hour > 23) {
210 return NULL;
211 }
212 tm->tm_hour = hour;
213 s += 2;
214 break;
215 }
216
217 case 'k': { // Hour (0-23) - space-padded
218 if (*s == ' ') {
219 s++;
220 }
221 if (!isdigit((unsigned char)s[0])) {
222 return NULL;
223 }
224 int hour = (s[0] - '0');
225 s++;
226 if (isdigit((unsigned char)s[0])) {
227 hour = hour * 10 + (s[0] - '0');
228 s++;
229 }
230 if (hour > 23) {
231 return NULL;
232 }
233 tm->tm_hour = hour;
234 break;
235 }
236
237 case 'I': { // Hour (01-12) - zero-padded
238 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
239 return NULL;
240 }
241 int hour = (s[0] - '0') * 10 + (s[1] - '0');
242 if (hour < 1 || hour > 12) {
243 return NULL;
244 }
245 tm->tm_hour = hour;
246 s += 2;
247 break;
248 }
249
250 case 'l': { // Hour (1-12) - space-padded
251 if (*s == ' ') {
252 s++;
253 }
254 if (!isdigit((unsigned char)s[0])) {
255 return NULL;
256 }
257 int hour = (s[0] - '0');
258 s++;
259 if (isdigit((unsigned char)s[0])) {
260 hour = hour * 10 + (s[0] - '0');
261 s++;
262 }
263 if (hour < 1 || hour > 12) {
264 return NULL;
265 }
266 tm->tm_hour = hour;
267 break;
268 }
269
270 case 'M': { // Minute (00-59) - zero-padded
271 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
272 return NULL;
273 }
274 int min = (s[0] - '0') * 10 + (s[1] - '0');
275 if (min > 59) {
276 return NULL;
277 }
278 tm->tm_min = min;
279 s += 2;
280 break;
281 }
282
283 case 'S': { // Second (00-60, allowing leap second) - zero-padded
284 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
285 return NULL;
286 }
287 int sec = (s[0] - '0') * 10 + (s[1] - '0');
288 if (sec > 60) { // Allow 60 for leap seconds
289 return NULL;
290 }
291 tm->tm_sec = sec;
292 s += 2;
293 break;
294 }
295
296 case 'p': { // AM/PM (case-insensitive per POSIX)
297 if ((s[0] == 'A' || s[0] == 'a') && (s[1] == 'M' || s[1] == 'm')) {
298 is_pm = false;
299 has_ampm = true;
300 s += 2;
301 } else if ((s[0] == 'P' || s[0] == 'p') && (s[1] == 'M' || s[1] == 'm')) {
302 is_pm = true;
303 has_ampm = true;
304 s += 2;
305 } else {
306 return NULL;
307 }
308 break;
309 }
310
311 case 'r': { // 12-hour time with AM/PM (%I:%M:%S %p)
312 char* result = strptime(s, "%I:%M:%S %p", tm);
313 if (result == NULL) {
314 return NULL;
315 }
316 s = result;
317 has_ampm = true; // Mark that AM/PM was handled
318 break;
319 }
320
321 case 'R': { // Time in HH:MM format
322 char* result = strptime(s, "%H:%M", tm);
323 if (result == NULL) {
324 return NULL;
325 }
326 s = result;
327 break;
328 }
329
330 case 'T': { // Time in HH:MM:SS format
331 char* result = strptime(s, "%H:%M:%S", tm);
332 if (result == NULL) {
333 return NULL;
334 }
335 s = result;
336 break;
337 }
338
339 case 'D': { // Date in MM/DD/YY format
340 char* result = strptime(s, "%m/%d/%y", tm);
341 if (result == NULL) {
342 return NULL;
343 }
344 s = result;
345 break;
346 }
347
348 case 'F': { // Date in YYYY-MM-DD format (ISO 8601)
349 char* result = strptime(s, "%Y-%m-%d", tm);
350 if (result == NULL) {
351 return NULL;
352 }
353 s = result;
354 break;
355 }
356
357 case 'b': // Abbreviated month name
358 case 'h': // Same as %b
359 case 'B': { // Full month name
360 static const char* months[] = {"january", "february", "march", "april", "may", "june",
361 "july", "august", "september", "october", "november", "december"};
362 static const char* abbr_months[] = {"jan", "feb", "mar", "apr", "may", "jun",
363 "jul", "aug", "sep", "oct", "nov", "dec"};
364
365 bool found = false;
366 // Try full names first, then abbreviations
367 for (int i = 0; i < 12; i++) {
368 const char* full = months[i];
369 const char* abbr = abbr_months[i];
370 size_t full_len = strlen(full);
371 size_t abbr_len = 3;
372
373 // For %B, match full name; for %b/%h, match abbreviation
374 if (*f == 'B') {
375 if (_strnicmp(s, full, full_len) == 0) {
376 tm->tm_mon = i;
377 s += full_len;
378 found = true;
379 break;
380 }
381 } else {
382 if (_strnicmp(s, abbr, abbr_len) == 0) {
383 tm->tm_mon = i;
384 s += abbr_len;
385 found = true;
386 break;
387 }
388 }
389 }
390 if (!found) {
391 return NULL;
392 }
393 need_date_validation = true;
394 break;
395 }
396
397 case 'a': // Abbreviated weekday name
398 case 'A': { // Full weekday name
399 static const char* weekdays[] = {"sunday", "monday", "tuesday", "wednesday",
400 "thursday", "friday", "saturday"};
401 static const char* abbr_weekdays[] = {"sun", "mon", "tue", "wed", "thu", "fri", "sat"};
402
403 bool found = false;
404 for (int i = 0; i < 7; i++) {
405 const char* full = weekdays[i];
406 const char* abbr = abbr_weekdays[i];
407 size_t full_len = strlen(full);
408 size_t abbr_len = 3;
409
410 if (*f == 'A') {
411 if (_strnicmp(s, full, full_len) == 0) {
412 tm->tm_wday = i;
413 s += full_len;
414 found = true;
415 break;
416 }
417 } else {
418 if (_strnicmp(s, abbr, abbr_len) == 0) {
419 tm->tm_wday = i;
420 s += abbr_len;
421 found = true;
422 break;
423 }
424 }
425 }
426 if (!found) {
427 return NULL;
428 }
429 break;
430 }
431
432 case 'j': { // Day of year (001-366) - zero-padded, 3 digits
433 if (!isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1]) || !isdigit((unsigned char)s[2])) {
434 return NULL;
435 }
436 int yday = (s[0] - '0') * 100 + (s[1] - '0') * 10 + (s[2] - '0');
437 if (yday < 1 || yday > 366) {
438 return NULL;
439 }
440 tm->tm_yday = yday - 1;
441 s += 3;
442 break;
443 }
444
445 case 'u': { // Weekday (1-7, Monday=1)
446 if (!isdigit((unsigned char)s[0])) {
447 return NULL;
448 }
449 int wday = s[0] - '0';
450 if (wday < 1 || wday > 7) {
451 return NULL;
452 }
453 tm->tm_wday = (wday == 7) ? 0 : wday; // Convert to 0-6 (Sunday=0)
454 s++;
455 break;
456 }
457
458 case 'w': { // Weekday (0-6, Sunday=0)
459 if (!isdigit((unsigned char)s[0])) {
460 return NULL;
461 }
462 int wday = s[0] - '0';
463 if (wday > 6) {
464 return NULL;
465 }
466 tm->tm_wday = wday;
467 s++;
468 break;
469 }
470
471 case 'z': { // Timezone offset (+hhmm or -hhmm or +hh:mm or -hh:mm)
472 // Consume the timezone but don't parse it into tm
473 // The caller (xtime_parse) should handle this separately
474 if (*s == '+' || *s == '-') {
475 s++; // Skip sign
476 // Consume hours (2 digits)
477 if (isdigit((unsigned char)s[0]) && isdigit((unsigned char)s[1])) {
478 s += 2;
479 } else {
480 return NULL;
481 }
482 // Optional colon
483 if (*s == ':') {
484 s++;
485 }
486 // Consume minutes (2 digits)
487 if (isdigit((unsigned char)s[0]) && isdigit((unsigned char)s[1])) {
488 s += 2;
489 } else {
490 return NULL;
491 }
492 } else if (*s == 'Z' || *s == 'z') {
493 s++; // UTC designator
494 } else {
495 return NULL;
496 }
497 break;
498 }
499
500 case 'Z': { // Timezone name - variable length
501 // Skip timezone name/abbreviation
502 while (*s != '\0' && !isspace((unsigned char)*s) && *s != '+' && *s != '-' &&
503 isalpha((unsigned char)*s)) {
504 s++;
505 }
506 if (s == buf) {
507 return NULL; // No timezone found
508 }
509 break;
510 }
511
512 case 'n': // Any whitespace
513 case 't': {
514 // Match one or more whitespace characters
515 if (!isspace((unsigned char)*s)) {
516 return NULL;
517 }
518 while (isspace((unsigned char)*s)) {
519 s++;
520 }
521 break;
522 }
523
524 case '%': { // Literal '%'
525 if (*s != '%') {
526 return NULL;
527 }
528 s++;
529 break;
530 }
531
532 default:
533 // Unsupported/unknown format specifier
534 return NULL;
535 }
536
537 f++;
538 }
539
540 // Apply AM/PM adjustment if needed (deferred from %I parsing)
541 if (has_ampm) {
542 if (is_pm && tm->tm_hour < 12) {
543 tm->tm_hour += 12;
544 } else if (!is_pm && tm->tm_hour == 12) {
545 tm->tm_hour = 0;
546 }
547 }
548
549 // Validate the date if month and day were parsed
550 if (need_date_validation && !is_valid_date(tm->tm_mday, tm->tm_mon, tm->tm_year)) {
551 return NULL; // Invalid date (e.g., Feb 30, Apr 31, etc.)
552 }
553
554 // Ensure DST flag is set to unknown if not explicitly set
555 if (tm->tm_isdst == 0) {
556 tm->tm_isdst = -1;
557 }
558
559 return (char*)s;
560}
561#endif