| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::JobLog::Command::summary; | 
| 2 |  |  |  |  |  |  | $App::JobLog::Command::summary::VERSION = '1.040'; | 
| 3 |  |  |  |  |  |  | # ABSTRACT: show what you did during a particular period | 
| 4 |  |  |  |  |  |  |  | 
| 5 | 2 |  |  | 2 |  | 1826 | use App::JobLog -command; | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 14 |  | 
| 6 | 2 |  |  | 2 |  | 538 | use Modern::Perl; | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 12 |  | 
| 7 | 2 |  |  |  |  | 13 | use Class::Autouse qw( | 
| 8 |  |  |  |  |  |  | App::JobLog::Log | 
| 9 |  |  |  |  |  |  | App::JobLog::Log::Day | 
| 10 | 2 |  |  | 2 |  | 218 | ); | 
|  | 2 |  |  |  |  | 4 |  | 
| 11 | 2 |  |  | 2 |  | 307 | use autouse 'App::JobLog::TimeGrammar'  => qw(parse daytime); | 
|  | 2 |  |  |  |  | 6 |  | 
|  | 2 |  |  |  |  | 13 |  | 
| 12 | 2 |  |  | 2 |  | 134 | use autouse 'Carp'                      => qw(carp); | 
|  | 2 |  |  |  |  | 4 |  | 
|  | 2 |  |  |  |  | 9 |  | 
| 13 | 2 |  |  | 2 |  | 160 | use autouse 'Getopt::Long::Descriptive' => qw(prog_name); | 
|  | 2 |  |  |  |  | 3 |  | 
|  | 2 |  |  |  |  | 14 |  | 
| 14 | 2 |  |  |  |  | 9 | use autouse 'App::JobLog::Config'       => qw( | 
| 15 |  |  |  |  |  |  | columns | 
| 16 |  |  |  |  |  |  | is_hidden | 
| 17 |  |  |  |  |  |  | merge | 
| 18 | 2 |  |  | 2 |  | 129 | ); | 
|  | 2 |  |  |  |  | 4 |  | 
| 19 | 2 |  |  |  |  | 8 | use autouse 'App::JobLog::Log::Format' => qw( | 
| 20 |  |  |  |  |  |  | display | 
| 21 |  |  |  |  |  |  | single_interval | 
| 22 |  |  |  |  |  |  | summary | 
| 23 | 2 |  |  | 2 |  | 248 | ); | 
|  | 2 |  |  |  |  | 9 |  | 
| 24 | 2 |  |  |  |  | 9 | use autouse 'App::JobLog::Log::Synopsis' => qw( | 
| 25 |  |  |  |  |  |  | MERGE_ALL | 
| 26 |  |  |  |  |  |  | MERGE_ADJACENT | 
| 27 |  |  |  |  |  |  | MERGE_ADJACENT_SAME_TAGS | 
| 28 |  |  |  |  |  |  | MERGE_SAME_TAGS | 
| 29 |  |  |  |  |  |  | MERGE_SAME_DAY | 
| 30 |  |  |  |  |  |  | MERGE_SAME_DAY_SAME_TAGS | 
| 31 |  |  |  |  |  |  | MERGE_NONE | 
| 32 | 2 |  |  | 2 |  | 229 | ); | 
|  | 2 |  |  |  |  | 8 |  | 
| 33 | 2 |  |  | 2 |  | 367 | use autouse 'App::JobLog::Time' => qw(today); | 
|  | 2 |  |  |  |  | 5 |  | 
|  | 2 |  |  |  |  | 7 |  | 
| 34 | 2 |  |  | 2 |  | 162 | no if $] >= 5.018, warnings => "experimental::smartmatch"; | 
|  | 2 |  |  |  |  | 5 |  | 
|  | 2 |  |  |  |  | 12 |  | 
| 35 |  |  |  |  |  |  |  | 
| 36 |  |  |  |  |  |  | sub execute { | 
| 37 | 15 |  |  | 15 | 1 | 57 | my ( $self, $opt, $args ) = @_; | 
| 38 |  |  |  |  |  |  |  | 
| 39 | 15 |  | 100 |  |  | 102 | my $tags          = $opt->{tag}         || []; | 
| 40 | 15 |  | 50 |  |  | 121 | my $excluded_tags = $opt->{exclude_tag} || []; | 
| 41 | 15 |  | 50 |  |  | 113 | my $match         = $opt->{match}       || []; | 
| 42 | 15 |  | 50 |  |  | 129 | my $no_match      = $opt->{no_match}    || []; | 
| 43 | 15 |  | 33 |  |  | 79 | my $time_expr = join( ' ', @$args ) || $opt->{date}; | 
| 44 | 15 |  |  |  |  | 36 | my $time = $opt->{time}; | 
| 45 |  |  |  |  |  |  |  | 
| 46 | 15 |  | 33 |  |  | 50 | $time_expr ||= $opt->{date}; | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | # validate regexes, if any, while generating test | 
| 49 |  |  |  |  |  |  |  | 
| 50 |  |  |  |  |  |  | # NOTE: using $opt->{x} form rather than $opt->x to facilitate invoking summary | 
| 51 |  |  |  |  |  |  | # from today command | 
| 52 |  |  |  |  |  |  |  | 
| 53 | 15 |  |  |  |  | 78 | my $test = _make_test( $tags, $excluded_tags, $match, $no_match, $time ); | 
| 54 | 15 |  |  |  |  | 33 | my $merge_level; | 
| 55 | 15 |  | 100 |  |  | 93 | for ( $opt->{merge} || '' ) { | 
| 56 | 15 |  |  |  |  | 62 | when ('no_merge') { | 
| 57 | 11 |  |  |  |  | 107 | $merge_level = MERGE_NONE | 
| 58 |  |  |  |  |  |  | } | 
| 59 | 4 |  |  |  |  | 11 | when ('merge_all') { | 
| 60 | 0 |  |  |  |  | 0 | $merge_level = MERGE_ALL | 
| 61 |  |  |  |  |  |  | } | 
| 62 | 4 |  |  |  |  | 9 | when ('merge_adjacent') { | 
| 63 | 0 |  |  |  |  | 0 | $merge_level = MERGE_ADJACENT | 
| 64 |  |  |  |  |  |  | } | 
| 65 | 4 |  |  |  |  | 9 | when ('merge_adjacent_same_tags') { | 
| 66 | 0 |  |  |  |  | 0 | $merge_level = MERGE_ADJACENT_SAME_TAGS | 
| 67 |  |  |  |  |  |  | } | 
| 68 | 4 |  |  |  |  | 10 | when ('merge_same_tags') { | 
| 69 | 0 |  |  |  |  | 0 | $merge_level = MERGE_SAME_TAGS | 
| 70 |  |  |  |  |  |  | } | 
| 71 | 4 |  |  |  |  | 11 | when ('merge_same_day') { | 
| 72 | 0 |  |  |  |  | 0 | $merge_level = MERGE_SAME_DAY | 
| 73 |  |  |  |  |  |  | } | 
| 74 | 4 |  |  |  |  | 8 | when ('merge_same_day_same_tags') { | 
| 75 | 0 |  |  |  |  | 0 | $merge_level = MERGE_SAME_DAY_SAME_TAGS | 
| 76 |  |  |  |  |  |  | } | 
| 77 | 4 |  |  |  |  | 9 | default { | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | # some dark wizardry here | 
| 80 | 4 |  |  |  |  | 25 | my $m = uc merge; | 
| 81 | 4 |  |  |  |  | 22 | $m =~ s/ /_/g; | 
| 82 | 4 |  |  |  |  | 9 | $m           = \&{"MERGE_$m"}; | 
|  | 4 |  |  |  |  | 21 |  | 
| 83 | 4 |  |  |  |  | 18 | $merge_level = &$m; | 
| 84 |  |  |  |  |  |  | } | 
| 85 |  |  |  |  |  |  | } | 
| 86 | 15 |  | 33 |  |  | 521 | my $dateless = $merge_level == MERGE_ALL || $merge_level == MERGE_SAME_TAGS; | 
| 87 | 15 | 0 | 0 |  |  | 148 | if ( | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 88 |  |  |  |  |  |  | $opt->{no_totals} | 
| 89 |  |  |  |  |  |  | && ( $dateless || $opt->{no_date} || is_hidden('date') ) | 
| 90 |  |  |  |  |  |  | && (  !single_interval($merge_level) | 
| 91 |  |  |  |  |  |  | || $opt->{no_time} | 
| 92 |  |  |  |  |  |  | || is_hidden('time') ) | 
| 93 |  |  |  |  |  |  | && ( $opt->{no_duration}    || is_hidden('duration') ) | 
| 94 |  |  |  |  |  |  | && ( $opt->{no_tags}        || is_hidden('tags') ) | 
| 95 |  |  |  |  |  |  | && ( $opt->{no_description} || is_hidden('description') ) | 
| 96 |  |  |  |  |  |  | ) | 
| 97 |  |  |  |  |  |  | { | 
| 98 | 0 |  |  |  |  | 0 | $self->usage_error('you have chosen not to display anything'); | 
| 99 |  |  |  |  |  |  | } | 
| 100 |  |  |  |  |  |  |  | 
| 101 |  |  |  |  |  |  | # record hiding options in hash reference | 
| 102 |  |  |  |  |  |  | my $hidden = { | 
| 103 |  |  |  |  |  |  | vacation => $opt->{no_vacation} || $opt->{notes}, | 
| 104 |  |  |  |  |  |  | date => $dateless || $opt->{no_date} || is_hidden('date'), | 
| 105 |  |  |  |  |  |  | time => $opt->{no_time} || is_hidden('time'), | 
| 106 |  |  |  |  |  |  | duration => $opt->{notes} || $opt->{no_duration} || is_hidden('duration'), | 
| 107 |  |  |  |  |  |  | tags        => $opt->{no_tags}        || is_hidden('tags'), | 
| 108 |  |  |  |  |  |  | description => $opt->{no_description} || is_hidden('description'), | 
| 109 |  |  |  |  |  |  | totals      => $opt->{notes}          || $opt->{no_totals}, | 
| 110 | 15 |  | 33 |  |  | 210 | }; | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 66 |  |  |  |  | 
| 111 |  |  |  |  |  |  |  | 
| 112 |  |  |  |  |  |  | # parse time expression | 
| 113 | 15 |  |  |  |  | 39 | my ( $days, $show_year ); | 
| 114 | 15 |  |  |  |  | 29 | eval { | 
| 115 | 15 |  |  |  |  | 111 | ( $days, $show_year ) = summary $time_expr, $test, $hidden, $opt->{notes}; | 
| 116 |  |  |  |  |  |  | }; | 
| 117 | 15 | 50 |  |  |  | 85 | $self->usage_error($@) if $@; | 
| 118 | 15 | 50 |  |  |  | 94 | unless ( $opt->{hidden} ) { | 
| 119 |  |  |  |  |  |  |  | 
| 120 |  |  |  |  |  |  | # figure out how wide to make things | 
| 121 | 15 |  |  |  |  | 30 | my $screen_width; | 
| 122 | 15 | 100 |  |  |  | 67 | if ( $opt->{wrap} ) { | 
| 123 | 3 | 50 |  |  |  | 10 | if ( $opt->{no_wrap} ) { | 
| 124 | 3 |  |  |  |  | 10 | $screen_width = -1; | 
| 125 |  |  |  |  |  |  | } | 
| 126 |  |  |  |  |  |  | else { | 
| 127 | 0 |  |  |  |  | 0 | $screen_width = $opt->columns; | 
| 128 |  |  |  |  |  |  | } | 
| 129 |  |  |  |  |  |  | } | 
| 130 |  |  |  |  |  |  | else { | 
| 131 | 12 |  |  |  |  | 61 | $screen_width = columns; | 
| 132 |  |  |  |  |  |  | } | 
| 133 | 15 | 50 |  |  |  | 133 | if ($dateless) { | 
| 134 |  |  |  |  |  |  |  | 
| 135 |  |  |  |  |  |  | # create "day" containing all events | 
| 136 | 0 |  |  |  |  | 0 | my $duck_day = App::JobLog::Log::Day->new( | 
| 137 |  |  |  |  |  |  | start   => $days->[0]->start->clone, | 
| 138 |  |  |  |  |  |  | end     => $days->[$#$days]->end->clone, | 
| 139 |  |  |  |  |  |  | no_date => 1, | 
| 140 |  |  |  |  |  |  | ); | 
| 141 | 0 |  |  |  |  | 0 | for my $d (@$days) { | 
| 142 | 0 |  |  |  |  | 0 | push @{ $duck_day->events },   @{ $d->events }; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 143 | 0 |  |  |  |  | 0 | push @{ $duck_day->vacation }, @{ $d->vacation }; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 144 |  |  |  |  |  |  | } | 
| 145 | 0 |  |  |  |  | 0 | display [$duck_day], $merge_level, $hidden, $screen_width; | 
| 146 |  |  |  |  |  |  | } | 
| 147 |  |  |  |  |  |  | else { | 
| 148 | 15 |  |  |  |  | 281 | display $days, $merge_level, $hidden, $screen_width, $show_year; | 
| 149 |  |  |  |  |  |  | } | 
| 150 |  |  |  |  |  |  |  | 
| 151 |  |  |  |  |  |  | # check for long task | 
| 152 | 15 |  |  |  |  | 912 | my ($last_e) = App::JobLog::Log->new->last_event; | 
| 153 | 15 | 50 | 33 |  |  | 127 | if ( $last_e && $last_e->is_open ) { | 
| 154 | 15 |  |  |  |  | 81 | my ( $then, $today ) = ( $last_e->start, today ); | 
| 155 | 15 | 50 | 66 |  |  | 332 | if ( | 
| 156 |  |  |  |  |  |  | !( | 
| 157 |  |  |  |  |  |  | $then->year == $today->year | 
| 158 |  |  |  |  |  |  | && $then->month == $today->month | 
| 159 |  |  |  |  |  |  | && $then->day == $today->day | 
| 160 |  |  |  |  |  |  | ) | 
| 161 |  |  |  |  |  |  | ) | 
| 162 |  |  |  |  |  |  | { | 
| 163 | 15 |  |  |  |  | 358 | print < | 
| 164 |  |  |  |  |  |  |  | 
| 165 |  |  |  |  |  |  | WARNING! The last event in the log has been open since before 12:00 am today! | 
| 166 |  |  |  |  |  |  |  | 
| 167 |  |  |  |  |  |  | END | 
| 168 |  |  |  |  |  |  | } | 
| 169 |  |  |  |  |  |  | } | 
| 170 |  |  |  |  |  |  | } | 
| 171 |  |  |  |  |  |  | } | 
| 172 |  |  |  |  |  |  |  | 
| 173 |  |  |  |  |  |  | # Construct a test from the tags, excluded-tags, match, no-match, and time options. | 
| 174 |  |  |  |  |  |  | # The test determines what portion of what events are included in synopses. | 
| 175 |  |  |  |  |  |  | sub _make_test { | 
| 176 | 15 |  |  | 15 |  | 48 | my ( $tags, $excluded_tags, $match, $no_match, $time ) = @_; | 
| 177 |  |  |  |  |  |  |  | 
| 178 | 15 |  |  |  |  | 52 | my %tags          = map { $_ => 1 } @$tags; | 
|  | 8 |  |  |  |  | 33 |  | 
| 179 | 15 |  |  |  |  | 47 | my %excluded_tags = map { $_ => 1 } @$excluded_tags; | 
|  | 0 |  |  |  |  | 0 |  | 
| 180 | 15 |  |  |  |  | 44 | my @no_match = map { _re_test($_); qr/$_/ } @$no_match; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 181 | 15 |  |  |  |  | 43 | my @match    = map { _re_test($_); qr/$_/ } @$match; | 
|  | 0 |  |  |  |  | 0 |  | 
|  | 0 |  |  |  |  | 0 |  | 
| 182 | 15 |  |  |  |  | 71 | $time = _parse_time($time); | 
| 183 | 15 | 50 | 66 |  |  | 199 | return unless %tags || %excluded_tags || @no_match || @match || $time; | 
|  |  |  | 66 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
|  |  |  | 33 |  |  |  |  | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | my $test = sub { | 
| 186 | 8 |  |  | 8 |  | 22 | my ($e) = @_; | 
| 187 | 8 | 50 | 33 |  |  | 53 | if ( %tags || %excluded_tags ) { | 
| 188 | 8 |  |  |  |  | 22 | my $good = !%tags; | 
| 189 | 8 |  |  |  |  | 17 | for my $t ( @{ $e->tags } ) { | 
|  | 8 |  |  |  |  | 43 |  | 
| 190 | 9 | 50 |  |  |  | 33 | return if $excluded_tags{$t}; | 
| 191 | 9 |  | 100 |  |  | 73 | $good ||= $tags{$t}; | 
| 192 |  |  |  |  |  |  | } | 
| 193 | 8 | 100 |  |  |  | 36 | return unless $good; | 
| 194 |  |  |  |  |  |  | } | 
| 195 | 6 | 50 | 33 |  |  | 58 | if ( @no_match || @match ) { | 
| 196 | 0 |  |  |  |  | 0 | my $good = !@match; | 
| 197 | 0 |  |  |  |  | 0 | for my $d ( @{ $e->data->description } ) { | 
|  | 0 |  |  |  |  | 0 |  | 
| 198 | 0 |  |  |  |  | 0 | for my $re (@no_match) { | 
| 199 | 0 | 0 |  |  |  | 0 | return if $d =~ $re; | 
| 200 |  |  |  |  |  |  | } | 
| 201 | 0 | 0 |  |  |  | 0 | unless ($good) { | 
| 202 | 0 |  |  |  |  | 0 | for my $re (@match) { | 
| 203 | 0 |  |  |  |  | 0 | $good = $d =~ $re; | 
| 204 | 0 | 0 |  |  |  | 0 | last if $good; | 
| 205 |  |  |  |  |  |  | } | 
| 206 |  |  |  |  |  |  | } | 
| 207 |  |  |  |  |  |  | } | 
| 208 | 0 | 0 |  |  |  | 0 | return unless $good; | 
| 209 |  |  |  |  |  |  | } | 
| 210 | 6 | 50 |  |  |  | 23 | if ($time) { | 
| 211 | 0 |  |  |  |  | 0 | my $start = $e->start->clone->set( %{ $time->{start} } ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 212 | 0 |  |  |  |  | 0 | my $end   = $e->end->clone->set( %{ $time->{end} } ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 213 | 0 |  |  |  |  | 0 | return $e->overlap( $start, $end ); | 
| 214 |  |  |  |  |  |  | } | 
| 215 | 6 |  |  |  |  | 36 | return $e; | 
| 216 | 6 |  |  |  |  | 65 | }; | 
| 217 | 6 |  |  |  |  | 27 | return $test; | 
| 218 |  |  |  |  |  |  | } | 
| 219 |  |  |  |  |  |  |  | 
| 220 |  |  |  |  |  |  | # look for regular expressions with side effects | 
| 221 |  |  |  |  |  |  | sub _re_test { | 
| 222 | 0 | 0 |  | 0 |  | 0 | carp 'regex ' . $_[0] . '" appears to contain executable code' | 
| 223 |  |  |  |  |  |  | if $_[0] =~ /\(\?{1,2}{/; | 
| 224 |  |  |  |  |  |  | } | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | # parse time expressions | 
| 227 |  |  |  |  |  |  | our ( $b1, $b2 ); | 
| 228 |  |  |  |  |  |  | my $time_re = qr/ | 
| 229 |  |  |  |  |  |  | ^ \s*+ (?&start) (?&end) \s*+ $ | 
| 230 |  |  |  |  |  |  | (?(DEFINE) | 
| 231 |  |  |  |  |  |  | (? (?&ba) | (?&time) ) | 
| 232 |  |  |  |  |  |  | (? (?:(?&before)|(?&after)) \s*+) | 
| 233 |  |  |  |  |  |  | (? (?: b(?:e(?:f(?:o(?:r(?:e)?)?)?)?)? | < ) (?{$b1 = 'before'})) | 
| 234 |  |  |  |  |  |  | (? (?: a(?:f(?:t(?:e(?:r)?)?)?)? | > ) (?{$b1 = 'after'})) | 
| 235 |  |  |  |  |  |  | (? | 
| 236 |  |  |  |  |  |  | (? (\S.*) (?{$b2 = $^N})) | 
| 237 |  |  |  |  |  |  | ) | 
| 238 |  |  |  |  |  |  | /xi; | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  | sub _parse_time { | 
| 241 | 15 |  |  | 15 |  | 36 | my ($time) = @_; | 
| 242 | 15 |  |  |  |  | 58 | local ( $b1, $b2 ); | 
| 243 | 15 | 50 |  |  |  | 61 | return unless $time; | 
| 244 | 0 | 0 |  |  |  | 0 | if ( $time =~ $time_re ) { | 
| 245 | 0 |  |  |  |  | 0 | my ( $t1, $t2 ); | 
| 246 | 0 |  |  |  |  | 0 | for ($b1) { | 
| 247 | 0 |  |  |  |  | 0 | when ('before') { | 
| 248 | 0 |  |  |  |  | 0 | $t1 = { | 
| 249 |  |  |  |  |  |  | hour   => 0, | 
| 250 |  |  |  |  |  |  | minute => 0, | 
| 251 |  |  |  |  |  |  | second => 0 | 
| 252 |  |  |  |  |  |  | }; | 
| 253 | 0 |  |  |  |  | 0 | $t2 = { daytime $b2 }; | 
| 254 |  |  |  |  |  |  | } | 
| 255 | 0 |  |  |  |  | 0 | when ('after') { | 
| 256 | 0 |  |  |  |  | 0 | $t1 = { daytime $b2 }; | 
| 257 | 0 |  |  |  |  | 0 | $t2 = { | 
| 258 |  |  |  |  |  |  | hour   => 23, | 
| 259 |  |  |  |  |  |  | minute => 59, | 
| 260 |  |  |  |  |  |  | second => 59 | 
| 261 |  |  |  |  |  |  | }; | 
| 262 |  |  |  |  |  |  | } | 
| 263 | 0 |  |  |  |  | 0 | default { | 
| 264 | 0 |  |  |  |  | 0 | $t1 = { daytime $b1 }; | 
| 265 | 0 |  |  |  |  | 0 | $t2 = { daytime $b2 }; | 
| 266 |  |  |  |  |  |  | } | 
| 267 |  |  |  |  |  |  | } | 
| 268 | 0 | 0 | 0 |  |  | 0 | if (  $t2->{hour} < $t1->{hour} | 
|  |  |  | 0 |  |  |  |  | 
| 269 |  |  |  |  |  |  | || $t2->{minute} < $t1->{minute} | 
| 270 |  |  |  |  |  |  | || $t2->{second} < $t1->{second} ) | 
| 271 |  |  |  |  |  |  | { | 
| 272 | 0 | 0 | 0 |  |  | 0 | if ( $t2->{suffix} && $t2->{suffix} eq 'x' ) { | 
| 273 | 0 |  |  |  |  | 0 | $t2->{hour} += 12; | 
| 274 |  |  |  |  |  |  | } | 
| 275 |  |  |  |  |  |  | else { | 
| 276 | 0 |  |  |  |  | 0 | carp '"' . $time | 
| 277 |  |  |  |  |  |  | . '" invalid time expression: endpoints out of order'; | 
| 278 |  |  |  |  |  |  | } | 
| 279 |  |  |  |  |  |  | } | 
| 280 | 0 |  |  |  |  | 0 | delete $t1->{suffix}, delete $t2->{suffix}; | 
| 281 | 0 |  |  |  |  | 0 | return { start => $t1, end => $t2 }; | 
| 282 |  |  |  |  |  |  | } | 
| 283 |  |  |  |  |  |  | } | 
| 284 |  |  |  |  |  |  |  | 
| 285 | 4 |  |  | 4 | 1 | 59980 | sub usage_desc { '%c ' . __PACKAGE__->name . ' %o []' } | 
| 286 |  |  |  |  |  |  |  | 
| 287 |  |  |  |  |  |  | sub abstract { | 
| 288 | 0 |  |  | 0 | 1 | 0 | 'list tasks with certain properties in a particular time range'; | 
| 289 |  |  |  |  |  |  | } | 
| 290 |  |  |  |  |  |  |  | 
| 291 |  |  |  |  |  |  | sub full_description { | 
| 292 |  |  |  |  |  |  | < | 
| 293 |  |  |  |  |  |  | List events or notes with certain properties in a particular time range. Only the notes or | 
| 294 |  |  |  |  |  |  | portions of events falling within the range will be listed. | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | Events and notes may be filtered in numerous ways: by tag, time of day, or terms used in descriptions. | 
| 297 |  |  |  |  |  |  | If tags to match are provided, only those items that contain at least one such tag will be shown. If | 
| 298 |  |  |  |  |  |  | tags not to match are provided, only those items that contain none of these tags will be shown. | 
| 299 |  |  |  |  |  |  |  | 
| 300 |  |  |  |  |  |  | If you provide description filters to match or avoid, these will be interpreted as regexes. Try 'perldoc perlre' | 
| 301 |  |  |  |  |  |  | for more details, or perhaps 'perldoc perlretut' (these will only work if you have the Perl documentation | 
| 302 |  |  |  |  |  |  | installed on your machine). If you don't want to worry about regular expressions, simple strings will work. | 
| 303 |  |  |  |  |  |  | Prefix your expression with '(?i)' to turn off case sensitivity. And don't enclose regexes in slashes or any other | 
| 304 |  |  |  |  |  |  | sort of delimiter. Use 'ab', not '/ab/' or 'm!ab!', etc. Finally, you may need to enclose your regexes in quotes | 
| 305 |  |  |  |  |  |  | to prevent the shell from trying to interpret them. | 
| 306 |  |  |  |  |  |  |  | 
| 307 |  |  |  |  |  |  | Time subranges may be of the form '11-12pm', '1am-12:30:15', 'before 2', 'after 6:12pm', etc. Either 'before' | 
| 308 |  |  |  |  |  |  | or 'after' (or some prefix of these such as 'bef' or 'aft') may be followed by a time or you may use two time | 
| 309 |  |  |  |  |  |  | expressions separated by a dash. The code will attempt to infer the precise time of ambiguous time expressions, | 
| 310 |  |  |  |  |  |  | but it's best to be explicit. Case is ignored. Whitespace is optional in the expected places. | 
| 311 |  |  |  |  |  |  |  | 
| 312 |  |  |  |  |  |  | Note that any filtering of events specifying particular times for the start and end of the period in question, | 
| 313 |  |  |  |  |  |  | e.g., "yesterday at 8:00 am until today", will cause all flex time vacation to be ignored. This is because, given | 
| 314 |  |  |  |  |  |  | the flexible nature of this vacation, it is unclear how much should be accounted for when filtering events. Since | 
| 315 |  |  |  |  |  |  | notes are not "on the clock", no consideration of vacation periods is used in filtering them. | 
| 316 |  |  |  |  |  |  |  | 
| 317 | 0 |  |  |  |  | 0 | @{[__PACKAGE__->name]} provides many ways to consolidate events and notes. These are the "merge" options | 
| 318 |  |  |  |  |  |  | By default items are grouped into days and within days into subgroups of adjacent items with the same tags. | 
| 319 |  |  |  |  |  |  | All the merge options that require adjacency will also group by days but not vice versa. | 
| 320 |  |  |  |  |  |  | END | 
| 321 | 0 |  |  | 0 | 0 | 0 | } | 
| 322 |  |  |  |  |  |  |  | 
| 323 |  |  |  |  |  |  | sub options { | 
| 324 |  |  |  |  |  |  | return ( | 
| 325 |  |  |  |  |  |  | [ | 
| 326 | 4 |  |  | 4 | 0 | 9 | "Use '@{[prog_name]} help " | 
|  | 4 |  |  |  |  | 21 |  | 
| 327 |  |  |  |  |  |  | . __PACKAGE__->name | 
| 328 |  |  |  |  |  |  | . '\' to see full details.' | 
| 329 |  |  |  |  |  |  | ], | 
| 330 |  |  |  |  |  |  | [], | 
| 331 |  |  |  |  |  |  | [ | 
| 332 |  |  |  |  |  |  | 'date|d=s', | 
| 333 |  |  |  |  |  |  | 'provide the time expression as an option instead of an argument' | 
| 334 |  |  |  |  |  |  | ], | 
| 335 |  |  |  |  |  |  | [ 'notes|n', 'show notes instead of events' ], | 
| 336 |  |  |  |  |  |  | [ | 
| 337 |  |  |  |  |  |  | 'tag|t=s@', | 
| 338 |  |  |  |  |  |  | 'filter events/notes to include only those with given tags; ' | 
| 339 |  |  |  |  |  |  | . 'multiple tags may be specified' | 
| 340 |  |  |  |  |  |  | ], | 
| 341 |  |  |  |  |  |  | [ | 
| 342 |  |  |  |  |  |  | 'exclude-tag|T=s@', | 
| 343 |  |  |  |  |  |  | 'filter events/notes to exclude those with given tags; ' | 
| 344 |  |  |  |  |  |  | . 'multiple tags may be specified' | 
| 345 |  |  |  |  |  |  | ], | 
| 346 |  |  |  |  |  |  | [ | 
| 347 |  |  |  |  |  |  | 'match|m=s@', | 
| 348 |  |  |  |  |  |  | 'filter events/notes to include only those one of whose descriptions matches the given regex; ' | 
| 349 |  |  |  |  |  |  | . 'multiple regexes may be specified' | 
| 350 |  |  |  |  |  |  | ], | 
| 351 |  |  |  |  |  |  | [ | 
| 352 |  |  |  |  |  |  | 'no-match|M=s@', | 
| 353 |  |  |  |  |  |  | 'filter events/notes to include only those one of whose descriptions do not match the given regex; ' | 
| 354 |  |  |  |  |  |  | . 'multiple regexes may be specified' | 
| 355 |  |  |  |  |  |  | ], | 
| 356 |  |  |  |  |  |  | [ | 
| 357 |  |  |  |  |  |  | 'time|i=s', | 
| 358 |  |  |  |  |  |  | 'consider only those portions of events/notes that overlap the given time range' | 
| 359 |  |  |  |  |  |  | ], | 
| 360 |  |  |  |  |  |  | [ | 
| 361 |  |  |  |  |  |  | "merge" => hidden => { | 
| 362 |  |  |  |  |  |  | one_of => [ | 
| 363 |  |  |  |  |  |  | [ | 
| 364 |  |  |  |  |  |  | "merge-all|mall|ma" => | 
| 365 |  |  |  |  |  |  | "glom all events/notes into one synopsis" | 
| 366 |  |  |  |  |  |  | ], | 
| 367 |  |  |  |  |  |  | [ "merge-adjacent|madj" => "merge contiguous events" ], | 
| 368 |  |  |  |  |  |  | [ | 
| 369 |  |  |  |  |  |  | "merge-adjacent-same-tags|mast" => | 
| 370 |  |  |  |  |  |  | "merge contiguous, identically-tagged events/notes (default)" | 
| 371 |  |  |  |  |  |  | ], | 
| 372 |  |  |  |  |  |  | [ | 
| 373 |  |  |  |  |  |  | "merge-same-tags|mst" => | 
| 374 |  |  |  |  |  |  | "merge all identically tagged events/notes" | 
| 375 |  |  |  |  |  |  | ], | 
| 376 |  |  |  |  |  |  | [ | 
| 377 |  |  |  |  |  |  | "merge-same-day|msd" => | 
| 378 |  |  |  |  |  |  | "merge all events/notes in a given day" | 
| 379 |  |  |  |  |  |  | ], | 
| 380 |  |  |  |  |  |  | [ | 
| 381 |  |  |  |  |  |  | "merge-same-day-same-tags|msdst" => | 
| 382 |  |  |  |  |  |  | "merge all events/notes in a given day" | 
| 383 |  |  |  |  |  |  | ], | 
| 384 |  |  |  |  |  |  | [ "no-merge|nm" => "keep all events/notes separate" ], | 
| 385 |  |  |  |  |  |  | ] | 
| 386 |  |  |  |  |  |  | } | 
| 387 |  |  |  |  |  |  | ], | 
| 388 |  |  |  |  |  |  | [ 'no-vacation|V', 'do not display vacation hours' ], | 
| 389 |  |  |  |  |  |  | [ 'no-date',       'do not display a date before each distinct day' ], | 
| 390 |  |  |  |  |  |  | [ | 
| 391 |  |  |  |  |  |  | 'no-time', | 
| 392 |  |  |  |  |  |  | 'do not display event or note start times and event end times' | 
| 393 |  |  |  |  |  |  | ], | 
| 394 |  |  |  |  |  |  | [ 'no-duration',    'do not display event durations' ], | 
| 395 |  |  |  |  |  |  | [ 'no-tags',        'do not display tags' ], | 
| 396 |  |  |  |  |  |  | [ 'no-description', 'do not display event/note descriptions' ], | 
| 397 |  |  |  |  |  |  | [ | 
| 398 |  |  |  |  |  |  | 'no-totals', | 
| 399 |  |  |  |  |  |  | 'do not display the footer containing total hours worked, etc.' | 
| 400 |  |  |  |  |  |  | ], | 
| 401 |  |  |  |  |  |  | [ | 
| 402 |  |  |  |  |  |  | 'wrap' => 'hidden' => { | 
| 403 |  |  |  |  |  |  | one_of => [ | 
| 404 |  |  |  |  |  |  | [ | 
| 405 |  |  |  |  |  |  | 'columns|c=i', | 
| 406 |  |  |  |  |  |  | 'limit the width of the report to the specified number of columns; ' | 
| 407 |  |  |  |  |  |  | . ' by default the width of the terminal is automatically detected and, if that fails, a width of 76 is used' | 
| 408 |  |  |  |  |  |  | ], | 
| 409 |  |  |  |  |  |  | [ 'no-wrap|W', 'do not wrap the text to fit columns' ], | 
| 410 |  |  |  |  |  |  | ] | 
| 411 |  |  |  |  |  |  | } | 
| 412 |  |  |  |  |  |  | ], | 
| 413 |  |  |  |  |  |  | [ 'hidden', 'display nothing', { hidden => 1 } ], | 
| 414 |  |  |  |  |  |  | ); | 
| 415 |  |  |  |  |  |  | } | 
| 416 |  |  |  |  |  |  |  | 
| 417 |  |  |  |  |  |  | sub validate { | 
| 418 | 4 |  |  | 4 | 0 | 12 | my ( $self, $opt, $args ) = @_; | 
| 419 |  |  |  |  |  |  |  | 
| 420 | 4 | 50 | 33 |  |  | 21 | $self->usage_error('no time expression provided') | 
| 421 |  |  |  |  |  |  | unless @$args || $opt->date; | 
| 422 | 4 | 50 | 33 |  |  | 28 | $self->usage_error('two time expression provided') if @$args && $opt->date; | 
| 423 |  |  |  |  |  |  | $self->usage_error('columns must be positive') | 
| 424 | 4 | 50 | 33 |  |  | 58 | if defined $opt->{columns} && $opt->columns < 1; | 
| 425 |  |  |  |  |  |  | } | 
| 426 |  |  |  |  |  |  |  | 
| 427 |  |  |  |  |  |  | 1; | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | __END__ |