/lib/DDG/Goodie/WorkdaysBetween.pm

https://github.com/Apgar-LLC/zeroclickinfo-goodies · Perl · 134 lines · 75 code · 28 blank · 31 comment · 14 complexity · a7d41f39d12269c5821f1234ce0a1d09 MD5 · raw file

  1. use strict;
  2. package DDG::Goodie::WorkdaysBetween;
  3. # ABSTRACT: Give the number of work days between two given dates. Does not
  4. # consider holidays.
  5. use DDG::Goodie;
  6. use Time::Piece;
  7. use List::Util qw( min max );
  8. use Date::Calendar;
  9. use Date::Calendar::Profiles qw($Profiles);
  10. triggers start => "workdays between", "business days between", "work days between", "working days", "workdays from";
  11. zci answer_type => "workdays_between";
  12. primary_example_queries 'workdays between 01/31/2000 01/31/2001';
  13. secondary_example_queries 'workdays between 01/31/2000 01/31/2001 inclusive';
  14. description 'Calculate the number of workdays between two dates. Does not consider holidays.';
  15. name 'WorkDaysBetween';
  16. code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/WorkdaysBetween.pm';
  17. category 'calculations';
  18. topics 'everyday';
  19. attribution github => ['http://github.com/mgarriott', 'mgarriott'];
  20. handle remainder => sub {
  21. my ($start, $end) = get_dates($_);
  22. # If get_dates failed, return nothing.
  23. unless ($start && $end) {
  24. return;
  25. }
  26. my $calendar = Date::Calendar->new($Profiles->{US});
  27. my $workdays = $calendar->delta_workdays($start->year, $start->mon, $start->mday, $end->year, $end->mon, $end->mday, 1, 1);
  28. my $date_format = "%b %d, %Y";
  29. my $start_str = $start->strftime($date_format);
  30. my $end_str = $end->strftime($date_format);
  31. my $verb = $workdays == 1 ? 'is' : 'are';
  32. my $number = $workdays == 1 ? 'workday' : 'workdays';
  33. return "There $verb $workdays $number between $start_str and $end_str.";
  34. };
  35. # Given a string containing two dates, parse out the dates, and return them in
  36. # chronological order.
  37. #
  38. # On success this subroutine returns a two element array of
  39. # Time::Piece in the following format ( $start_date, $end_date )
  40. #
  41. # On failure this function returns nothing.
  42. sub get_dates {
  43. my @date_strings = $_ =~ m#(\d{1,2}/\d{1,2}/\d{2,4}|\w{0,9} \d{1,2},? \d{2,4}|\d{1,2}-\d{1,2}-\d{2,4}|\d{1,2}\.\d{1,2}\.\d{2,4})#gi;
  44. # If we don't have two dates matching the correct format, return nothing.
  45. if (scalar(@date_strings) != 2) {
  46. return;
  47. }
  48. # A list of date formats to try sequentially.
  49. my $day_format_slash = "%d/%m/";
  50. my $day_format_dash = "%d-%m-";
  51. my $day_format_period = "%d.%m.";
  52. my @date_formats = ( "%m/%d/", "%m-%d-", "%m.%d.", $day_format_slash, $day_format_dash, $day_format_period, "%b %d ", "%b %d, ", "%B %d ", "%B %d, ");
  53. # Flag that determines if we are using the DD/MM/YYYY format
  54. my $day_is_first = 0;
  55. # Populate the @dates array. With Time::Piece
  56. my @dates;
  57. for (my $i = 0; $i < scalar(@date_strings); $i++) {
  58. my $date_string = $date_strings[$i];
  59. foreach (@date_formats) {
  60. local $@;
  61. # Check to see if we're using the shortened year format or not.
  62. my $year_format = '%y';
  63. if($date_string =~ /\d{4}$/) {
  64. $year_format = '%Y';
  65. }
  66. my $time;
  67. eval {
  68. # Attempt to parse the date here.
  69. $time = Time::Piece->strptime($date_string, "$_$year_format");
  70. };
  71. # If we didn't get an error parsing the time...
  72. unless ($@) {
  73. # If a date matches the DD/MM/YYYY format we want to ensure
  74. # that all the XX/XX/XXXX dates match that specific format.
  75. # Therefore, we remove the MM/DD/YYYY format from the
  76. # dates_format array, clear the dates array, and restart the
  77. # loop. This way all XX/XX/XXXX dates will match only the
  78. # DD/MM/YYYY format.
  79. if ( ($_ eq $day_format_slash || $_ eq $day_format_dash || $_ eq $day_format_period) && !$day_is_first ) {
  80. # Set the flag indicating that we are using DD/MM/YYYY
  81. $day_is_first = 1;
  82. # Remove the formats in the array that begin with the month.
  83. shift(@date_formats) for(1 .. 3);
  84. # Empty the @dates array
  85. undef @dates;
  86. # Reset the loop index
  87. $i = -1;
  88. # Restart the loop iteration
  89. next;
  90. }
  91. # If the format was acceptable, add the date to the @dates array
  92. push(@dates, $time);
  93. last;
  94. }
  95. }
  96. }
  97. # Bail out if we don't have exactly two dates.
  98. if (scalar(@dates) != 2) {
  99. return;
  100. }
  101. # Find the start and end dates.
  102. my $start = min(@dates);
  103. my $end = max(@dates);
  104. return ($start, $end);
  105. }
  106. 1;