1 #include <stdio.h>
2 #include <string.h>
3 #include <unistd.h>
4 #include <ctype.h>
5 #include <sys/time.h>
6 #include <sys/stat.h>
7 #include "lib/mlr_globals.h"
8 #include "lib/mlr_arch.h"
9 #include "lib/mlrutil.h"
10 #include "lib/mlrdatetime.h"
11 #include "lib/string_builder.h"
12
13 // NZ since this isn't counting the null terminator.
14 #define NZBUFLEN 63
15
16 // ----------------------------------------------------------------
17 // seconds since the epoch
get_systime()18 double get_systime() {
19 struct timeval tv = { .tv_sec = 0, .tv_usec = 0 };
20 (void)gettimeofday(&tv, NULL);
21 return (double)tv.tv_sec + (double)tv.tv_usec * 1e-6;
22 }
23
24 // ----------------------------------------------------------------
25 // The essential idea is that we use the library function gmtime to get a struct tm, then strftime
26 // to produce a formatted string. The only complication is that we support "%1S" through "%9S" for
27 // formatting the seconds with a desired number of decimal places.
28
mlr_alloc_time_string_from_seconds(double seconds_since_the_epoch,char * format_string,timezone_handling_t timezone_handling)29 char* mlr_alloc_time_string_from_seconds(double seconds_since_the_epoch, char* format_string,
30 timezone_handling_t timezone_handling)
31 {
32
33 // 1. Split out the integer seconds since the epoch, which the stdlib can handle, and
34 // the fractional part, which it cannot.
35 time_t iseconds = (time_t) seconds_since_the_epoch;
36 double fracsec = seconds_since_the_epoch - iseconds;
37
38 struct tm tm;
39 switch(timezone_handling) {
40 case TIMEZONE_HANDLING_GMT:
41 tm = *gmtime(&iseconds); // No gmtime_r on Windows so just use gmtime.
42 break;
43 case TIMEZONE_HANDLING_LOCAL:
44 tm = *localtime(&iseconds); // No gmtime_r on Windows so just use gmtime.
45 break;
46 default:
47 fprintf(stderr, "%s: internal coding error detected in file %s at line %d.\n",
48 MLR_GLOBALS.bargv0, __FILE__, __LINE__);
49 exit(1);
50 break;
51 }
52
53 // 2. See if "%nS" (for n in 1..9) is a substring of the format string.
54 char* middle_nS_format = NULL;
55 char* right_subformat = NULL;
56 int n = 0; // Save off n for round-up handling below.
57 for (char* p = format_string; *p; p++) {
58 // We can't use strstr since we're searching for a pattern, and regexes are overkill.
59 // Here we rely on left-to-right evaluation of the boolean expressions, with non-evaluation
60 // of a subexpression if a subexpression to its left is false (this keeps us from reading
61 // past end of input string).
62 if (p[0] == '%' && p[1] >= '1' && p[1] <= '9' && p[2] == 'S') {
63 middle_nS_format = p;
64 right_subformat = &p[3];
65 n = p[1] - '0';
66 break;
67 }
68 }
69
70 // 3. If "%nS" (for n in 1..9) is not a substring of the format string, just use strftime.
71 if (middle_nS_format == NULL) {
72 char* output_string = mlr_malloc_or_die(NZBUFLEN+1);
73 int written_length = strftime(output_string, NZBUFLEN, format_string, &tm);
74 if (written_length > NZBUFLEN || written_length == 0) {
75 fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
76 MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
77 exit(1);
78 }
79
80 return output_string;
81 }
82
83 // Now we know "%nS" (for n in 1..9) is a substring of the format string. Idea is to
84 // copy the subformats to the left and right of the %nS part and format them both using
85 // strftime, and format the middle part ourselves using sprintf. Then concatenate all
86 // those pieces.
87
88 // 5. Find the left-of-%nS subformat, and format the input using that.
89 int left_subformat_length = middle_nS_format - format_string;
90 char* left_subformat = mlr_malloc_or_die(left_subformat_length + 1);
91 memcpy(left_subformat, format_string, left_subformat_length);
92 left_subformat[left_subformat_length] = 0;
93
94 char left_formatted[NZBUFLEN+1];
95 if (*left_subformat == 0) {
96 // There's nothing to the left of %nS. strftime will error on empty format string, so we can
97 // just map empty format to empty result ourselves.
98 *left_formatted = 0;
99 } else {
100 int written_length = strftime(left_formatted, NZBUFLEN, left_subformat, &tm);
101 if (written_length > NZBUFLEN || written_length == 0) {
102 fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
103 MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
104 exit(1);
105 }
106 }
107 free(left_subformat);
108
109 // 6. There are two parts in the middle: the integer part which strftime can populate
110 // from the struct tm, using %S format, and the fractional-seconds part which we sprintf.
111 // First do the int part.
112 char middle_int_formatted[NZBUFLEN+1];
113 char* middle_int_format = "%S";
114 int written_length = strftime(middle_int_formatted, NZBUFLEN, middle_int_format, &tm);
115 if (written_length > NZBUFLEN || written_length == 0) {
116 fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
117 MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
118 exit(1);
119 }
120
121 // 7. Do the fractional-seconds part. One key point is that sprintf always writes a leading zero,
122 // e.g. .123456 becomes "0.123456". We'll take off the leading zero later.
123 char middle_sprintf_format[] = "%.xlf";
124 char middle_fractional_formatted[16];
125 // "%6S" maps to "%.6lf" and so on. We found the middle_nS_format by searching for "%nS" for
126 // n in 1..9 so sprintf-format subscript 2 is the same as strftime format subscript 1.
127 middle_sprintf_format[2] = middle_nS_format[1];
128 sprintf(middle_fractional_formatted, middle_sprintf_format, fracsec);
129
130 // 8. Format the right-of-%nS part, also using strftime.
131 char right_formatted[NZBUFLEN];
132 if (*right_subformat == 0) {
133 // There's nothing to the right of %nS. strftime will error on empty format string, so we can
134 // just map empty format to empty result ourselves.
135 *right_formatted = 0;
136 } else {
137 int written_length = strftime(right_formatted, NZBUFLEN, right_subformat, &tm);
138 if (written_length > NZBUFLEN || written_length == 0) {
139 fprintf(stderr, "%s: could not strftime(%lf, \"%s\"). See \"%s --help-function strftime\".\n",
140 MLR_GLOBALS.bargv0, seconds_since_the_epoch, format_string, MLR_GLOBALS.bargv0);
141 exit(1);
142 }
143 }
144
145 // 9. Concatenate the output. For string_builder, the size argument is just an initial size;
146 // it can realloc beyond that initial estimate if necessary.
147 string_builder_t* psb = sb_alloc(NZBUFLEN+1);
148 sb_append_string(psb, left_formatted);
149 sb_append_string(psb, middle_int_formatted);
150 // When the input has fractional seconds like 0.999999 and the format is shorter than that,
151 // e.g. "%3S", there can be round-up to 1.0 on the sprintf.
152 if (middle_fractional_formatted[0] == '1') {
153 sb_append_char(psb, '.');
154 for (int i = 0; i < n; i++)
155 sb_append_char(psb, '9');
156 } else if (middle_fractional_formatted[0] == '0') {
157 sb_append_string(psb, &middle_fractional_formatted[1]);
158 } else {
159 MLR_INTERNAL_CODING_ERROR();
160 }
161 sb_append_string(psb, right_formatted);
162 char* output_string = sb_finish(psb);
163 sb_free(psb);
164
165 return output_string;
166 }
167
168 // ----------------------------------------------------------------
169 // Miller supports fractional seconds in the input string, but strptime doesn't. So we have
170 // to play some tricks, inspired in part by some ideas on StackOverflow. Special shout-out
171 // to @tinkerware on Github for the push in the right direction! :)
172
mlr_seconds_from_time_string(char * time_string,char * format_string,timezone_handling_t timezone_handling)173 double mlr_seconds_from_time_string(char* time_string, char* format_string,
174 timezone_handling_t timezone_handling)
175 {
176 struct tm tm;
177
178 // 1. Just try strptime on the input as-is and return quickly if it's OK.
179 memset(&tm, 0, sizeof(tm));
180 char* strptime_retval = mlr_arch_strptime(time_string, format_string, &tm);
181 if (strptime_retval != NULL) {
182 if (*strptime_retval != 0) { // Extraneous stuff in the input not matching the format
183 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
184 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
185 exit(1);
186 }
187
188 // printf("TIME_STRING %s\n", time_string);
189 // printf("FORMAT_STRING %s\n", time_string);
190 // printf("tm_year =%d\n", tm.tm_year);
191 // printf("tm_mon =%d\n", tm.tm_mon);
192 // printf("tm_mday =%d\n", tm.tm_mday);
193 // printf("tm_wday =%d\n", tm.tm_wday);
194 // printf("tm_yday =%d\n", tm.tm_yday);
195 // printf("tm_hour =%d\n", tm.tm_hour);
196 // printf("tm_min =%d\n", tm.tm_min);
197 // printf("tm_sec =%d\n", tm.tm_sec);
198 // printf("tm_isdst =%d\n", tm.tm_isdst);
199
200 return (double)mlr_arch_timegmlocal(&tm, timezone_handling);
201 }
202
203 // 2. Now either there's floating-point seconds in the input, or something else is wrong.
204 // First look for "%S" in the format string, for two reasons: (a) if there isn't "%S"
205 // then something else is wrong; (b) if there is, we'll need that to split the format
206 // string.
207 char* pS = strstr(format_string, "%S");
208 if (pS == NULL) {
209 // strptime failure couldn't have been because of floating-point-seconds stuff. No
210 // reason to try any harder.
211 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
212 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
213 exit(1);
214 }
215
216 // There's "%S" in the format string, and the input has fractional seconds matching that
217 // and no other problems, or there's something else wrong.
218 //
219 // Running example as we work through the rest:
220 // * Input is "2017-04-09T00:51:09.123456 TZBLAHBLAH"
221 // * Format is "%Y-%m-%dT%H:%M:%S TZBLAHBLAH"
222
223 // 3. Copy the format up to the %S but with nothing else after. This is temporary to help us locate
224 // the fractional-seconds part of the input string.
225 // Example temporary format: "%Y-%m-%dT%H:%M:%S"
226
227 int truncated_format_string_length = pS - format_string + 2; // 2 for the "%S"
228 char* truncated_format_string = mlr_malloc_or_die(truncated_format_string_length + 1);
229 memcpy(truncated_format_string, format_string, truncated_format_string_length);
230 truncated_format_string[truncated_format_string_length] = 0;
231
232 // 4. strptime using that truncated format and ignore the tm. Only look at the string return value.
233 // Example return value: ".123456 TZBLAHBLAH"
234 strptime_retval = mlr_arch_strptime(time_string, truncated_format_string, &tm);
235 if (strptime_retval == NULL) {
236 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
237 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
238 exit(1);
239 }
240 free(truncated_format_string);
241
242 // 5. strtod the return value to find the fractional-seconds part, and whatever's after.
243 // Example fractional-seconds part: ".123456"
244 // Example stuff after: " TZBLAHBLAH"
245 char* stuff_after = NULL;
246 double fractional_seconds;
247 if (strptime_retval[0] == '.' && !isdigit(strptime_retval[1])) {
248 // If they had just a decimal point with no digits after, e.g. "10." seconds rather than "10.0",
249 // that's OK, but strtod won't parse that as double.
250 fractional_seconds = 0.0;
251 stuff_after = &strptime_retval[1];
252 } else {
253 fractional_seconds = strtod(strptime_retval, &stuff_after);
254 if (stuff_after == strptime_retval) {
255 // Non-parseable
256 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
257 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
258 exit(1);
259 }
260 }
261
262 // 6. Make a copy of the input string with the fractional seconds elided.
263 // Example: "2017-04-09T00:51:09 TZBLAHBLAH"
264 char* elided_fraction_input = mlr_malloc_or_die(strlen(time_string) + 1);
265 int input_length_up_to_fractional_seconds = strptime_retval - time_string;
266 memcpy(elided_fraction_input, time_string, input_length_up_to_fractional_seconds);
267 strcpy(&elided_fraction_input[input_length_up_to_fractional_seconds], stuff_after);
268
269 // 7. strptime the elided-fraction input string using the original format string. (This is like
270 // the input never had fractional seconds in the first place.) Get the tm parsed from that.
271 memset(&tm, 0, sizeof(tm));
272 strptime_retval = mlr_arch_strptime(elided_fraction_input, format_string, &tm);
273 if (strptime_retval == NULL) {
274 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
275 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
276 exit(1);
277 }
278 if (*strptime_retval != 0) { // Extraneous stuff in the input not matching the format
279 fprintf(stderr, "%s: could not strptime(\"%s\", \"%s\"). See \"%s --help-function strptime\".\n",
280 MLR_GLOBALS.bargv0, time_string, format_string, MLR_GLOBALS.bargv0);
281 exit(1);
282 }
283 free(elided_fraction_input);
284
285 // 8. Convert the tm to a time_t (seconds since the epoch) and then add the fractional seconds.
286 return mlr_arch_timegmlocal(&tm, timezone_handling) + fractional_seconds;
287 }
288