1 /*******************************************************************************
2 
3     Handler for time interval CLI arguments
4 
5     One Argument
6     ------------
7 
8     Date:
9         Gives the range for that day.
10         "-t 2019-04-01" (start: 2019-04-01 00:00:00, end: 2019-04-01 23:59:59 )
11 
12     TimeStamp
13         Gives a range from the provided timestamp plus one day.
14         "-t 1554132392" (start: 1554132392, end: 1554218791)
15 
16     Timerange
17         Gives X amount in the past NOT including the current second.
18         "-t 1m" with now:1000 (start:940, end:999)
19 
20     Two Arguments
21     -------------
22 
23     Dates:
24         Start is parsed inclusively, end is parsed exclusively
25         eg. -t "2019-04-01 2019-04-02"
26             (start: 2019-04-01 00:00:00)
27             (  end: 2019-04-02 23:59:59)
28 
29     TimeStamp
30         Start is treated as is, end is NOT included in final range.
31         eg. -t 100 200 = (begin: 100, end: 199)
32 
33     Timerange
34         End range includes the starting second.
35         eg. -t 100 1m = (begin: 100, end: 159)
36         eg. -t 1m 100 = (begin: 60, end: 99)
37 
38     Intended Range Usage
39         for ( long i = range.begin; i <= range.end; ++i )
40 
41     copyright: Copyright (c) 2018 dunnhumby Germany GmbH. All rights reserved
42 
43 *******************************************************************************/
44 
45 module ocean.text.arguments.TimeIntervalArgs;
46 
47 import core.stdc.time;
48 
49 import ocean.core.Enforce;
50 import ocean.text.Arguments;
51 import ocean.text.convert.DateTime;
52 import ocean.text.convert.Integer;
53 import ocean.meta.types.Qualifiers;
54 
55 version (unittest)
56 {
57     import Test = ocean.core.Test;
58 }
59 
60 /// Structure reoresenting a Timestamp interval
61 struct TimestampInterval
62 {
63     /// The timestamp for the beginning of the interval
64     long begin;
65 
66     /// The timestamp for the end of the interval
67     long end;
68 }
69 
70 /// Number of seconds in the day for creating day ranges.
71 static immutable SECONDS_IN_DAY = 86_400;
72 
73 /***************************************************************************
74 
75     Add application-specific command line arguments for the selecting a
76     time interval
77 
78     Params:
79         args = The arguments to add to
80         required = True if the fields are mandatory
81 
82 ***************************************************************************/
83 
84 public void setupTimeIntervalArgs ( Arguments args, bool required )
85 {
86     auto interval = args("time-interval")
87         .aliased('t')
88         .params(1, 2)
89         .help("Specify a time interval. It 1 or 2 values, where they can be:\n\t" ~
90             "'now' an alias for the current timestamp\n\t" ~
91             "'yesterday' an alias for yesterdays date\n\t" ~
92             "an integer for unix timestamps\n\t" ~
93             "a time duration `{int}m`. Supported units are [m]inutes, [h]ours and [d]ays\n\t" ~
94             "an iso1806 date (YYYY-MM-DD)");
95     args("time-interval-exclude")
96         .help("If not set, then a range of '-t 2019-04-01 2019-04-02' will \n" ~
97               "include the end date's data. eg. (end: 2019-04-02 23:59:59) \n" ~
98               "If set, end date data will not be included, eg.(end: 2019-04-01 23:59:59)");
99 
100     if ( required )
101     {
102         interval.required();
103     }
104 }
105 
106 /***************************************************************************
107 
108     Validate the time-interval argument
109 
110     Params:
111         app = The application
112         args = The arguments to add to
113 
114     Returns:
115         The error message, if any argument failed to validate
116 
117 ***************************************************************************/
118 
119 public istring validateTimeIntervalArgs ( Arguments args )
120 {
121     auto num_args = args["time-interval"].assigned.length;
122     if ( num_args < 1 || num_args > 2 )
123     {
124         return "The 'time-interval' argument must have 1 or 2 values";
125     }
126 
127     return null;
128 }
129 
130 /***************************************************************************
131 
132     Process the time-interval argument
133 
134     Params:
135         args = The arguments to process
136 
137 ***************************************************************************/
138 
139 public TimestampInterval processTimeIntervalArgs ( Arguments args )
140 {
141     auto num_args = args["time-interval"].assigned.length;
142     enforce(num_args >= 1 && num_args <= 2, "Not enough arguments provided");
143     bool include_end_date = !args["time-interval-exclude"].set;
144 
145     long begin, end;
146 
147     if ( num_args == 1 )
148     {
149         auto arg = args["time-interval"].assigned[0];
150         if ( isTimeInterval(arg) )
151         {
152             end = time(null);
153             begin = end - parseTimeInterval(arg);
154         }
155         else
156         {
157             begin = parseDateString(arg);
158             end = begin + SECONDS_IN_DAY;
159         }
160     }
161     else if ( num_args == 2 )
162     {
163         cstring begin_arg = args["time-interval"].assigned[0];
164         cstring end_arg = args["time-interval"].assigned[1];
165 
166         if ( isTimeInterval(begin_arg) && isTimeInterval(end_arg) )
167         {
168             begin = time(null) - parseTimeInterval(begin_arg);
169             end = time(null) + parseTimeInterval(end_arg);
170         }
171         else if ( isTimeInterval(begin_arg) )
172         {
173             end = parseDateString(end_arg, include_end_date);
174             begin = end - parseTimeInterval(begin_arg);
175         }
176         else if ( isTimeInterval(end_arg) )
177         {
178             begin = parseDateString(begin_arg);
179             end = begin + parseTimeInterval(end_arg);
180         }
181         else
182         {
183             begin = parseDateString(begin_arg);
184             end = parseDateString(end_arg, include_end_date);
185         }
186     }
187     //Don't include the last second in the range.
188     return TimestampInterval(begin, end-1);
189 }
190 
191 /**************************************************************************
192 
193     Converts a string interval to a unix timestamp. If the value is an
194     ISO 8601 date. If the value is a stirng date and from the end of the range
195     then we say the value is actually the next day
196 
197     Params:
198         value = `now`, "yesterday", a string timestamp or iso8601 date
199         include_end_date = For string dates, actually parse the next day to
200                            include the dates data.
201 
202     Returns:
203         an unix timestamp
204 
205 **************************************************************************/
206 
207 private long parseDateString ( cstring value, bool include_end_date = false )
208 {
209     if ( value == "now" )
210     {
211         return time(null);
212     }
213 
214     if ( value == "yesterday" )
215     {
216         auto cur_time = time(null) - SECONDS_IN_DAY;
217         return cur_time - (cur_time % SECONDS_IN_DAY);
218     }
219 
220     long timestamp;
221     if ( toLong(value, timestamp) )
222     {
223         return timestamp;
224     }
225 
226     long result;
227     DateConversion dummy_conv;
228     if ( timeToUnixTime(value, result, dummy_conv) )
229     {
230         return result + (include_end_date ? SECONDS_IN_DAY : 0);
231     }
232 
233     enforce(isTimeInterval(value), cast(istring) ("`" ~ value ~ "` is an invalid time interval argument. " ~
234         "Only `now`, unix timestamp, iso8601 date and durations are permited."));
235 
236     return 0;
237 }
238 
239 /// Timestamp to use in unit tests;
240 version (unittest)
241 {
242     // 2019-04-01 15:26:32
243     static immutable TEST_TIME_NOW = 1554132392;
244 
245     /***************************************************************************
246 
247         Params
248             _unused = Unused.
249 
250         Returns:
251             Fake "now" time to use within the unit tests.
252 
253     ***************************************************************************/
254 
255     private time_t time ( tm* _unused )
256     {
257         return TEST_TIME_NOW;
258     }
259 }
260 
261 /// Check the date arguments setup
262 unittest
263 {
264     long dummy_time;
265     DateConversion dummy_conv;
266     TimestampInterval interval;
267 
268     auto args = new Arguments;
269 
270     /// When there is no begin and end date set
271     setupTimeIntervalArgs(args, false);
272     args.parse("");
273     Test.testThrown!(Exception)(processTimeIntervalArgs(args));
274 
275     /// When the begin date is provided
276     args = new Arguments;
277 
278     setupTimeIntervalArgs(args, false);
279     args.parse("--time-interval 2014-03-09 now");
280     interval = processTimeIntervalArgs(args);
281     timeToUnixTime("2014-03-09 00:00:00", dummy_time, dummy_conv);
282 
283     Test.test!("==")(interval.begin, dummy_time);
284     Test.test!("==")(interval.end, TEST_TIME_NOW - 1);
285 
286     /// When the end date is provided
287     args = new Arguments;
288 
289     setupTimeIntervalArgs(args, false);
290     args.parse("--time-interval now 2014-04-09");
291     interval = processTimeIntervalArgs(args);
292     timeToUnixTime("2014-04-09 23:59:59", dummy_time, dummy_conv);
293 
294     Test.test!("==")(interval.begin, time(null));
295     Test.test!("==")(interval.end, dummy_time);
296 
297     /// When the end date is provided with exclusion arg
298     args = new Arguments;
299 
300     setupTimeIntervalArgs(args, false);
301     args.parse("--time-interval now 2014-04-09 --time-interval-exclude");
302     interval = processTimeIntervalArgs(args);
303     timeToUnixTime("2014-04-08 23:59:59", dummy_time, dummy_conv);
304 
305     Test.test!("==")(interval.begin, time(null));
306     Test.test!("==")(interval.end, dummy_time);
307 
308     /// When both dates are provided
309     args = new Arguments;
310 
311     setupTimeIntervalArgs(args, false);
312     args.parse("--time-interval 2014-03-09 2014-03-09");
313     interval = processTimeIntervalArgs(args);
314 
315     timeToUnixTime("2014-03-09 00:00:00", dummy_time, dummy_conv);
316     Test.test!("==")(interval.begin, dummy_time);
317 
318     timeToUnixTime("2014-03-09 23:59:59", dummy_time, dummy_conv);
319     Test.test!("==")(interval.end, dummy_time);
320 
321     /// Using timestamps
322     args = new Arguments;
323 
324     setupTimeIntervalArgs(args, false);
325     args.parse("--time-interval 1000 1200");
326     interval = processTimeIntervalArgs(args);
327 
328     Test.test!("==")(interval.begin, 1000);
329     Test.test!("==")(interval.end, 1199);
330 
331     /// Using interval starting with time duration
332     args = new Arguments;
333 
334     setupTimeIntervalArgs(args, false);
335     args.parse("--time-interval 1m 1000");
336     interval = processTimeIntervalArgs(args);
337 
338     Test.test!("==")(interval.begin, 940);
339     Test.test!("==")(interval.end, 999);
340 
341     /// Using interval ending with time duration
342     args = new Arguments;
343 
344     setupTimeIntervalArgs(args, false);
345     args.parse("--time-interval 1000 1m");
346     interval = processTimeIntervalArgs(args);
347 
348     Test.test!("==")(interval.begin, 1000);
349     Test.test!("==")(interval.end, 1059);
350 
351     /// Using two time durations
352     args = new Arguments;
353 
354     setupTimeIntervalArgs(args, false);
355     args.parse("--time-interval 1m 1m");
356     interval = processTimeIntervalArgs(args);
357 
358     Test.test!("==")(interval.begin, TEST_TIME_NOW - 60);
359     Test.test!("==")(interval.end, TEST_TIME_NOW + 59);
360 
361     /// Using an invalid argument
362     args = new Arguments;
363 
364     setupTimeIntervalArgs(args, false);
365     args.parse("--time-interval invalid 1m");
366     Test.testThrown!(Exception)(processTimeIntervalArgs(args));
367 
368     /// Test range for all of "yesterday"
369     args = new Arguments;
370 
371     setupTimeIntervalArgs(args, false);
372     args.parse("--time-interval yesterday");
373     interval = processTimeIntervalArgs(args);
374 
375     /// 03/31/2019 @ 12:00am(UTC)
376     Test.test!("==")(interval.begin, 1553990400);
377     /// 03/31/2019 @ 11:59pm (UTC)
378     Test.test!("==")(interval.end, 1554076799);
379 
380     /// Test last 2 hours of data receiving the last 2 hours of data.
381     args = new Arguments;
382 
383     setupTimeIntervalArgs(args, false);
384     args.parse("--time-interval 2h");
385     interval = processTimeIntervalArgs(args);
386 
387     /// 2019-04-01 13:26:32
388     Test.test!("==")(interval.begin, 1554125192);
389     /// 2019-04-01 15:26:31
390     Test.test!("==")(interval.end, 1554132391);
391 }
392 
393 /******************************************************************************
394 
395     Converts a string to seconds
396 
397     Params:
398         value = a string containing an integer and the first leter of a time
399             unit. The supported units are minutes, hours and days.
400 
401     Returns:
402         The interval in seconds
403 
404 ******************************************************************************/
405 
406 public long parseTimeInterval ( cstring value )
407 {
408     long result;
409 
410     if ( value.length < 2 || !toLong(value[0..$-1], result) || result == 0 )
411     {
412         throw new Exception("The provided time interval has an invalid value.");
413     }
414 
415     char unit = value[value.length - 1];
416     switch ( unit )
417     {
418         case 's':
419             break;
420 
421         case 'm':
422             result *= 60;
423             break;
424 
425         case 'h':
426             result *= 3600;
427             break;
428 
429         case 'd':
430             result *= 3600 * 24;
431             break;
432 
433         default:
434             throw new Exception("The provided time interval has an invalid unit.");
435     }
436 
437     return result;
438 }
439 
440 ///
441 unittest
442 {
443     Test.test!("==")(parseTimeInterval("1s"), 1);
444     Test.test!("==")(parseTimeInterval("1m"), 60);
445     Test.test!("==")(parseTimeInterval("2m"), 120);
446     Test.test!("==")(parseTimeInterval("1h"), 3_600);
447     Test.test!("==")(parseTimeInterval("2h"), 7_200);
448     Test.test!("==")(parseTimeInterval("1d"), 3600 * 24);
449     Test.test!("==")(parseTimeInterval("2d"), 3600 * 48);
450 
451     Test.testThrown!(Exception)(parseTimeInterval(""), false);
452     Test.testThrown!(Exception)(parseTimeInterval("0s"), false);
453     Test.testThrown!(Exception)(parseTimeInterval("2x"), false);
454     Test.testThrown!(Exception)(parseTimeInterval("1xm"), false);
455 }
456 
457 
458 /******************************************************************************
459 
460     Checks if a string can be converted to a time interval
461 
462     Params:
463         value = a string containing an integer and the first leter of a time
464             unit. The supported units are minutes, hours and days.
465 
466     Returns:
467         true, if the string is a valid interval
468 
469 ******************************************************************************/
470 
471 public bool isTimeInterval ( cstring value )
472 {
473     if (value == "")
474     {
475         return false;
476     }
477 
478     long result;
479     long tempValue;
480 
481     char unit = value[value.length - 1];
482 
483     if ( !toLong(value[0..$-1], tempValue) )
484     {
485         return false;
486     }
487 
488     if ( unit != 's' &&  unit != 'm' && unit != 'h' && unit != 'd' )
489     {
490         return false;
491     }
492 
493     return true;
494 }
495 
496 ///
497 unittest
498 {
499     Test.test!("==")(isTimeInterval(""), false);
500     Test.test!("==")(isTimeInterval("1s"), true);
501     Test.test!("==")(isTimeInterval("1m"), true);
502     Test.test!("==")(isTimeInterval("2m"), true);
503     Test.test!("==")(isTimeInterval("1h"), true);
504     Test.test!("==")(isTimeInterval("2h"), true);
505     Test.test!("==")(isTimeInterval("1d"), true);
506     Test.test!("==")(isTimeInterval("2d"), true);
507 
508     Test.test!("==")(isTimeInterval("1x"), false);
509     Test.test!("==")(isTimeInterval("2xm"), false);
510 }