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