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