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 }