| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::JobLog::TimeGrammar; | 
| 2 |  |  |  |  |  |  | $App::JobLog::TimeGrammar::VERSION = '1.039'; | 
| 3 |  |  |  |  |  |  | # ABSTRACT: parse natural (English) language time expressions | 
| 4 |  |  |  |  |  |  |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 | 3 |  |  | 3 |  | 777 | use Exporter 'import'; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 155 |  | 
| 7 |  |  |  |  |  |  | our @EXPORT = qw( | 
| 8 |  |  |  |  |  |  | parse | 
| 9 |  |  |  |  |  |  | daytime | 
| 10 |  |  |  |  |  |  | ); | 
| 11 |  |  |  |  |  |  |  | 
| 12 | 3 |  |  | 3 |  | 15 | use Modern::Perl; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 25 |  | 
| 13 | 3 |  |  | 3 |  | 366 | use DateTime; | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 25 |  | 
| 14 | 3 |  |  |  |  | 23 | use Class::Autouse qw( | 
| 15 |  |  |  |  |  |  | App::JobLog::Log | 
| 16 | 3 |  |  | 3 |  | 74 | ); | 
|  | 3 |  |  |  |  | 6 |  | 
| 17 | 3 |  |  | 3 |  | 254 | use Carp 'croak'; | 
|  | 3 |  |  |  |  | 7 |  | 
|  | 3 |  |  |  |  | 160 |  | 
| 18 | 3 |  |  |  |  | 33 | use autouse 'App::JobLog::Config' => qw( | 
| 19 |  |  |  |  |  |  | log | 
| 20 |  |  |  |  |  |  | sunday_begins_week | 
| 21 |  |  |  |  |  |  | pay_period_length | 
| 22 |  |  |  |  |  |  | start_pay_period | 
| 23 |  |  |  |  |  |  | DIRECTORY | 
| 24 | 3 |  |  | 3 |  | 15 | ); | 
|  | 3 |  |  |  |  | 25 |  | 
| 25 | 3 |  |  |  |  | 65 | use autouse 'App::JobLog::Time' => qw( | 
| 26 |  |  |  |  |  |  | now | 
| 27 |  |  |  |  |  |  | today | 
| 28 |  |  |  |  |  |  | tz | 
| 29 | 3 |  |  | 3 |  | 469 | ); | 
|  | 3 |  |  |  |  | 6 |  | 
| 30 | 3 |  |  | 3 |  | 1242 | no if $] >= 5.018, warnings => "experimental::smartmatch"; | 
|  | 3 |  |  |  |  | 13 |  | 
|  | 3 |  |  |  |  | 21 |  | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | # some variables we need visible inside the date parsing regex | 
| 33 |  |  |  |  |  |  | # %matches holds a complete parsing | 
| 34 |  |  |  |  |  |  | # %buffer, as its name suggests, is a temporary buffer | 
| 35 |  |  |  |  |  |  | # $d1 and $d2 are the starting and ending dates | 
| 36 |  |  |  |  |  |  | our ( %matches, %buffer, $d1, $d2 ); | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | # buffers for numeric month, day, or year | 
| 39 |  |  |  |  |  |  | our ( $b1, $b2 ); | 
| 40 |  |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | # holds time of day information | 
| 42 |  |  |  |  |  |  | our $time_buffer; | 
| 43 |  |  |  |  |  |  |  | 
| 44 |  |  |  |  |  |  | # static maps for translating month and day names to numbers | 
| 45 |  |  |  |  |  |  | my ( %month_abbr, %day_abbr ); | 
| 46 |  |  |  |  |  |  |  | 
| 47 |  |  |  |  |  |  | # the master date parsing regex | 
| 48 |  |  |  |  |  |  | my $re = qr{ | 
| 49 |  |  |  |  |  |  | \A \s*+ (?: (?&ever) | (?&span) ) \s*+ \Z | 
| 50 |  |  |  |  |  |  |  | 
| 51 |  |  |  |  |  |  | (?(DEFINE) | 
| 52 |  |  |  |  |  |  |  | 
| 53 | 0 |  |  |  |  | 0 | (? (?: all | always | ever | (?:(?:the \s++)? (?: entire | whole ) \s++ )? log ) (?{ $matches{ever} = 1 }) ) | 
| 54 |  |  |  |  |  |  |  | 
| 55 |  |  |  |  |  |  | (? | 
| 56 | 911 |  |  |  |  | 1687 | ((?&date)) (?{ $d1 = $^N; stow($d1) }) | 
|  | 911 |  |  |  |  | 1570 |  | 
| 57 | 213 |  |  |  |  | 371 | (?: (?&span_divider) ((?&date)) (?{ $d2 = $^N; stow($d2) }) )? | 
|  | 213 |  |  |  |  | 306 |  | 
| 58 |  |  |  |  |  |  | ) | 
| 59 |  |  |  |  |  |  |  | 
| 60 |  |  |  |  |  |  | (? \s*+ (?: -++ | \b(?: through | thru | to | till?+ | until )\b ) \s*+) | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | (? at | @ ) | 
| 63 |  |  |  |  |  |  |  | 
| 64 |  |  |  |  |  |  | (? | 
| 65 | 515 |  |  |  |  | 2591 | (?{ $time_buffer = undef }) | 
| 66 |  |  |  |  |  |  | (?: (?: \s++ | \s*+ (?&at) \s*+ ) (?&time))? | 
| 67 |  |  |  |  |  |  | ) | 
| 68 |  |  |  |  |  |  |  | 
| 69 |  |  |  |  |  |  | (? (?:(?&at) \s++)? (?&time) \s++ on \s++ ) | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | (? | 
| 72 | 487 |  |  |  |  | 1888 | (?{ (%buffer, $b1, $b2, $time_buffer) = ()}) | 
| 73 |  |  |  |  |  |  | (?: (?&numeric) | (?&verbal) ) | 
| 74 | 1124 | 50 |  |  |  | 4150 | (?{ $buffer{time} = $time_buffer if $time_buffer }) | 
| 75 |  |  |  |  |  |  | ) | 
| 76 |  |  |  |  |  |  |  | 
| 77 |  |  |  |  |  |  | (? | 
| 78 | 1846 |  |  |  |  | 13291 | (?{ $time_buffer = undef }) | 
| 79 |  |  |  |  |  |  | ( | 
| 80 |  |  |  |  |  |  | \d{1,2} | 
| 81 |  |  |  |  |  |  | (?: | 
| 82 |  |  |  |  |  |  | : \d{2} | 
| 83 |  |  |  |  |  |  | (?: | 
| 84 |  |  |  |  |  |  | : \d{2} | 
| 85 |  |  |  |  |  |  | )? | 
| 86 |  |  |  |  |  |  | )? | 
| 87 |  |  |  |  |  |  | (?: \s*+ (?&time_suffix))? | 
| 88 |  |  |  |  |  |  | ) | 
| 89 | 1878 |  |  |  |  | 16197 | (?{ $time_buffer = $^N }) | 
| 90 |  |  |  |  |  |  | ) | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | (? [ap] (?:m|\.m\.)) | 
| 93 |  |  |  |  |  |  |  | 
| 94 |  |  |  |  |  |  | (? | 
| 95 |  |  |  |  |  |  | (?: | 
| 96 |  |  |  |  |  |  | (?&year) | 
| 97 |  |  |  |  |  |  | | | 
| 98 |  |  |  |  |  |  | (?&ym) | 
| 99 |  |  |  |  |  |  | | | 
| 100 |  |  |  |  |  |  | (?&at_time_on) (?&numeric_no_time) | 
| 101 |  |  |  |  |  |  | | | 
| 102 |  |  |  |  |  |  | (?&numeric_no_time) (?&at_time)) | 
| 103 | 676 |  |  |  |  | 2131 | (?{ $buffer{type} = 'numeric' }) | 
| 104 |  |  |  |  |  |  | ) | 
| 105 |  |  |  |  |  |  |  | 
| 106 | 1004 |  |  |  |  | 4709 | (? (?{ %buffer = () }) (\d{4}) (?{ $buffer{year} = $^N }) ) | 
|  | 153 |  |  |  |  | 927 |  | 
| 107 |  |  |  |  |  |  |  | 
| 108 | 91 |  |  |  |  | 531 | (? (?&year) (?÷r) (\d{1,2}) (?{ @buffer{qw(month unit)} = ($^N, 'months') }) ) | 
| 109 |  |  |  |  |  |  |  | 
| 110 | 557 |  |  |  |  | 4994 | (? (?{ %buffer = () }) (?&us) | (?&iso) | (?&md) | (?&dom) ) | 
| 111 |  |  |  |  |  |  |  | 
| 112 |  |  |  |  |  |  | (? | 
| 113 | 480 |  |  |  |  | 2549 | (\d{1,2}) (?{ $b1 = $^N }) | 
| 114 |  |  |  |  |  |  | ((?÷r)) | 
| 115 | 142 |  |  |  |  | 686 | (\d{1,2}) (?{ $b2 = $^N }) | 
| 116 |  |  |  |  |  |  | \g{-2} | 
| 117 |  |  |  |  |  |  | (\d{4}) | 
| 118 |  |  |  |  |  |  | (?{ | 
| 119 | 80 |  |  |  |  | 224 | $buffer{year}  = $^N; | 
| 120 | 80 |  |  |  |  | 145 | $buffer{month} = $b1; | 
| 121 | 80 |  |  |  |  | 384 | $buffer{day}   = $b2; | 
| 122 |  |  |  |  |  |  | }) | 
| 123 |  |  |  |  |  |  | ) | 
| 124 |  |  |  |  |  |  |  | 
| 125 |  |  |  |  |  |  | (? | 
| 126 | 102 |  |  |  |  | 630 | (\d{4}) (?{ $b1 = $^N }) | 
| 127 |  |  |  |  |  |  | ((?÷r)) | 
| 128 | 102 |  |  |  |  | 400 | (\d{1,2}) (?{ $b2 = $^N }) | 
| 129 |  |  |  |  |  |  | \g{-2} | 
| 130 |  |  |  |  |  |  | (\d{1,2}) | 
| 131 |  |  |  |  |  |  | (?{ | 
| 132 | 102 |  |  |  |  | 238 | $buffer{year}  = $b1; | 
| 133 | 102 |  |  |  |  | 183 | $buffer{month} = $b2; | 
| 134 | 102 |  |  |  |  | 499 | $buffer{day}   = $^N; | 
| 135 |  |  |  |  |  |  | }) | 
| 136 |  |  |  |  |  |  | ) | 
| 137 |  |  |  |  |  |  |  | 
| 138 |  |  |  |  |  |  | (? | 
| 139 | 196 |  |  |  |  | 1024 | (\d{1,2}) (?{ $b1 = $^N }) | 
| 140 |  |  |  |  |  |  | (?÷r) | 
| 141 |  |  |  |  |  |  | (\d{1,2}) | 
| 142 |  |  |  |  |  |  | (?{ | 
| 143 | 62 |  |  |  |  | 161 | $buffer{month} = $b1; | 
| 144 | 62 |  |  |  |  | 309 | $buffer{day}   = $^N; | 
| 145 |  |  |  |  |  |  | }) | 
| 146 |  |  |  |  |  |  | ) | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | (? | 
| 149 |  |  |  |  |  |  | (\d{1,2}) | 
| 150 | 154 |  |  |  |  | 838 | (?{ $buffer{day} = $^N }) | 
| 151 |  |  |  |  |  |  | ) | 
| 152 |  |  |  |  |  |  |  | 
| 153 |  |  |  |  |  |  | (? | 
| 154 |  |  |  |  |  |  | (?: (?&my) | (?&named_period) | (?&relative_period) | (?&month_day) | (?&full) ) | 
| 155 | 448 |  |  |  |  | 1681 | (?{ $buffer{type} = 'verbal' }) | 
| 156 |  |  |  |  |  |  | ) | 
| 157 |  |  |  |  |  |  |  | 
| 158 |  |  |  |  |  |  | (? (?&modifiable_day) | (?&modifiable_month) | (?&modifiable_period) ) | 
| 159 |  |  |  |  |  |  |  | 
| 160 |  |  |  |  |  |  | (? (?&at_time_on) (?&modifiable_day_no_time) | (?&modifiable_day_no_time) (?&at_time)) | 
| 161 |  |  |  |  |  |  |  | 
| 162 |  |  |  |  |  |  | (? | 
| 163 | 310 |  |  |  |  | 5290 | ((?:(?&modifier) \s++ )?) (?{ $b1 = $^N }) | 
| 164 |  |  |  |  |  |  | ((?&weekday)) | 
| 165 |  |  |  |  |  |  | (?{ | 
| 166 | 83 | 100 |  |  |  | 303 | $buffer{modifier} = $b1 if $b1; | 
| 167 | 83 |  |  |  |  | 552 | $buffer{day}      = $^N; | 
| 168 |  |  |  |  |  |  | }) | 
| 169 |  |  |  |  |  |  | ) | 
| 170 |  |  |  |  |  |  |  | 
| 171 |  |  |  |  |  |  | (? | 
| 172 | 182 |  |  |  |  | 1951 | ((?:(?&month_modifier) \s++ )?) (?{ $b1 = $^N }) | 
| 173 |  |  |  |  |  |  | ((?&month)) | 
| 174 |  |  |  |  |  |  | (?{ | 
| 175 | 32 | 100 |  |  |  | 82 | $buffer{modifier} = $b1 if $b1; | 
| 176 | 32 |  |  |  |  | 231 | $buffer{month}    = $^N; | 
| 177 |  |  |  |  |  |  | }) | 
| 178 |  |  |  |  |  |  | ) | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | (? | 
| 181 | 172 |  |  |  |  | 3981 | (?{ $b1 = undef }) | 
| 182 | 6 |  |  |  |  | 42 | (?:((?&period_modifier)) \s*+  (?{ $b1 = $^N }))? | 
| 183 |  |  |  |  |  |  | ((?&period)) | 
| 184 |  |  |  |  |  |  | (?{ | 
| 185 | 11 | 100 |  |  |  | 39 | $buffer{modifier} = $b1 if $b1; | 
| 186 | 11 |  |  |  |  | 70 | $buffer{period}   = $^N; | 
| 187 |  |  |  |  |  |  | }) | 
| 188 |  |  |  |  |  |  | ) | 
| 189 |  |  |  |  |  |  |  | 
| 190 |  |  |  |  |  |  | (? pay | pp | pay \s*+ period ) | 
| 191 |  |  |  |  |  |  |  | 
| 192 |  |  |  |  |  |  | (? | 
| 193 |  |  |  |  |  |  | (?:(?&at) \s*+)? (?&time) \s++ (?&relative_period_no_time) | 
| 194 |  |  |  |  |  |  | | | 
| 195 |  |  |  |  |  |  | (?&relative_period_no_time) (?&at_time) | 
| 196 |  |  |  |  |  |  | | | 
| 197 |  |  |  |  |  |  | (?&now) | 
| 198 |  |  |  |  |  |  | ) | 
| 199 |  |  |  |  |  |  |  | 
| 200 | 6 |  |  |  |  | 58 | (? now (?{ $buffer{day} = 'today' })) | 
| 201 |  |  |  |  |  |  |  | 
| 202 | 7 |  |  |  |  | 47 | (? ( yesterday | today | tomorrow ) (?{ $buffer{day} = $^N })) | 
| 203 |  |  |  |  |  |  |  | 
| 204 |  |  |  |  |  |  | (? (?&at_time_on) (?&month_day_no_time) | (?&month_day_no_time) (?&at_time)) | 
| 205 |  |  |  |  |  |  |  | 
| 206 |  |  |  |  |  |  | (? (?&month_first) | (?&day_first) ) | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | (? | 
| 209 | 43 |  |  |  |  | 177 | ((?&month)) (?{ $b1 = $^N }) | 
| 210 |  |  |  |  |  |  | \s++ | 
| 211 |  |  |  |  |  |  | (\d{1,2}) | 
| 212 |  |  |  |  |  |  | (?{ | 
| 213 | 43 |  |  |  |  | 94 | $buffer{month} = $b1; | 
| 214 | 43 |  |  |  |  | 350 | $buffer{day}   = $^N; | 
| 215 |  |  |  |  |  |  | }) | 
| 216 |  |  |  |  |  |  | ) | 
| 217 |  |  |  |  |  |  |  | 
| 218 | 1 |  |  |  |  | 7 | (? ((?&month)) ,? \s*+ (?&year) (?{ @buffer{qw(month unit)} = ($^N, 'months') }) ) | 
| 219 |  |  |  |  |  |  |  | 
| 220 |  |  |  |  |  |  | (? | 
| 221 | 142 |  |  |  |  | 1630 | (\d{1,2}) (?{ $b1 = $^N }) | 
| 222 |  |  |  |  |  |  | \s++ | 
| 223 |  |  |  |  |  |  | ((?&month)) | 
| 224 |  |  |  |  |  |  | (?{ | 
| 225 | 86 |  |  |  |  | 214 | $buffer{month} = $^N; | 
| 226 | 86 |  |  |  |  | 681 | $buffer{day}   = $b1; | 
| 227 |  |  |  |  |  |  | }) | 
| 228 |  |  |  |  |  |  | ) | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | (? (?&at_time_on) (?&full_no_time) | (?&full_no_time) (?&at_time)) | 
| 231 |  |  |  |  |  |  |  | 
| 232 |  |  |  |  |  |  | (? (?&dm_full) | (?&md_full) ) | 
| 233 |  |  |  |  |  |  |  | 
| 234 |  |  |  |  |  |  | (? | 
| 235 | 127 |  |  |  |  | 1514 | (\d{1,2}) (?{ $b1 = $^N }) | 
| 236 |  |  |  |  |  |  | \s++ | 
| 237 | 83 |  |  |  |  | 386 | ((?&month)) (?{ $b2 = $^N }) | 
| 238 |  |  |  |  |  |  | ,? \s++ | 
| 239 |  |  |  |  |  |  | (\d{4}) | 
| 240 |  |  |  |  |  |  | (?{ | 
| 241 | 83 |  |  |  |  | 201 | $buffer{year}  = $^N; | 
| 242 | 83 |  |  |  |  | 154 | $buffer{month} = $b2; | 
| 243 | 83 |  |  |  |  | 823 | $buffer{day}   = $b1; | 
| 244 |  |  |  |  |  |  | }) | 
| 245 |  |  |  |  |  |  | ) | 
| 246 |  |  |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | (? | 
| 248 | 40 |  |  |  |  | 187 | ((?&month)) (?{ $b2 = $^N }) | 
| 249 |  |  |  |  |  |  | \s++ | 
| 250 | 40 |  |  |  |  | 170 | (\d{1,2}) (?{ $b1 = $^N }) | 
| 251 |  |  |  |  |  |  | , \s++ | 
| 252 |  |  |  |  |  |  | (\d{4}) | 
| 253 |  |  |  |  |  |  | (?{ | 
| 254 | 40 |  |  |  |  | 92 | $buffer{year}  = $^N; | 
| 255 | 40 |  |  |  |  | 72 | $buffer{month} = $b2; | 
| 256 | 40 |  |  |  |  | 326 | $buffer{day}   = $b1; | 
| 257 |  |  |  |  |  |  | }) | 
| 258 |  |  |  |  |  |  | ) | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | (? (?&full_weekday) | (?&short_weekday) ) | 
| 261 |  |  |  |  |  |  |  | 
| 262 |  |  |  |  |  |  | (? sunday | monday | tuesday | wednesday | thursday | friday | saturday ) | 
| 263 |  |  |  |  |  |  |  | 
| 264 |  |  |  |  |  |  | (? sun | mon | tue | wed | thu | fri | sat ) | 
| 265 |  |  |  |  |  |  |  | 
| 266 |  |  |  |  |  |  | (? (?&full_month) | (?&short_month) ) | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  | (? january | february | march | april | may | june | july | august | september | october | november | december ) | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | (? jan | feb | mar | apr | may | jun | jul | aug | sep | oct | nov | dec ) | 
| 271 |  |  |  |  |  |  |  | 
| 272 |  |  |  |  |  |  | (? last | this | next ) | 
| 273 |  |  |  |  |  |  |  | 
| 274 |  |  |  |  |  |  | (? (?&modifier) | (?&termini) (?: \s++ of (?: \s++ the )? )? ) | 
| 275 |  |  |  |  |  |  |  | 
| 276 |  |  |  |  |  |  | (? week | month | year | (?&pay) ) | 
| 277 |  |  |  |  |  |  |  | 
| 278 |  |  |  |  |  |  | (? (?&modifier) | (?&termini) (?: \s++ of )? ) | 
| 279 |  |  |  |  |  |  |  | 
| 280 |  |  |  |  |  |  | (? (?: the \s++ )? (?: (?&beginning) | end ) ) | 
| 281 |  |  |  |  |  |  |  | 
| 282 |  |  |  |  |  |  | (? beg(?:in(?:ning)?)?) | 
| 283 |  |  |  |  |  |  |  | 
| 284 |  |  |  |  |  |  | (? [-/.]) | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  | ) | 
| 287 |  |  |  |  |  |  | }xi; | 
| 288 |  |  |  |  |  |  |  | 
| 289 |  |  |  |  |  |  | # stows everything matched so far in %matches | 
| 290 |  |  |  |  |  |  | sub stow { | 
| 291 | 1124 |  |  | 1124 | 0 | 4631 | my %h = %buffer; | 
| 292 | 1124 |  |  |  |  | 3328 | $matches{ $_[0] } = \%h; | 
| 293 | 1124 |  |  |  |  | 15060 | %buffer = (); | 
| 294 |  |  |  |  |  |  | } | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  |  | 
| 297 |  |  |  |  |  |  | sub daytime { | 
| 298 | 416 |  |  | 416 | 1 | 694 | my $time = shift; | 
| 299 |  |  |  |  |  |  |  | 
| 300 |  |  |  |  |  |  | #parse | 
| 301 | 416 |  |  |  |  | 2121 | $time =~ /(?\d++) | 
| 302 |  |  |  |  |  |  | (?: | 
| 303 |  |  |  |  |  |  | : (?\d++) | 
| 304 |  |  |  |  |  |  | (?: | 
| 305 |  |  |  |  |  |  | : (?\d++) | 
| 306 |  |  |  |  |  |  | )? | 
| 307 |  |  |  |  |  |  | )? | 
| 308 |  |  |  |  |  |  | (?: \s*+ (?[ap]) (\.?)m\g{-1})? | 
| 309 |  |  |  |  |  |  | /ix; | 
| 310 |  |  |  |  |  |  | my ( $hour, $minute, $second, $suffix ) = | 
| 311 | 416 |  | 50 | 1 |  | 8247 | ( $+{hour}, $+{minute} || 0, $+{second} || 0, lc( $+{suffix} || 'x' ) ); | 
|  | 1 |  | 100 |  |  | 3401 |  | 
|  | 1 |  | 100 |  |  | 407 |  | 
|  | 1 |  |  |  |  | 5326 |  | 
| 312 | 416 | 50 | 33 |  |  | 1855 | $hour += 12 if $suffix eq 'p' && $hour < 12; | 
| 313 | 416 | 50 |  |  |  | 1119 | $suffix = 'p' if $hour > 11; | 
| 314 | 416 | 50 | 33 |  |  | 1002 | $hour = 0 if $hour == 12 && $suffix eq 'a'; | 
| 315 | 416 | 50 | 33 |  |  | 3997 | croak | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
| 316 |  |  |  |  |  |  | "impossible time: $time" #<--- syntax error at (eval 4158) line 23, near "croak "impossible time: $time"" | 
| 317 |  |  |  |  |  |  |  | 
| 318 |  |  |  |  |  |  | if $hour > 23 | 
| 319 |  |  |  |  |  |  | || $minute > 59 | 
| 320 |  |  |  |  |  |  | || $second > 59 | 
| 321 |  |  |  |  |  |  | || $suffix eq 'a' && $hour > 12; | 
| 322 | 416 | 50 | 66 |  |  | 1579 | $hour = 0 if $suffix eq 'a' && $hour == 12; | 
| 323 |  |  |  |  |  |  | return ( | 
| 324 | 416 |  |  |  |  | 2594 | hour   => $hour, | 
| 325 |  |  |  |  |  |  | minute => $minute, | 
| 326 |  |  |  |  |  |  | second => $second, | 
| 327 |  |  |  |  |  |  | suffix => $suffix | 
| 328 |  |  |  |  |  |  | ); | 
| 329 |  |  |  |  |  |  | } | 
| 330 |  |  |  |  |  |  |  | 
| 331 |  |  |  |  |  |  |  | 
| 332 |  |  |  |  |  |  | sub parse { | 
| 333 | 445 |  |  | 445 | 1 | 508696 | my $phrase = shift; | 
| 334 | 445 |  |  |  |  | 1328 | local ( %matches, %buffer, $d1, $d2, $b1, $b2, $time_buffer ); | 
| 335 | 445 | 50 |  |  |  | 6237 | if ( $phrase =~ $re ) { | 
| 336 | 445 | 50 |  |  |  | 1129 | if ( $matches{ever} ) { | 
| 337 |  |  |  |  |  |  |  | 
| 338 |  |  |  |  |  |  | # we want the entire timespan of the log | 
| 339 | 0 |  |  |  |  | 0 | my ($se) = App::JobLog::Log->new->first_event; | 
| 340 | 0 | 0 |  |  |  | 0 | if ($se) { | 
| 341 | 0 |  |  |  |  | 0 | return $se->start, now, 0; | 
| 342 |  |  |  |  |  |  | } | 
| 343 |  |  |  |  |  |  | else { | 
| 344 | 0 |  |  |  |  | 0 | return now->subtract( seconds => 1 ), now, 0; | 
| 345 |  |  |  |  |  |  | } | 
| 346 |  |  |  |  |  |  | } | 
| 347 |  |  |  |  |  |  |  | 
| 348 | 445 |  |  |  |  | 807 | my $h1   = $matches{$d1}; | 
| 349 | 445 |  |  |  |  | 762 | my $unit = delete $h1->{unit}; | 
| 350 | 445 |  |  |  |  | 1016 | normalize($h1); | 
| 351 | 445 | 100 |  |  |  | 869 | if ($unit) { | 
| 352 |  |  |  |  |  |  |  | 
| 353 |  |  |  |  |  |  | # $h1 is necessarily fixed and there is no time associated | 
| 354 | 4 |  |  |  |  | 15 | $h1 = fix_date( $h1, 1 ); | 
| 355 | 4 |  |  |  |  | 1224 | my $h2 = $h1->clone->add( $unit => 1 )->subtract( seconds => 1 ); | 
| 356 | 4 |  |  |  |  | 6515 | return $h1, $h2, 1; | 
| 357 |  |  |  |  |  |  | } | 
| 358 |  |  |  |  |  |  | else { | 
| 359 | 441 |  |  |  |  | 931 | my %t1 = extract_time( $h1, 1 ); | 
| 360 | 441 |  |  |  |  | 806 | my ( $h2, $count, %t2 ); | 
| 361 | 441 | 100 | 66 |  |  | 1288 | if ( $d2 && $matches{$d2} ) { | 
| 362 | 20 |  |  |  |  | 33 | $h2 = $matches{$d2}; | 
| 363 | 20 |  |  |  |  | 49 | normalize($h2); | 
| 364 | 20 |  |  |  |  | 52 | %t2    = extract_time($h2); | 
| 365 | 20 |  |  |  |  | 47 | $count = 2; | 
| 366 |  |  |  |  |  |  | } | 
| 367 |  |  |  |  |  |  | else { | 
| 368 | 421 |  |  |  |  | 1851 | $h2    = {%$h1}; | 
| 369 | 421 |  |  |  |  | 1375 | %t2    = ( hour => 23, minute => 59, second => 59 ); | 
| 370 | 421 |  |  |  |  | 591 | $count = 1; | 
| 371 |  |  |  |  |  |  | } | 
| 372 | 441 |  |  |  |  | 936 | infer_modifier( $h1, $h2 ); | 
| 373 | 441 |  |  |  |  | 974 | my ( $s1, $s2 ) = ( $t1{suffix}, $t2{suffix} ); | 
| 374 | 441 |  |  |  |  | 834 | delete $t1{suffix}, delete $t2{suffix}; | 
| 375 | 441 | 100 |  |  |  | 906 | if ( is_fixed($h1) ) { | 
|  |  | 100 |  |  |  |  |  | 
| 376 | 356 |  |  |  |  | 771 | ( $h1, $h2 ) = fixed_start( $h1, $h2, $count == 2 ); | 
| 377 |  |  |  |  |  |  | } | 
| 378 |  |  |  |  |  |  | elsif ( is_fixed($h2) ) { | 
| 379 | 1 |  |  |  |  | 4 | ( $h1, $h2 ) = fixed_end( $h1, $h2 ); | 
| 380 |  |  |  |  |  |  | } | 
| 381 |  |  |  |  |  |  | else { | 
| 382 | 84 |  |  |  |  | 188 | ( $h1, $h2 ) = before_now( $h1, $h2, $count == 2 ); | 
| 383 |  |  |  |  |  |  | } | 
| 384 | 441 | 50 |  |  |  | 4768 | croak "dates in \"$phrase\" are out of order" | 
| 385 |  |  |  |  |  |  | unless DateTime->compare( $h1, $h2 ) <= 0; | 
| 386 | 441 |  |  |  |  | 42058 | $h1->set(%t1); | 
| 387 | 441 |  |  |  |  | 140018 | $h2->set(%t2); | 
| 388 | 441 | 50 |  |  |  | 135268 | if ( $h1 > $h2 ) { | 
| 389 | 0 | 0 | 0 |  |  | 0 | if (   $h1->year == $h2->year | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 390 |  |  |  |  |  |  | && $h1->month == $h2->month | 
| 391 |  |  |  |  |  |  | && $h1->day == $h2->day | 
| 392 |  |  |  |  |  |  | && $h2->hour < 12 | 
| 393 |  |  |  |  |  |  | && $s2 eq 'x' ) | 
| 394 |  |  |  |  |  |  | { | 
| 395 |  |  |  |  |  |  |  | 
| 396 |  |  |  |  |  |  | # we inferred the 12 hour period of the second endpoint incorrectly; | 
| 397 |  |  |  |  |  |  | # it was in the evening rather than morning | 
| 398 | 0 |  |  |  |  | 0 | $h2->add( hours => 12 ); | 
| 399 |  |  |  |  |  |  | } | 
| 400 |  |  |  |  |  |  | else { | 
| 401 | 0 |  |  |  |  | 0 | croak "dates in \"$phrase\" are out of order"; | 
| 402 |  |  |  |  |  |  | } | 
| 403 |  |  |  |  |  |  | } | 
| 404 | 441 |  |  |  |  | 44861 | return $h1, $h2, $count == 2; | 
| 405 |  |  |  |  |  |  | } | 
| 406 |  |  |  |  |  |  | } | 
| 407 | 0 |  |  |  |  | 0 | croak "cannot parse \"$phrase\" as a date expression"; | 
| 408 |  |  |  |  |  |  | } | 
| 409 |  |  |  |  |  |  |  | 
| 410 |  |  |  |  |  |  | # if the sole expression is a unit identifier, infer the modifier 'this' | 
| 411 |  |  |  |  |  |  | sub infer_modifier { | 
| 412 | 441 |  |  | 441 | 0 | 656 | my ( $h1, $h2 ) = @_; | 
| 413 | 441 | 50 | 100 |  |  | 1679 | if ( keys %$h1 == 2 && keys %$h2 == 2 && $h1->{period} && $h2->{period} ) { | 
|  |  |  | 66 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
| 414 | 3 |  |  |  |  | 8 | $h1->{modifier} = $h2->{modifier} = 'this'; | 
| 415 |  |  |  |  |  |  | } | 
| 416 |  |  |  |  |  |  | } | 
| 417 |  |  |  |  |  |  |  | 
| 418 |  |  |  |  |  |  | # pulls time expression -- 11:00 am, e.g. -- out of hash and converts it | 
| 419 |  |  |  |  |  |  | # to a series of units | 
| 420 |  |  |  |  |  |  | sub extract_time { | 
| 421 | 461 |  |  | 461 | 0 | 832 | my ( $h, $is_start ) = @_; | 
| 422 | 461 |  |  |  |  | 763 | my $time = $h->{time}; | 
| 423 | 461 | 100 |  |  |  | 894 | if ( defined $time ) { | 
| 424 | 416 |  |  |  |  | 737 | delete $h->{time}; | 
| 425 |  |  |  |  |  |  |  | 
| 426 | 416 |  |  |  |  | 873 | return daytime($time); | 
| 427 |  |  |  |  |  |  | } | 
| 428 |  |  |  |  |  |  | else { | 
| 429 |  |  |  |  |  |  |  | 
| 430 |  |  |  |  |  |  | #return default values | 
| 431 | 45 | 100 |  |  |  | 310 | return $is_start | 
| 432 |  |  |  |  |  |  | ? ( hour => 0, minute => 0, second => 0, suffix => 'a' ) | 
| 433 |  |  |  |  |  |  | : ( hour => 23, minute => 59, second => 59, suffix => 'p' ); | 
| 434 |  |  |  |  |  |  | } | 
| 435 |  |  |  |  |  |  | } | 
| 436 |  |  |  |  |  |  |  | 
| 437 |  |  |  |  |  |  | # produces interpretation of date expression consistent with a fixed end date | 
| 438 |  |  |  |  |  |  | sub fixed_end { | 
| 439 | 1 |  |  | 1 | 0 | 3 | my ( $h1, $h2 ) = @_; | 
| 440 | 1 |  |  |  |  | 3 | $h2 = fix_date($h2); | 
| 441 | 1 | 50 |  |  |  | 156 | if ( is_fixed($h1) ) { | 
| 442 | 0 |  |  |  |  | 0 | $h1 = fix_date( $h1, 1 ); | 
| 443 |  |  |  |  |  |  | } | 
| 444 |  |  |  |  |  |  | else { | 
| 445 | 1 |  |  |  |  | 3 | my ( $unit, $amt ) = time_unit($h1); | 
| 446 | 1 |  |  |  |  | 3 | $h1 = decontextualized_date( $h1, 1 ); | 
| 447 | 1 | 50 |  |  |  | 7 | if ( ref $h1 eq 'DateTime' ) { | 
| 448 | 1 |  |  |  |  | 5 | while ( DateTime->compare( $h1, $h2 ) > 0 ) { | 
| 449 | 3 |  |  |  |  | 1439 | $h1->subtract( $unit => $amt ); | 
| 450 |  |  |  |  |  |  | } | 
| 451 |  |  |  |  |  |  | } | 
| 452 |  |  |  |  |  |  | else { | 
| 453 |  |  |  |  |  |  |  | 
| 454 |  |  |  |  |  |  | # we just have a floating weekday | 
| 455 | 0 |  |  |  |  | 0 | $h1 = adjust_weekday( $h1, $h2 ); | 
| 456 |  |  |  |  |  |  | } | 
| 457 |  |  |  |  |  |  | } | 
| 458 | 1 |  |  |  |  | 650 | return ( $h1, $h2 ); | 
| 459 |  |  |  |  |  |  | } | 
| 460 |  |  |  |  |  |  |  | 
| 461 |  |  |  |  |  |  | # picks a day of the week before a given date | 
| 462 |  |  |  |  |  |  | sub adjust_weekday { | 
| 463 | 81 |  |  | 81 | 0 | 121 | my ( $ref, $date ) = @_; | 
| 464 |  |  |  |  |  |  | my $delta = $ref->{day_of_week} | 
| 465 | 81 |  | 50 |  |  | 214 | || die 'should always be day_of_week key at this point'; | 
| 466 | 81 |  |  |  |  | 229 | my $d = $date->clone; | 
| 467 | 81 |  |  |  |  | 974 | $delta = $date->day_of_week - $delta; | 
| 468 | 81 | 50 |  |  |  | 402 | $delta += 7 if $delta <= 0; | 
| 469 | 81 |  |  |  |  | 248 | $d->subtract( days => $delta ); | 
| 470 | 81 |  |  |  |  | 50426 | return $d; | 
| 471 |  |  |  |  |  |  | } | 
| 472 |  |  |  |  |  |  |  | 
| 473 |  |  |  |  |  |  | # determines the finest grained unit of time by which a given date can be modified | 
| 474 |  |  |  |  |  |  | sub time_unit { | 
| 475 | 172 |  |  | 172 | 0 | 260 | my $h = shift; | 
| 476 | 172 | 100 |  |  |  | 387 | if ( $h->{type} eq 'numeric' ) { | 
| 477 | 82 | 50 |  |  |  | 292 | return 'years' => 1 if exists $h->{month}; | 
| 478 | 0 |  |  |  |  | 0 | return 'months' => 1; | 
| 479 |  |  |  |  |  |  | } | 
| 480 |  |  |  |  |  |  | else { | 
| 481 | 90 | 100 |  |  |  | 193 | if ( my $period = $h->{period} ) { | 
| 482 | 2 |  |  |  |  | 5 | for ($period) { | 
| 483 | 2 |  |  |  |  | 5 | when ('mon') { return 'months' => 1 } | 
|  | 0 |  |  |  |  | 0 |  | 
| 484 | 2 |  |  |  |  | 4 | when ('wee') { return 'weeks'  => 1 } | 
|  | 0 |  |  |  |  | 0 |  | 
| 485 | 2 |  |  |  |  | 3 | when ('pay') { return 'days'   => pay_period_length() } | 
|  | 2 |  |  |  |  | 6 |  | 
| 486 |  |  |  |  |  |  | } | 
| 487 |  |  |  |  |  |  | } | 
| 488 |  |  |  |  |  |  | else { | 
| 489 | 88 | 100 |  |  |  | 229 | return 'years' => 1 if exists $h->{month}; | 
| 490 | 81 | 50 |  |  |  | 299 | return 'weeks' => 1 if exists $h->{day}; | 
| 491 | 0 |  |  |  |  | 0 | return 'months' => 1; | 
| 492 |  |  |  |  |  |  | } | 
| 493 |  |  |  |  |  |  | } | 
| 494 |  |  |  |  |  |  | } | 
| 495 |  |  |  |  |  |  |  | 
| 496 |  |  |  |  |  |  | # produces interpretation of date expression consistent with a fixed start date | 
| 497 |  |  |  |  |  |  | sub fixed_start { | 
| 498 | 356 |  |  | 356 | 0 | 629 | my ( $h1, $h2, $two_endpoints ) = @_; | 
| 499 | 356 |  |  |  |  | 667 | $h1 = fix_date( $h1, 1 ); | 
| 500 | 356 | 100 | 100 |  |  | 56820 | unless ( $two_endpoints || $h2->{type} ne 'numeric' ) { | 
| 501 | 164 | 100 |  |  |  | 715 | return $h1, $h1->clone if defined $h2->{day}; | 
| 502 | 1 |  |  |  |  | 43 | return $h1, $h1->clone->add( years => 1 )->subtract( days => 1 ); | 
| 503 |  |  |  |  |  |  | } | 
| 504 | 192 | 100 |  |  |  | 451 | if ( is_fixed($h2) ) { | 
| 505 | 189 |  |  |  |  | 382 | $h2 = fix_date($h2); | 
| 506 |  |  |  |  |  |  | } | 
| 507 |  |  |  |  |  |  | else { | 
| 508 | 3 |  |  |  |  | 9 | my ( $unit, $amt ) = time_unit($h2); | 
| 509 | 3 |  |  |  |  | 7 | $h2 = decontextualized_date($h2); | 
| 510 | 3 | 100 |  |  |  | 15 | $h2 = adjust_weekday( $h2, $h1 ) unless ref $h2 eq 'DateTime'; | 
| 511 | 3 |  |  |  |  | 10 | $h2->subtract( $unit => $amt ) while $h2 > $h1; | 
| 512 | 3 |  |  |  |  | 3580 | $h2->add( $unit => $amt ); | 
| 513 |  |  |  |  |  |  | } | 
| 514 | 192 |  |  |  |  | 23547 | return ( $h1, $h2 ); | 
| 515 |  |  |  |  |  |  | } | 
| 516 |  |  |  |  |  |  |  | 
| 517 |  |  |  |  |  |  | # date relative to now not yet adjusted relative to its position in the span or | 
| 518 |  |  |  |  |  |  | # another fixed date | 
| 519 |  |  |  |  |  |  | sub decontextualized_date { | 
| 520 | 172 |  |  | 172 | 0 | 266 | my ( $h, $is_start ) = @_; | 
| 521 |  |  |  |  |  |  | return decontextualized_numeric_date( $h, $is_start ) | 
| 522 | 172 | 100 |  |  |  | 477 | if $h->{type} eq 'numeric'; | 
| 523 | 90 |  |  |  |  | 201 | for ( $h->{modifier} ) { | 
| 524 | 90 |  |  |  |  | 127 | when ('end')       { $is_start = 0 } | 
|  | 0 |  |  |  |  | 0 |  | 
| 525 | 90 |  |  |  |  | 156 | when ('beginning') { $is_start = 1 } | 
|  | 0 |  |  |  |  | 0 |  | 
| 526 |  |  |  |  |  |  | } | 
| 527 | 90 | 100 |  |  |  | 216 | if ( my $period = $h->{period} ) { | 
| 528 | 2 |  |  |  |  | 7 | my $date = today; | 
| 529 | 2 |  |  |  |  | 22 | for ($period) { | 
| 530 | 2 |  |  |  |  | 5 | when ('mon') { | 
| 531 | 0 |  |  |  |  | 0 | $date->truncate( to => 'month' ); | 
| 532 | 0 | 0 |  |  |  | 0 | $date->add( months => 1 ) unless $is_start; | 
| 533 |  |  |  |  |  |  | } | 
| 534 | 2 |  |  |  |  | 4 | when ('wee') { | 
| 535 | 0 |  |  |  |  | 0 | $date->truncate( to => 'week' ); | 
| 536 | 0 | 0 |  |  |  | 0 | $date->subtract( days => 1 ) if sunday_begins_week; | 
| 537 | 0 | 0 |  |  |  | 0 | $date->add( weeks => 1 ) unless $is_start; | 
| 538 |  |  |  |  |  |  | } | 
| 539 | 2 |  |  |  |  | 4 | when ('pay') { | 
| 540 | 2 |  |  |  |  | 6 | my $days = | 
| 541 |  |  |  |  |  |  | $date->delta_days(start_pay_period)->in_units('days'); | 
| 542 | 2 |  |  |  |  | 508 | $days %= pay_period_length; | 
| 543 | 2 |  |  |  |  | 8 | $date->subtract( days => $days ); | 
| 544 | 2 | 100 |  |  |  | 1169 | $date->add( days => pay_period_length ) unless $is_start; | 
| 545 |  |  |  |  |  |  | } | 
| 546 | 0 |  |  |  |  | 0 | default { | 
| 547 | 0 |  |  |  |  | 0 | croak 'DEBUG' | 
| 548 |  |  |  |  |  |  | } | 
| 549 |  |  |  |  |  |  | } | 
| 550 | 2 | 100 |  |  |  | 540 | $date->subtract( days => 1 ) unless $is_start; | 
| 551 | 2 |  |  |  |  | 604 | return $date; | 
| 552 |  |  |  |  |  |  | } | 
| 553 |  |  |  |  |  |  | else { | 
| 554 | 88 | 100 | 100 |  |  | 491 | if ( exists $h->{day} && $h->{day} !~ /^\d++$/ ) { | 
| 555 | 81 |  |  |  |  | 154 | init_day_abbr(); | 
| 556 | 81 |  |  |  |  | 209 | $h->{day_of_week} = $day_abbr{ $h->{day} }; | 
| 557 | 81 |  |  |  |  | 128 | delete $h->{day}; | 
| 558 | 81 |  |  |  |  | 203 | return $h; | 
| 559 |  |  |  |  |  |  | } | 
| 560 | 7 | 50 |  |  |  | 18 | if ( exists $h->{month} ) { | 
| 561 | 7 |  |  |  |  | 14 | init_month_abbr(); | 
| 562 | 7 |  |  |  |  | 19 | $h->{month} = $month_abbr{ $h->{month} }; | 
| 563 |  |  |  |  |  |  | } | 
| 564 | 7 |  |  |  |  | 14 | return decontextualized_numeric_date( $h, $is_start ); | 
| 565 |  |  |  |  |  |  | } | 
| 566 |  |  |  |  |  |  | } | 
| 567 |  |  |  |  |  |  |  | 
| 568 |  |  |  |  |  |  | sub decontextualized_numeric_date { | 
| 569 | 89 |  |  | 89 | 0 | 129 | my ( $h, $is_start ) = @_; | 
| 570 | 89 |  |  |  |  | 201 | my $date = today; | 
| 571 | 89 |  |  |  |  | 894 | delete $h->{type}; | 
| 572 | 89 |  |  |  |  | 124 | delete $h->{modifier}; | 
| 573 | 89 |  | 33 |  |  | 400 | $h->{year}  //= $date->year; | 
| 574 | 89 |  | 33 |  |  | 595 | $h->{month} //= $date->month; | 
| 575 | 89 |  |  |  |  | 156 | my $day_unspecified = !exists $h->{day}; | 
| 576 | 89 |  | 100 |  |  | 205 | $date = DateTime->new( time_zone => tz(), %$h, day => $h->{day} // 1 ); | 
| 577 |  |  |  |  |  |  |  | 
| 578 | 89 | 100 | 100 |  |  | 14903 | if ( !( exists $h->{day} || $is_start ) ) { | 
| 579 | 2 |  |  |  |  | 8 | $date->add( months => 1 ); | 
| 580 | 2 |  |  |  |  | 1075 | $date->subtract( days => 1 ); | 
| 581 |  |  |  |  |  |  | } | 
| 582 | 89 |  |  |  |  | 1415 | return $date; | 
| 583 |  |  |  |  |  |  | } | 
| 584 |  |  |  |  |  |  |  | 
| 585 |  |  |  |  |  |  | sub fix_date { | 
| 586 | 550 |  |  | 550 | 0 | 814 | my ( $d, $is_start ) = @_; | 
| 587 | 550 | 100 |  |  |  | 1397 | if ( $d->{type} eq 'verbal' ) { | 
| 588 | 363 | 100 |  |  |  | 919 | if ( $d->{year} ) { | 
|  |  | 100 |  |  |  |  |  | 
| 589 | 247 |  |  |  |  | 457 | init_month_abbr(); | 
| 590 | 247 |  |  |  |  | 610 | $d->{month} = $month_abbr{ $d->{month} }; | 
| 591 | 247 |  |  |  |  | 363 | delete $d->{type}; | 
| 592 | 247 |  |  |  |  | 688 | return DateTime->new( time_zone => tz(), %$d ); | 
| 593 |  |  |  |  |  |  | } | 
| 594 |  |  |  |  |  |  | elsif ( my $day = $d->{day} ) { | 
| 595 | 95 |  |  |  |  | 295 | my $date = today; | 
| 596 | 95 | 100 |  |  |  | 1188 | return $date if $day eq 'tod'; | 
| 597 | 85 | 100 |  |  |  | 259 | if ( $day eq 'yes' ) { | 
|  |  | 100 |  |  |  |  |  | 
| 598 | 2 |  |  |  |  | 9 | $date->subtract( days => 1 ); | 
| 599 | 2 |  |  |  |  | 1295 | return $date; | 
| 600 |  |  |  |  |  |  | } | 
| 601 |  |  |  |  |  |  | elsif ( $day eq 'tom' ) { | 
| 602 | 3 |  |  |  |  | 10 | $date->add( days => 1 ); | 
| 603 | 3 |  |  |  |  | 1474 | return $date; | 
| 604 |  |  |  |  |  |  | } | 
| 605 | 80 |  |  |  |  | 158 | init_day_abbr(); | 
| 606 | 80 |  |  |  |  | 163 | my $day_num    = $day_abbr{$day}; | 
| 607 | 80 |  |  |  |  | 296 | my $todays_num = $date->day_of_week; | 
| 608 | 80 | 50 |  |  |  | 376 | if ( $d->{modifier} eq 'this' ) { | 
| 609 | 0 | 0 |  |  |  | 0 | return $date if $day_num == $todays_num; | 
| 610 | 0 | 0 |  |  |  | 0 | my $delta = | 
| 611 |  |  |  |  |  |  | $day_num > $todays_num | 
| 612 |  |  |  |  |  |  | ? $day_num - $todays_num | 
| 613 |  |  |  |  |  |  | : 7 - $todays_num + $day_num; | 
| 614 | 0 |  |  |  |  | 0 | $date->add( days => $delta ); | 
| 615 | 0 |  |  |  |  | 0 | return $date; | 
| 616 |  |  |  |  |  |  | } | 
| 617 |  |  |  |  |  |  | else { | 
| 618 | 80 |  |  |  |  | 95 | my $delta = 7; | 
| 619 | 80 | 50 |  |  |  | 230 | if ( $day_num < $todays_num ) { | 
|  |  | 50 |  |  |  |  |  | 
| 620 | 0 |  |  |  |  | 0 | $delta = $todays_num - $day_num; | 
| 621 |  |  |  |  |  |  | } | 
| 622 |  |  |  |  |  |  | elsif ( $day_num > $todays_num ) { | 
| 623 | 0 |  |  |  |  | 0 | $delta = 7 - $day_num + $todays_num; | 
| 624 |  |  |  |  |  |  | } | 
| 625 | 80 |  |  |  |  | 228 | $date->subtract( days => $delta ); | 
| 626 | 80 | 50 |  |  |  | 48162 | $date->add( days => 14 ) if $d->{modifier} eq 'next'; | 
| 627 | 80 |  |  |  |  | 200 | return $date; | 
| 628 |  |  |  |  |  |  | } | 
| 629 |  |  |  |  |  |  | } | 
| 630 |  |  |  |  |  |  |  | 
| 631 | 21 | 100 |  |  |  | 62 | if ( my $period = $d->{period} ) { | 
| 632 | 17 |  |  |  |  | 55 | my $date = today; | 
| 633 | 17 | 100 |  |  |  | 221 | if ( $d->{modifier} eq 'this' ) { | 
| 634 | 10 |  |  |  |  | 20 | for ($period) { | 
| 635 | 10 |  |  |  |  | 24 | when ('mon') { | 
| 636 | 4 |  |  |  |  | 11 | $date->truncate( to => 'month' ); | 
| 637 | 4 | 100 |  |  |  | 840 | $date->add( months => 1 ) unless $is_start; | 
| 638 |  |  |  |  |  |  | } | 
| 639 | 6 |  |  |  |  | 10 | when ('wee') { | 
| 640 | 2 |  |  |  |  | 8 | my $is_sunday = $date->day_of_week == 7; | 
| 641 | 2 |  |  |  |  | 12 | $date->truncate( to => 'week' ); | 
| 642 | 2 | 50 |  |  |  | 1876 | if (sunday_begins_week) { | 
| 643 | 2 | 50 |  |  |  | 11 | $date->subtract( days => $is_sunday ? -6 : 1 ); | 
| 644 |  |  |  |  |  |  | } | 
| 645 | 2 | 100 |  |  |  | 1156 | $date->add( weeks => 1 ) unless $is_start; | 
| 646 |  |  |  |  |  |  | } | 
| 647 | 4 |  |  |  |  | 8 | when ('yea') { | 
| 648 | 2 |  |  |  |  | 6 | $date->truncate( to => 'year' ); | 
| 649 | 2 | 100 |  |  |  | 419 | $date->add( years => 1 ) unless $is_start; | 
| 650 |  |  |  |  |  |  | } | 
| 651 | 2 |  |  |  |  | 4 | when ('pay') { | 
| 652 | 2 |  |  |  |  | 7 | my $days = | 
| 653 |  |  |  |  |  |  | $date->delta_days(start_pay_period)->in_units('days'); | 
| 654 | 2 |  |  |  |  | 502 | $days %= pay_period_length; | 
| 655 | 2 |  |  |  |  | 8 | $date->subtract( days => $days ); | 
| 656 | 2 | 100 |  |  |  | 1230 | $date->add( days => pay_period_length ) | 
| 657 |  |  |  |  |  |  | unless $is_start; | 
| 658 |  |  |  |  |  |  | } | 
| 659 |  |  |  |  |  |  | } | 
| 660 | 10 | 100 |  |  |  | 2642 | $date->subtract( days => 1 ) unless $is_start; | 
| 661 |  |  |  |  |  |  | } | 
| 662 |  |  |  |  |  |  | else { | 
| 663 | 7 |  |  |  |  | 16 | for ($period) { | 
| 664 | 7 |  |  |  |  | 19 | when ('mon') { | 
| 665 | 0 |  |  |  |  | 0 | $date->truncate( to => 'month' ); | 
| 666 | 0 | 0 |  |  |  | 0 | if ($is_start) { | 
| 667 | 0 |  |  |  |  | 0 | $date->subtract( months => 1 ); | 
| 668 |  |  |  |  |  |  | } | 
| 669 |  |  |  |  |  |  | else { | 
| 670 | 0 |  |  |  |  | 0 | $date->subtract( days => 1 ); | 
| 671 |  |  |  |  |  |  | } | 
| 672 | 0 | 0 |  |  |  | 0 | $date->add( months => 2 ) if $d->{modifier} eq 'next'; | 
| 673 |  |  |  |  |  |  | } | 
| 674 | 7 |  |  |  |  | 16 | when ('wee') { | 
| 675 | 5 |  |  |  |  | 20 | my $is_sunday = $date->day_of_week == 7; | 
| 676 | 5 |  |  |  |  | 31 | $date->truncate( to => 'week' ); | 
| 677 | 5 | 50 |  |  |  | 4910 | if (sunday_begins_week) { | 
| 678 | 5 | 50 |  |  |  | 26 | $date->subtract( days => $is_sunday ? -6 : 1 ); | 
| 679 |  |  |  |  |  |  | } | 
| 680 | 5 | 100 |  |  |  | 4013 | if ($is_start) { | 
| 681 | 3 |  |  |  |  | 13 | $date->subtract( weeks => 1 ); | 
| 682 |  |  |  |  |  |  | } | 
| 683 |  |  |  |  |  |  | else { | 
| 684 | 2 |  |  |  |  | 10 | $date->subtract( days => 1 ); | 
| 685 |  |  |  |  |  |  | } | 
| 686 | 5 | 50 |  |  |  | 3703 | $date->add( days => 14 ) if $d->{modifier} eq 'next'; | 
| 687 |  |  |  |  |  |  | } | 
| 688 | 2 |  |  |  |  | 4 | when ('yea') { | 
| 689 | 2 |  |  |  |  | 8 | $date->truncate( to => 'year' ); | 
| 690 | 2 | 100 |  |  |  | 414 | if ($is_start) { | 
| 691 | 1 |  |  |  |  | 6 | $date->subtract( years => 1 ); | 
| 692 |  |  |  |  |  |  | } | 
| 693 |  |  |  |  |  |  | else { | 
| 694 | 1 |  |  |  |  | 4 | $date->subtract( days => 1 ); | 
| 695 |  |  |  |  |  |  | } | 
| 696 | 2 | 50 |  |  |  | 1215 | $date->add( years => 2 ) if $d->{modifier} eq 'next'; | 
| 697 |  |  |  |  |  |  | } | 
| 698 | 0 |  |  |  |  | 0 | when ('pay') { | 
| 699 | 0 |  |  |  |  | 0 | my $days = | 
| 700 |  |  |  |  |  |  | $date->delta_days(start_pay_period)->in_units('days'); | 
| 701 | 0 |  |  |  |  | 0 | $days %= pay_period_length; | 
| 702 | 0 |  |  |  |  | 0 | $date->subtract( days => $days ); | 
| 703 | 0 | 0 |  |  |  | 0 | if ($is_start) { | 
| 704 | 0 |  |  |  |  | 0 | $date->subtract( days => pay_period_length ); | 
| 705 |  |  |  |  |  |  | } | 
| 706 |  |  |  |  |  |  | else { | 
| 707 | 0 |  |  |  |  | 0 | $date->subtract( days => 1 ); | 
| 708 |  |  |  |  |  |  | } | 
| 709 |  |  |  |  |  |  | $date->add( days => 2 * pay_period_length ) | 
| 710 | 0 | 0 |  |  |  | 0 | if $d->{modifier} eq 'next'; | 
| 711 |  |  |  |  |  |  | } | 
| 712 |  |  |  |  |  |  | } | 
| 713 |  |  |  |  |  |  | } | 
| 714 | 17 |  |  |  |  | 2859 | return $date; | 
| 715 |  |  |  |  |  |  | } | 
| 716 |  |  |  |  |  |  |  | 
| 717 | 4 |  |  |  |  | 8 | init_month_abbr(); | 
| 718 | 4 |  |  |  |  | 14 | my $date = today; | 
| 719 | 4 |  |  |  |  | 48 | $date->truncate( to => 'month' ); | 
| 720 | 4 |  |  |  |  | 833 | my $month_num  = $month_abbr{ $d->{month} }; | 
| 721 | 4 |  |  |  |  | 12 | my $todays_num = $date->month; | 
| 722 | 4 | 100 |  |  |  | 26 | if ( $d->{modifier} eq 'this' ) { | 
| 723 | 2 |  |  |  |  | 4 | my $delta = 0; | 
| 724 | 2 | 50 |  |  |  | 10 | if ( $todays_num > $month_num ) { | 
|  |  | 50 |  |  |  |  |  | 
| 725 | 0 |  |  |  |  | 0 | $delta = 12 - $todays_num + $month_num; | 
| 726 |  |  |  |  |  |  | } | 
| 727 |  |  |  |  |  |  | elsif ( $todays_num < $month_num ) { | 
| 728 | 2 |  |  |  |  | 4 | $delta = $month_num - $todays_num; | 
| 729 |  |  |  |  |  |  | } | 
| 730 | 2 | 100 |  |  |  | 6 | $delta++ unless $is_start; | 
| 731 | 2 |  |  |  |  | 8 | $date->add( months => $delta ); | 
| 732 |  |  |  |  |  |  | } | 
| 733 |  |  |  |  |  |  | else { | 
| 734 | 2 |  |  |  |  | 4 | my $delta = 12; | 
| 735 | 2 | 50 |  |  |  | 10 | if ( $todays_num > $month_num ) { | 
|  |  | 50 |  |  |  |  |  | 
| 736 | 0 |  |  |  |  | 0 | $delta = $todays_num - $month_num; | 
| 737 |  |  |  |  |  |  | } | 
| 738 |  |  |  |  |  |  | elsif ( $todays_num < $month_num ) { | 
| 739 | 2 |  |  |  |  | 3 | $delta -= $month_num - $todays_num; | 
| 740 |  |  |  |  |  |  | } | 
| 741 | 2 | 100 |  |  |  | 6 | $delta-- unless $is_start; | 
| 742 | 2 |  |  |  |  | 8 | $date->subtract( months => $delta ); | 
| 743 |  |  |  |  |  |  | } | 
| 744 | 4 | 100 |  |  |  | 2210 | $date->subtract( days => 1 ) unless $is_start; | 
| 745 | 4 |  |  |  |  | 1141 | return $date; | 
| 746 |  |  |  |  |  |  | } | 
| 747 |  |  |  |  |  |  |  | 
| 748 |  |  |  |  |  |  | # numeric date | 
| 749 | 187 |  |  |  |  | 306 | delete $d->{type}; | 
| 750 | 187 |  |  |  |  | 579 | return DateTime->new( time_zone => tz(), %$d ); | 
| 751 |  |  |  |  |  |  | } | 
| 752 |  |  |  |  |  |  |  | 
| 753 |  |  |  |  |  |  | # lazy initialization of verbal -> numeric month map | 
| 754 |  |  |  |  |  |  | sub init_month_abbr { | 
| 755 | 260 | 100 |  | 260 | 0 | 1129 | unless (%month_abbr) { | 
| 756 | 2 |  |  |  |  | 14 | my @months = qw(jan feb mar apr may jun jul aug sep oct nov dec); | 
| 757 | 2 |  |  |  |  | 9 | init_hash( \%month_abbr, \@months ); | 
| 758 |  |  |  |  |  |  | } | 
| 759 |  |  |  |  |  |  | } | 
| 760 |  |  |  |  |  |  |  | 
| 761 |  |  |  |  |  |  | # lazy initialization of verbal -> numeric day of week map | 
| 762 |  |  |  |  |  |  | sub init_day_abbr { | 
| 763 | 161 | 100 |  | 161 | 0 | 666 | unless (%day_abbr) { | 
| 764 | 1 |  |  |  |  | 5 | my @days = qw(mon tue wed thu fri sat sun); | 
| 765 | 1 |  |  |  |  | 4 | init_hash( \%day_abbr, \@days ); | 
| 766 |  |  |  |  |  |  | } | 
| 767 |  |  |  |  |  |  | } | 
| 768 |  |  |  |  |  |  |  | 
| 769 |  |  |  |  |  |  | # generic verbal -> numeric map generator | 
| 770 |  |  |  |  |  |  | sub init_hash { | 
| 771 | 3 |  |  | 3 | 0 | 7 | my ( $h, $units ) = @_; | 
| 772 | 3 |  |  |  |  | 11 | while (@$units) { | 
| 773 | 31 |  |  |  |  | 42 | my $i = @$units; | 
| 774 | 31 |  |  |  |  | 41 | my $u = pop @$units; | 
| 775 | 31 |  |  |  |  | 90 | $h->{$u} = $i; | 
| 776 |  |  |  |  |  |  | } | 
| 777 |  |  |  |  |  |  | } | 
| 778 |  |  |  |  |  |  |  | 
| 779 |  |  |  |  |  |  | # produces interpretation of date expression such that neither date ends after | 
| 780 |  |  |  |  |  |  | # the present | 
| 781 |  |  |  |  |  |  | sub before_now { | 
| 782 | 84 |  |  | 84 | 0 | 149 | my ( $h1, $h2, $two_endpoints ) = @_; | 
| 783 | 84 | 100 |  |  |  | 140 | infer_missing( $h1, $h2 ) if $two_endpoints; | 
| 784 | 84 |  |  |  |  | 288 | my $now = today; | 
| 785 | 84 |  |  |  |  | 1026 | my ( $u1, $amt1, $u2, $amt2 ) = ( time_unit($h1), time_unit($h2) ); | 
| 786 | 84 |  |  |  |  | 215 | ( $h1, $h2 ) = | 
| 787 |  |  |  |  |  |  | ( decontextualized_date( $h1, 1 ), decontextualized_date($h2) ); | 
| 788 | 84 | 100 |  |  |  | 317 | $h2 = adjust_weekday( $h2, $now ) unless ref $h2 eq 'DateTime'; | 
| 789 | 84 | 100 |  |  |  | 256 | $h1 = adjust_weekday( $h1, $now ) unless ref $h1 eq 'DateTime'; | 
| 790 | 84 |  |  |  |  | 300 | while ( $now < $h2 ) { | 
| 791 | 1 |  |  |  |  | 94 | $h2->subtract( $u2 => $amt2 ); | 
| 792 |  |  |  |  |  |  | } | 
| 793 | 84 |  |  |  |  | 8841 | while ( $h2 < $h1 ) { | 
| 794 | 1 |  |  |  |  | 90 | $h1->subtract( $u1 => $amt1 ); | 
| 795 |  |  |  |  |  |  | } | 
| 796 |  |  |  |  |  |  |  | 
| 797 | 84 | 100 |  |  |  | 8493 | if ($two_endpoints) { | 
| 798 |  |  |  |  |  |  |  | 
| 799 |  |  |  |  |  |  | # move the two dates as close together as possible | 
| 800 | 2 |  |  |  |  | 6 | while ( $h1 < $h2 ) { | 
| 801 | 2 |  |  |  |  | 175 | $h2->subtract( $u2 => $amt2 ); | 
| 802 |  |  |  |  |  |  | } | 
| 803 | 2 |  |  |  |  | 1416 | $h2->add( $u2 => $amt2 ); | 
| 804 |  |  |  |  |  |  | } | 
| 805 | 84 |  |  |  |  | 1375 | return $h1, $h2; | 
| 806 |  |  |  |  |  |  | } | 
| 807 |  |  |  |  |  |  |  | 
| 808 |  |  |  |  |  |  | # fill in missing fields in two date hashes, each using the other as context | 
| 809 |  |  |  |  |  |  | # this is a bit of a hack, but a natural hack | 
| 810 |  |  |  |  |  |  | sub infer_missing { | 
| 811 | 2 |  |  | 2 | 0 | 4 | my ( $h1, $h2 ) = @_; | 
| 812 | 2 | 50 |  |  |  | 10 | if ( $h1->{type} eq $h2->{type} ) { | 
|  |  | 50 |  |  |  |  |  | 
| 813 | 0 |  |  |  |  | 0 | while ( my ( $k, $v ) = each %$h1 ) { | 
| 814 | 0 |  | 0 |  |  | 0 | $h2->{$k} //= $v; | 
| 815 |  |  |  |  |  |  | } | 
| 816 | 0 |  |  |  |  | 0 | while ( my ( $k, $v ) = each %$h2 ) { | 
| 817 | 0 |  | 0 |  |  | 0 | $h1->{$k} //= $v; | 
| 818 |  |  |  |  |  |  | } | 
| 819 |  |  |  |  |  |  | } | 
| 820 |  |  |  |  |  |  | elsif ( $h2->{type} eq 'numeric' ) { | 
| 821 | 2 | 50 | 33 |  |  | 18 | if ( $h1->{month} && !$h2->{month} ) { | 
| 822 | 2 |  |  |  |  | 5 | init_month_abbr(); | 
| 823 | 2 |  |  |  |  | 6 | $h2->{month} = $month_abbr{ $h1->{month} }; | 
| 824 |  |  |  |  |  |  | } | 
| 825 |  |  |  |  |  |  | } | 
| 826 |  |  |  |  |  |  | else { | 
| 827 |  |  |  |  |  |  |  | 
| 828 |  |  |  |  |  |  | # I don't think we have any problems in this case | 
| 829 |  |  |  |  |  |  | } | 
| 830 |  |  |  |  |  |  | } | 
| 831 |  |  |  |  |  |  |  | 
| 832 |  |  |  |  |  |  | # normalizes string values | 
| 833 |  |  |  |  |  |  | sub normalize { | 
| 834 | 465 |  |  | 465 | 0 | 668 | my $h = shift; | 
| 835 | 465 |  |  |  |  | 732 | delete $h->{debug}; | 
| 836 | 465 | 100 |  |  |  | 1378 | if ( $h->{type} eq 'verbal' ) { | 
| 837 | 236 |  |  |  |  | 444 | for my $key (qw(day month period)) { | 
| 838 | 708 | 100 |  |  |  | 2063 | if ( my $value = $h->{$key} ) { | 
| 839 | 362 | 100 |  |  |  | 1226 | next if $value =~ /\d/; | 
| 840 | 236 |  |  |  |  | 448 | $value = lc $value; | 
| 841 | 236 | 100 |  |  |  | 641 | if ( $value =~ /^p/ ) { | 
| 842 | 3 | 50 |  |  |  | 9 | croak 'pay period not defined' | 
| 843 |  |  |  |  |  |  | unless defined start_pay_period; | 
| 844 | 3 |  |  |  |  | 512 | $h->{$key} = 'pay'; | 
| 845 |  |  |  |  |  |  | } | 
| 846 |  |  |  |  |  |  | else { | 
| 847 | 233 |  |  |  |  | 663 | $h->{$key} = substr $value, 0, 3; | 
| 848 |  |  |  |  |  |  | } | 
| 849 |  |  |  |  |  |  | } | 
| 850 |  |  |  |  |  |  | } | 
| 851 | 236 |  | 100 |  |  | 1277 | for ( $h->{modifier} || '' ) { | 
| 852 | 236 |  |  |  |  | 503 | when (/beg/) { $h->{modifier} = 'beginning' } | 
|  | 0 |  |  |  |  | 0 |  | 
| 853 | 236 |  |  |  |  | 351 | when (/end/) { $h->{modifier} = 'end' } | 
|  | 0 |  |  |  |  | 0 |  | 
| 854 | 236 |  |  |  |  | 401 | when (/las/) { $h->{modifier} = 'last' } | 
|  | 45 |  |  |  |  | 158 |  | 
| 855 | 191 |  |  |  |  | 219 | when (/thi/) { $h->{modifier} = 'this' } | 
|  | 3 |  |  |  |  | 11 |  | 
| 856 | 188 |  |  |  |  | 450 | when (/nex/) { $h->{modifier} = 'next' } | 
|  | 0 |  |  |  |  | 0 |  | 
| 857 |  |  |  |  |  |  | } | 
| 858 |  |  |  |  |  |  | } | 
| 859 |  |  |  |  |  |  | } | 
| 860 |  |  |  |  |  |  |  | 
| 861 |  |  |  |  |  |  | # whether the particular date expression refers to a fixed | 
| 862 |  |  |  |  |  |  | # rather than relative date | 
| 863 |  |  |  |  |  |  | sub is_fixed { | 
| 864 | 719 |  |  | 719 | 0 | 1002 | my $h = shift; | 
| 865 |  |  |  |  |  |  | return 1 | 
| 866 | 719 | 100 |  |  |  | 2197 | if exists $h->{year}; | 
| 867 | 289 | 100 |  |  |  | 803 | if ( $h->{type} eq 'verbal' ) { | 
| 868 | 207 | 100 |  |  |  | 446 | if ( exists $h->{modifier} ) { | 
| 869 | 101 | 50 |  |  |  | 689 | return 1 if $h->{modifier} =~ /this|last|next/; | 
| 870 |  |  |  |  |  |  | } | 
| 871 | 106 | 100 |  |  |  | 260 | if ( exists $h->{day} ) { | 
| 872 | 100 | 100 |  |  |  | 497 | return 1 if $h->{day} =~ /yes|tod|tom/; | 
| 873 |  |  |  |  |  |  | } | 
| 874 |  |  |  |  |  |  | } | 
| 875 | 173 |  |  |  |  | 496 | return 0; | 
| 876 |  |  |  |  |  |  | } | 
| 877 |  |  |  |  |  |  |  | 
| 878 |  |  |  |  |  |  | 1; | 
| 879 |  |  |  |  |  |  |  | 
| 880 |  |  |  |  |  |  | __END__ |