Software Freedom Law Center

root/trunk/antimatter/tim/Modules/SFLC/TimeTracker/Input.pm

Revision 53, 33.6 kB (checked in by bkuhn, 9 months ago)
  • Added SFLC's internally developed tim bot released under AGPLv3
Line 
1 # Copyright (C) 2005, 2006   Software Freedom Law Center, Inc.
2 #  Author: Bradley M. Kuhn <bkuhn@softwarefreedom.org>
3 #
4 #  This software gives you freedom; it is licensed to you under version
5 #  3 of the GNU Affero General Public License.
6 #
7 #  This software is distributed WITHOUT ANY WARRANTY, without even the
8 #  implied warranties of MERCHANTABILITY and FITNESS FOR A PARTICULAR
9 #  PURPOSE.  See the GNU Affero General Public License for further
10 #  details.
11 #
12 # You should have received a copy of the GNU Affero General Public
13 # License, version 3 along with this software.  If not, see
14 # <http://www.gnu.org/licenses/>.
15 # Input.pm                                                         -*- Perl -*-
16 #  Input module for
17
18 package SFLC::TimeTracker::Input;
19
20 use strict;
21 use warnings;
22
23 require Exporter;
24
25 #use AutoLoader qw(AUTOLOAD);
26
27 our @ISA = qw(Exporter);
28
29 our @EXPORT_OK = (  );
30
31 our @EXPORT = qw( );
32
33 our $VERSION = '0.01';
34
35 my $SHORTEN_LINE_FOR_REOUTPUT = 15;
36
37 use Tree::Simple;
38
39 use Date::Manip;
40 use Data::Dumper;
41 use Carp;
42 use String::Approx 'amatch';
43
44
45 use SFLC::TimeTracker::Entry;
46 use SFLC::TimeTracker::Category;
47
48 use SFLC::TimeTracker::Input::CategoryParser::ResolutionNode;
49 use SFLC::TimeTracker::Input::CategoryParser;
50
51 use SFLC::TimeTracker::Question::Category;
52
53 use SFLC::TimeTracker::Output;
54 use SFLC::TimeTracker::Output::Dummy;
55 use SFLC::TimeTracker::Output::Statement;
56 use SFLC::TimeTracker::Output::Entry;
57
58 ###############################################################################
59 my $PARSE_WORD_LIMIT = 5;
60 my $IN_THE_BEGINNING_THERE_WAS_LIGHT = ParseDate("1975-11-02");
61
62 my %ERROR_MESSAGES = (
63      'TIME INVALID' =>
64         'Couldn\'t find a valid time data in "%s"',
65      'INTERNAL ERROR' =>
66         'REPORT THIS: your line "%s" yielded error of %s.',
67      'NOTHING STARTED' =>
68           'No record of a started entry; nothing was stopped when you said: "%s"',
69      'DUMMY' =>
70         'REPORT THIS: your line "%s" was considered %s!'
71 );
72
73 ###############################################################################
74
75 =head1 NAMEuse String::Approx 'amatch';
76
77
78 SFLC::TimeTrack::Input -
79
80 =head1 SYNOPSIS
81
82   use SFLC::TimeTrack::Input;
83
84 =head1 DESCRIPTION
85
86
87 =head2 EXPORT
88
89 None.
90
91 =head1 SEE ALSO
92
93 SFLC::TimeTrack::DB
94
95
96 =head1 EXTERNAL FUNCTIONS
97
98 =over
99
100 =cut
101
102 ###############################################################################
103
104 =item new
105
106 usage: new($database)
107
108 =back
109
110 =cut
111 our $DATABASE;
112
113 sub new {
114   my($class) = @_;
115
116   my $self = bless {}, $class;
117
118   return $self;
119 }
120 ###############################################################################
121 sub Initialize {
122   $DATABASE = shift;
123 }
124 sub getDatabase {
125   return $DATABASE;
126 }
127
128 ###############################################################################
129 sub _isTimeLine ($$$) {
130   my($entryDate, $line, $needFullParsing) = @_;
131
132   my($oP, $cP) = ('(', ')');
133   $oP = '(?:' unless $needFullParsing;
134
135   my $commandExp = $oP . 'pop|spent|start|push|add|adjust|worked(?:\s*on)?' .
136                    '|begin|started|ende?d?|last|stopp?e?d?(?:\s*on)?' . $cP;
137
138   my $stopExp = $oP . 'ende?d?|stopp?e?d?(?:\s*on)?' . $cP;
139   my $startExp = $oP . 'begin|starte?d?(?:\s*on|\s*at)?' . $cP;
140
141 #  If we allowed negative time ...
142 #  my $subDigitsExp = '\-?\d+[\d\.]*|\.\d+';
143   my $subDigitsExp = '\d+[\d\.]*|\.\d+';
144   my $timeDigitsExp = $oP  . $subDigitsExp . $cP;
145   my $timeExp = '(?:\[|for|at)?' . $oP . '(?:(?:' . $subDigitsExp . ')\s*' .
146      '(?:mi?nu?t?e?s?\s*|ho?u?rs?|seco?n?d?s?|da?ys?)\s*[\.,]?\s*)+(?:ago|later)?'
147                   . $cP . '\]?';
148   my $subClockExp = '\d{1,2}\s*(?::\s*\d{2,2}\s*)?(?:[ap]m?)?';
149   my $clockExp = $oP . $subClockExp  .$cP;
150   my $clockEntryExp = '(?:at|on|during)?\s*' . $clockExp;
151   my $clockRangeExp = '(?:from)?' . $clockExp .
152                      '\s*(?:\-+|to|until|through)\s*' . $clockExp;
153   my $catExp = '(?:on\s+|for\s+)?' . $oP . '[^\d\s][^\d]+' . $cP;
154
155   my $subDayExp = '(?:(?:last\s+)?\S+day|\d{1,2}\/\d{1,2}(?:\/\d{2,4})?)';
156   my $dayExp = $oP . $subDayExp . '\s*(?:\s+at\s+)?' . $subClockExp . $cP;
157
158   my($operation, $time, $startTime, $endTime);
159
160   print "parsing $line\n RE: $stopExp $clockEntryExp\n";
161   if ($line =~ /^\s*$stopExp\s+$dayExp\s*$/ix or
162            $line =~ /^\s*$stopExp\s+$clockEntryExp\s*$/ix) {
163     print "DOING DAY EXP $2\n";
164     if ($needFullParsing) {
165       $operation = 'stop';
166       $time = $2;
167       $time =~ s/\s+at\s+/ /i;
168       # We should assume they ment the last one that passed; no stops
169       #  should occur into the future
170       $time = "last $time" if $time =~ /^\s*(mon|tue|wed|thu|fri|sat|sun)/i;
171       $line = "";
172     }
173   } elsif ($line =~ /^\s*$startExp\s+$dayExp\s+$catExp\s*(.*)$/ix) {
174     $operation = 'start';
175     $time = $2;
176     $line = "$3 $4";
177   } elsif ($line =~ /^\s*$startExp\s+$clockEntryExp\s+$catExp\s*(.*)$/ix) {
178     $operation = 'start';
179     $time = $2;
180     $line = "$3 $4";
181   } elsif ($line =~ /^\s*$commandExp\s*$timeExp(.*)$/ix) {
182     if ($needFullParsing) {
183       $operation = "\L$1\E";
184       $time = "\L$2\E";
185       $line = $3;
186     }
187   } elsif ($line =~ /^\s*$clockRangeExp\s+$catExp\s*(.*)$/ix) {
188     if ($needFullParsing) {
189       $operation = "add";
190       $line = "$3 $4";
191
192       my(%times);
193       ($times{start}, $times{end}) = ($1, $2);
194
195       foreach my $key (qw/start end/) {
196         if (defined $entryDate and $entryDate !~ /^\s*$/) {
197           $times{$key} = "$entryDate $times{$key}";
198         }
199         my(@tokens) = split(/\s+/, $times{$key});
200         $times{$key} = ParseDate(\@tokens);
201         my $str = join(" ", @tokens);
202         $line = "$str $line" unless ($str =~ /^\s*$/)
203       }
204       $time = DateCalc($times{start}, $times{end});
205       if (defined $time) {
206         $startTime = $times{start};
207         $endTime = $times{end};
208       }
209     }
210   } elsif ($line =~ /^\s*$timeExp\s+$commandExp\s+(.*)$/ix) {
211     if ($needFullParsing) {
212       $time = "\L$1\E";
213       $operation = "\L$2\E";
214       $line = $3;
215     }
216   } elsif ($line =~ /^\s*$commandExp\s+$catExp\s*$timeExp(.*)$/ix) {
217     if ($needFullParsing) {
218       $operation = "\L$1\E";
219       $time = "\L$3\E";
220       $line = "$2 $4";
221     }
222   } elsif ($line =~ /^\s*$timeExp\s*(.*)$/i) {
223     if ($needFullParsing) {
224       $operation = "add";
225       $time = "\L$1\E";
226       $line = $2;
227     }
228   } elsif ($line =~ /^\s*$timeDigitsExp(?![,\-;\/])\s*(.*)$/i) {
229     if ($needFullParsing) {
230       $operation = "add";
231       $time = "$1 ASSUMED_UNIT_UNQUALIFIED";
232       $line = $2;
233     }
234   } elsif ($line =~ /^\s*$commandExp\s*$clockEntryExp(.*)$/ix) {
235     if ($needFullParsing) {
236       $operation = "\L$1\E";
237       $line = $3;
238       $startTime = $2;
239       unless (defined $startTime and $startTime !~ /^\s*$/) {
240         $line = "$startTime $line" if defined $startTime;
241         $startTime = undef;
242       }
243
244     }
245   } elsif ($line =~ /^\s*$clockEntryExp\s*$commandExp(.*)$/ix) {
246     if ($needFullParsing) {
247       $operation = "\L$2\E";
248       $line = $3;
249       $startTime = $1;
250       unless (defined $startTime and $startTime !~ /^\s*$/) {
251         $line = "$startTime $line" if defined $startTime;
252         $startTime = undef;
253       }
254
255     }
256   } elsif ($line =~ /^\s*$commandExp\s*$timeExp(.*)$/ix) {
257     if ($needFullParsing) {
258       $operation = "\L$1\E";
259       $time = "\L$2\E";
260       $line = $3;
261     }
262   } elsif ($line =~ /^\s*$commandExp\s*$timeDigitsExp(.*)$/ix) {
263     if ($needFullParsing) {
264       $operation = "\L$1\E";
265       $time = ($operation eq "pop") ? $2 : "$2 ASSUMED_UNIT_UNQUALIFIED";
266       $line = $3;
267     }
268   } elsif ($line =~ /^\s*$commandExp(.*)$/ix) {
269     if ($needFullParsing) {
270       $operation = "\L$1\E";
271       $line = $2;
272       $time = undef;
273     }
274   } elsif ($line =~ /^\s*$catExp\s*[\s,:\.]*$commandExp\s*$timeExp\s*(.*)$/ix) {
275     if ($needFullParsing) {
276       $operation = "\L$2\E";
277       $time = "\L$3\E";
278       $line = "$1 $4";
279     }
280   } elsif ($line =~ /^\s*$catExp\s*[\s,:\.]+\s*$timeExp\s*(.*)$/ix) {
281     if ($needFullParsing) {
282       $operation = "add";
283       $time = "\L$2\E";
284       $line = "$1 $3";
285     }
286   } else {
287     return undef;
288   }
289   if (not $needFullParsing) {
290     return {};
291   } else {
292     if ($operation =~  /^\s*worked/) {
293       $operation = "add";
294       $line =~ s/^\s*(?:on\s+)?(.*)\s+for\s+(.*)$/$2 $1/i;
295     } elsif ($operation eq "begin") {
296       $operation = "start";
297     } elsif ($operation =~ /^start/) {
298       $operation = "start";
299     } elsif ($operation =~ /^end/) {
300       $operation = "stop";
301     } elsif ($operation =~ /^stop/) {
302       $operation = "stop";
303     } elsif ($operation eq "last") {
304       $operation = "adjust";
305     } elsif ($operation eq "spent") {
306       $operation = "add";
307     }
308      while ($line =~ s/(?:^|\s+)(?:total|today|for|from|last)(?:\s+|$)/ /g) { } ;
309     if ($operation eq "stop") {
310       $endTime = $startTime;
311       $startTime = undef;
312     }
313     return ($needFullParsing ?
314              { operation => $operation, time => $time, rest => $line,
315                startTime => $startTime, endTime => $endTime }
316             : {});
317   }
318 }
319
320 ###############################################################################
321
322 sub _FindTimesInLine ($$) {
323   my($dateForEntry, $inputLine) = @_;
324
325   # remove trailing ; or "and" before we split on that
326
327   $inputLine =~ s/(?:;|and)\s*$//i;
328
329   my(@firstLineSet) = split(/(?:\s+[,\.;]?\s*and\s*[,\.;]?\s+|\s*;\s+)/i,
330                      $inputLine);
331
332   # Search through the split lines and try to find comma-seperated entries
333   # that seem to be seperate.
334   my @lines = ();
335   my($lastMatchIndex, $matchedCount) = (-1, 0);
336   foreach my $line (@firstLineSet) {
337     my(@subLines) = split(/\s*,\s*/i, $line);
338     foreach my $subLine (@subLines) {
339       # This is somewhat ineffecient here, because I process the lines twice; once to figure
340       # out if how the comma seperation should work and second to do the actual parsing
341       # ... but it avoids code duplication on the "hour detecting" stuff in _isTimeLine
342       if (defined _isTimeLine($dateForEntry, $subLine, 0)) {
343         push(@lines, $subLine);
344         $matchedCount++;  $lastMatchIndex = $#lines;
345       } else {
346         push(@lines, undef) if @lines == 0;
347         (defined $lines[$#lines]) ? ($lines[$#lines] .= ", $subLine") :
348                                     ($lines[$#lines] = $subLine);
349       }
350     }
351   }
352   # Special case if we found only one subline that has a match --- put it
353   # up front and everything else after it
354   if ($matchedCount == 1) {
355     my $str = $lines[$lastMatchIndex] . join(",", @lines[0.. ($lastMatchIndex -1)]);
356     $lastMatchIndex++;
357     $str .= join(",", @lines[$lastMatchIndex .. $#lines]) if ($lastMatchIndex <= $#lines);
358     $lines[0] = $str; $#lines = 0;
359   }
360
361
362   my %times;
363   my $rest = "";
364   foreach my $line (@lines) {
365     my $firstHash = _isTimeLine($dateForEntry, $line, 1);
366     if (defined $firstHash) {
367       $times{$line} = $firstHash;
368     } else {
369       ($rest =~ /^\s*$/) ? ($rest = $line) : ($rest .= $line);
370     }
371     # Ok, from this point on, we have a valid $operation
372   }
373   return { timeInfo => \%times, rest => $rest }
374 }
375 ###############################################################################
376 sub _operationDUMMY ($$$$$) {
377   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
378   $dateForEntry = "" unless defined $dateForEntry;
379   die "NO MORE DUMMYS\n";
380   return (["DUMMY", undef]);
381 }
382 ###############################################################################
383 sub _operationStart ($$$$$$) {
384   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
385
386   my $status = "";
387   my %entries;
388   $entries{completed} = [];
389   do {
390     my $thisEntries;
391     my(@l) =
392       $self->_operationStop($timeStamp, $dateForEntry, $buildData, $timeInfo);
393     ($status, $thisEntries) = @{$l[0]};
394     push(@{$entries{completed}}, @{$thisEntries->{completed}})
395       if ($status =~ /^STOPPED/ and @{$thisEntries->{completed}} >= 1);
396     push(@{$entries{abandoned}}, @{$thisEntries->{abandoned}})
397       if ($status =~ /^STOPPED/ and @{$thisEntries->{abandoned}} >= 1);
398   } until ($status !~  /^STOPPED/);
399
400   my $startTime;
401   foreach my $key (qw/startTime time/) {
402     $startTime = ParseDate($timeInfo->{$key})
403       if (defined $timeInfo->{$key} and $timeInfo->{$key} !~ /^\s*$/);
404     last if defined $startTime;
405   }
406   $startTime = $timeStamp if not defined $startTime;
407   unless (defined $startTime and $startTime !~ /^\s*$/) {
408     $status = "STARTED WITH CURRENT TIME DUE TO INVALID TIME";
409     $startTime = $timeStamp;
410   }
411
412   $entries{new} = [];
413   $entries{new}[0] = SFLC::TimeTracker::Entry::Create(
414                          userHandle => $buildData->{userHandle},
415                          dateOccurred => $timeStamp,
416                          startTime => $startTime);
417
418   return(["STARTED", \%entries]);
419 }
420 ###############################################################################
421 sub _operationStop ($$$$$) {
422   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
423
424   $self->getDatabase()->stackClear($buildData->{userHandle});
425
426   my $pending = $buildData->{pendingUserEntries};
427   my(@startedList);
428   foreach my $pp (keys %{$pending}) {
429     push(@startedList, $pending->{$pp})
430       if ($pending->{$pp}->needs("endTime") and
431           defined $pending->{$pp}->{startTime});
432   }
433
434   my $finishedEntry;
435   if (@startedList <= 0) {
436     return(["NOTHING STARTED", undef]);
437   }
438
439   my $status = "STOPPED WITH REQUESTED TIME";
440   my $stopTime;
441   my $triedThem = 0;
442   foreach my $key (qw/endTime time/) {
443     if (defined $timeInfo->{$key} and $timeInfo->{$key} !~ /^\s*$/) {
444       $stopTime = ParseDate($timeInfo->{$key});
445       print "Got stoptiem of $stopTime\n";
446       $triedThem = 1;
447     }
448     last if defined $stopTime;
449   }
450   unless (defined $stopTime and $stopTime !~ /^\s*$/) {
451     $stopTime = $timeStamp;
452     $status = $triedThem ? "STOPPED WITH CURRENT TIME DUE TO INVALID TIME"
453                          : "STOPPED";
454   }
455   my %entries; $entries{completed} = []; $entries{abandoned} = [];
456   foreach my $finishedEntry (@startedList) {
457     $finishedEntry->set("endTime", $stopTime);
458     if ($stopTime gt $finishedEntry->get('startTime')) {
459       push(@{$entries{completed}}, $finishedEntry);
460     } else {
461       push(@{$entries{abandoned}}, $finishedEntry);
462     }
463   }
464   return ([ $status, \%entries]);
465 }
466 ###############################################################################
467 sub _operationPush ($$$$$) {
468   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
469
470   #FIXME!!!
471   return $self->_operationStop($timeStamp,
472                                 $dateForEntry, $buildData, $timeInfo);
473
474   my @answers;
475
476   my($status, $category) = ("", undef);
477
478   my %entries = ();     $entries{completed} = [];
479   my $thisEntries;
480   my(@l) =
481       $self->_operationStart($timeStamp, $dateForEntry, $buildData, $timeInfo);
482
483   my(@dead);
484   my $ii = -1;
485   my $started;
486   foreach my $ar (@l) {
487     $ii++;
488     push(@dead, @{$ar->[1]->{completed}});
489     $started = $ii if $ar->[0] eq "STARTED";
490   }
491
492   if (@dead > 0) {
493     @dead = sort { $a->{startTime} cmp $b->{startTime} } @dead;
494     $self->getDatabase()->stackPush($dead[0]);
495   } else {
496     $l[$started][0] = "PUSHED" if defined $started;
497   }
498   return @l;
499 }
500 ###############################################################################
501 sub _operationPrintStack ($$$$$) {
502   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
503
504   my(@stack) = $self->getDatabase()->stackList($buildData->{userHandle});
505
506   return ([ "STACK LIST", { completed => \@stack}]);
507 }
508 ###############################################################################
509 sub _operationPop ($$$$$) {
510   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
511
512   #FIXME!!!
513   return $self->_operationStart($timeStamp,
514                                 $dateForEntry, $buildData, $timeInfo);
515   my @answers;
516   my $poppedItem = $self->getDatabase()->stackPop($buildData->{userHandle});
517
518   my($status, $category) = ("", undef);
519   if (not defined $poppedItem) {
520     push(@answers, [ "STACK EMPTY", undef ]);
521   } elsif ($poppedItem->needs('category')) {
522     push(@answers, [ "POPPED WITHOUT CATEGORY", { completed => [$poppedItem] }]);
523   } else {
524     push(@answers, [ "POPPED WITH CATEGORY", { completed => [$poppedItem] }]);
525 #    $timeInfo->{categoryID} = $poppedItem->get('category')->get('id');
526 # FIXME: CATEGORY
527   }
528
529   my %entries = ();     $entries{completed} = [];
530   my $thisEntries;
531   my(@l) =
532       $self->_operationStart($timeStamp, $dateForEntry, $buildData, $timeInfo);
533
534   foreach my $ar (@l) {
535     $ar->[0] = "POPPED" if $ar->[1] eq "STOPPED";
536     push(@answers, $ar);
537   }
538   return @answers;
539 }
540 ###############################################################################
541 sub _operationAdd ($$$$$) {
542   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
543
544   my($amountTime, $startTime, $endTime);
545   $amountTime = ParseDateDelta($timeInfo->{time});
546   $startTime = ParseDate($timeInfo->{startTime})
547     if defined $timeInfo->{startTime};
548   $endTime = (defined $timeInfo->{endTime}) ?
549     ParseDate($timeInfo->{endTime}) : $timeStamp;
550   $endTime = $timeStamp unless defined $endTime and $endTime !~ /^\s*$/;
551
552   print "Got $amountTime in Add\n";
553   return (["TIME INVALID", undef])
554     unless (defined $amountTime and $amountTime !~ /^\s*$/);
555
556   my $entry = SFLC::TimeTracker::Entry::Create(
557                          userHandle => $buildData->{userHandle},
558                          dateOccurred => $dateForEntry,
559                          amountTime => $amountTime,
560                          endTime => $endTime,
561                          startTime => $startTime);
562
563   my $status;
564 #  print "KEYS:", join(", ", keys %{$entry->{$key}{needToComplete}});
565
566   # FIXME: CATEGORY_HARDCODE, I think...
567 #  $status = $entry->isComplete() ? "INCOMPLETE ENTRY" : "ADDED";
568   $status = "ADDED";
569   return([$status, { new =>  [$entry]  }] );
570 }
571 ###############################################################################
572 sub _operationAdjust ($$$$$) {
573   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
574   my $pending = $buildData->{pendingUserEntries};
575   my(@startedList);
576   foreach my $pp (keys %{$pending}) {
577     push(@startedList, $pending->{$pp})
578       if ($pending->{$pp}->needs("endTime") and defined $pending->{$pp}->{startTime});
579   }
580
581   my $startedEntry;
582   if (@startedList == 0) {
583     my(@l) =
584       $self->_operationAdd($timeStamp, $dateForEntry, $buildData, $timeInfo);
585     my($status, $entries) = @{$l[0]};
586     $status = "ADDED, NO ENTRY TO ADJUST" if ($status eq "ADDED");
587     return ([$status, $entries ]);
588   } elsif (@startedList > 1) {
589     warn "Multiple entries have been started!";
590     print @startedList;
591     return (["INTERNAL ERROR - MULTIPLE STARTS", undef]);
592   } else {
593     $startedEntry = $startedList[0];
594   }
595   my $adjustTime = ParseDateDelta($timeInfo->{time});
596
597   return (["TIME INVALID", undef])
598     unless (defined $adjustTime and $adjustTime !~ /^\s*$/);
599
600   my %entries;
601   $entries{abandoned} = [];
602   my $remainingAdjustment = $adjustTime;
603   my $interruptedEntry;
604
605   my $newStartTime = DateCalc($startedEntry->{startTime}, $remainingAdjustment);
606
607   if (not defined $newStartTime or $newStartTime =~ /^\s*$/) {
608     carp "DateCalc of $timeStamp to $adjustTime failed and yieleded $newStartTime";
609     return (["INTERNAL ERROR - TIME PARSE", undef]);
610   }
611   if ($newStartTime ge $timeStamp) {
612     $startedEntry->remove();
613     push(@{$entries{abandoned}}, $startedEntry);
614   } else {
615     $startedEntry->set('startTime', $newStartTime);
616     $interruptedEntry = $startedEntry;
617   }
618
619   $entries{new} = [];
620   $entries{new}[0] = SFLC::TimeTracker::Entry::Create(
621                          userHandle => $buildData->{userHandle},
622                          dateOccurred => $dateForEntry,
623                          amountTime => $adjustTime,
624                          endTime => $timeStamp);
625
626   $entries{modified}[0] = $interruptedEntry   if (defined $interruptedEntry);
627
628 #  $pending->{$pp}->set('endTime', $newEnd);
629   return(["ADJUSTED", \%entries]);
630  
631 }
632 ###############################################################################
633 sub _processOperation ($$$$$) {
634   my ($self, $timeStamp, $dateForEntry, $buildData, $timeInfo) = @_;
635
636   # First, remove leading and trailing punctuation from the remaining string
637
638   $timeInfo->{rest} =~ s/^\s*[:;\-\>\<\[\]]\///;
639   $timeInfo->{rest} =~ s/\s*[:;\-\>\<\[\]]\/$//;
640
641   # Now, let's see if we have a new date string
642 #  unless (defined $dateForEntry and $dateForEntry !~ /^\s*$/) {
643 #    my $delta;
644 #    ($delta, $timeInfo->{rest}) =
645 #      _RemoveDateFromStringStart($timeInfo->{rest}, \&ParseDateDelta);
646 #    $dateForEntry = DateCalc($timeStamp, $delta)
647 #      if (defined $delta and $delta !~ /^\s*$/);
648
649     # Ok, it wasn't a delta, look for a date straight-up
650     unless (defined $dateForEntry and $dateForEntry !~ /^\s*$/) {
651       ($dateForEntry, $timeInfo->{rest}) =
652         _RemoveDateFromStringStart($timeInfo->{rest}, \&ParseDate);
653     }
654   # If we couldn't find a date string from the front, we just assume it is now.
655   $dateForEntry = $timeStamp
656     unless defined $dateForEntry and $dateForEntry !~ /^\s*$/;
657     # Re-remove punctuation
658     $timeInfo->{rest} =~ s/^\s*[:;\-\>\<\[\]]\///;
659     $timeInfo->{rest} =~ s/\s*[:;\-\>\<\[\]]\/$//;
660 #  }
661
662   if ($timeInfo->{operation} eq "start") {
663     $self->getDatabase()->stackClear($buildData->{userHandle});
664     return $self->_operationStart($timeStamp, $dateForEntry, $buildData,
665                                   $timeInfo);
666   } elsif ($timeInfo->{operation} eq "stop") {
667     $self->getDatabase()->stackClear($buildData->{userHandle});
668     return $self->_operationStop($timeStamp, $dateForEntry, $buildData,
669                                  $timeInfo);
670   } elsif ($timeInfo->{operation} eq "add") {
671     return $self->_operationAdd($timeStamp, $dateForEntry, $buildData, $timeInfo);
672   } elsif ($timeInfo->{operation} eq "adjust") {
673     return $self->_operationAdjust($timeStamp, $dateForEntry, $buildData,
674                                    $timeInfo);
675   } elsif ($timeInfo->{operation} eq "push") {
676     return $self->_operationPush($timeStamp, $dateForEntry, $buildData,
677                                  $timeInfo);
678   } elsif ($timeInfo->{operation} eq "pop") {
679     return $self->_operationPop($timeStamp, $dateForEntry, $buildData, $timeInfo);
680   } else {
681     croak "Unknown operation $timeInfo->{operation} on $timeStamp";
682   }
683 }
684
685 ###############################################################################
686 sub _PreprocessInputLine ($) {
687   my($inputLine) = @_;
688   my $dateForEntry;
689   # Preprocessing of line before finding seperate sections
690
691   chomp $inputLine;
692
693   # Remove addressing
694   $inputLine =~ s/^\s*time?\s*:?//;
695
696
697     # Take care of "half an hour" and such things
698   $inputLine =~ s/(^|\s+)quarter(?:\s+of)?(?:\s+an?)?($|\s+)/${1}0.25$2/gi;
699   $inputLine =~ s/(^|\s+)half(?:\s+of)?(?:\s+an?)?($|\s+)/${1}0.5$2/gi;
700   $inputLine =~ s/(^|\s+)(\d+)\s{1,3}1\/2(?:\s+of)?(?:\s+an?)?($|\s+)/$1$2.5$3/g;
701   $inputLine =~ s/(^|\s+)1\/2(?:\s+of)?(?:\s+an?)?($|\s+)/${1}0.5$2/g;
702   $inputLine =~ s/(^|\s+)quarter(?:\s+of)?(?:\s+an?)?($|\s+)/${1}0.25$2/gi;
703
704   $inputLine =~ s/(^|\s+)one($|\s+)/${1}1$2/gi;
705   $inputLine =~ s/(^|\s+)two($|\s+)/${1}2$2/gi;
706   $inputLine =~ s/(^|\s+)three($|\s+)/${1}3$2/gi;
707   $inputLine =~ s/(^|\s+)four($|\s+)/${1}4$2/gi;
708   $inputLine =~ s/(^|\s+)five($|\s+)/${1}5$2/gi;
709   $inputLine =~ s/(^|\s+)six($|\s+)/${1}6$2/gi;
710   $inputLine =~ s/(^|\s+)seven($|\s+)/${1}7$2/gi;
711   $inputLine =~ s/(^|\s+)eight($|\s+)/${1}8$2/gi;
712   $inputLine =~ s/(^|\s+)nine($|\s+)/${1}9$2/gi;
713   $inputLine =~ s/(^|\s+)ten($|\s+)/${1}10$2/gi;
714
715   $inputLine =~ s/([^\d]|^)(^|\s+)hour($|\s+)([^\d]|$)/$1${2}1 hour$3$4/gi;
716
717   # Find dates that have been seperated with dashes rater than slashes
718
719   $inputLine =~ s%(^|[\s,;]+)(\d{1,2})\-(\d{1,2})-(\d{2,4})
720                   ($|[\s,;]+)%$1$2/$3/$4$5%x;
721   $inputLine =~ s%(^|[\s,;]+)(\d{1,2})\-(\d{1,2})($|[\s,;]+)%$1$2/$3$4%;
722
723   $inputLine =~ s/last\s+(?:night|day)/yesterday/g;
724   $inputLine =~ s/yeste?r?da?y/yesterday/g;
725
726   # Handle "Hours (for|on) DATE: REAL_INFO
727
728 #      or $inputLine =~ /^\s*([^:;\-]+)[:;\-]\s*([^:;\-\s]+)/
729
730   if ($inputLine =~
731       s/^\s*(?:hours?)?\s*(?:on|for|during|at)\s+([^:;\-]+)\s*[:;\-]/$1/i) {
732     ($dateForEntry, $inputLine) = _RemoveDateFromStringStart($inputLine,
733                                                             \&ParseDate);
734   }
735   if ($inputLine =~ /^\s*(\S+)\s*,/) {
736     my $possibleDate = $1;
737     $dateForEntry = ParseDate($possibleDate);
738     $inputLine =~ s/^\s*(\S+)\s*,//
739       if (defined $dateForEntry and $dateForEntry !~ /^\s*$/);
740   }
741
742   return ($dateForEntry, $inputLine);
743 }
744 ###############################################################################
745 sub _RemoveDateFromStringStart ($$) {
746   my($line, $dateParseFunction) = @_;
747   my($newLine, $foundDate);
748
749   $line =~ s/^\s*([^:;\-]+?)[:;\-]([^0-9])/$1 $2/;
750   my(@words) = split /\s+/, $line;
751   my(@limitForSpeed, @rest);
752   if (@words <= $PARSE_WORD_LIMIT) {
753     @limitForSpeed = @words;
754     @rest = ();
755   } else {
756     @limitForSpeed = @words[0 .. ($PARSE_WORD_LIMIT -1)];
757     @rest = @words[$PARSE_WORD_LIMIT .. $#words];
758   }
759   $foundDate = $dateParseFunction->(\@limitForSpeed);
760   $newLine = join(" ", @limitForSpeed, @rest);
761   return ($foundDate, $newLine);
762 }
763 ###############################################################################
764 sub _buildTimeData ($$$$) {
765   my($self, $timeStamp, $userHandle, $originalLine) = @_;
766
767   $self->{currentOriginalLine} = $originalLine;
768
769   my($dateForEntry, $inputLine) = _PreprocessInputLine($originalLine);
770
771   my $data = _FindTimesInLine($dateForEntry, $inputLine);
772
773   use Data::Dumper;
774   print "First FindTimes in line: ", Data::Dumper->Dump([$data]);
775   # If we don't find time data, see if we actually have a string that starts
776   #  with a date.  If it does, then we use that date as the entry date, and
777   #  try to process the rest of the line as the actual operation.
778
779   if (not defined $data->{timeInfo} or keys(%{$data->{timeInfo}}) == 0) {
780     ($dateForEntry, $inputLine) = _RemoveDateFromStringStart($data->{rest},
781                                                             \&ParseDate);
782     $data = _FindTimesInLine($dateForEntry, $inputLine);
783     print "Second FindTimes in line: ", Data::Dumper->Dump([$data]);
784   }
785   $data->{dateForEntry} = $dateForEntry;
786   return $data;
787 }
788 ###############################################################################
789 sub dryRun {
790   my($self, $timeStamp, $user, $originalLine) = @_;
791
792   my $userHandle = $self->getDatabase()->getUserHandle($user);
793
794   my $data = $self->_buildTimeData($timeStamp, $userHandle, $originalLine);
795
796   $data->{rest} =~ s/^\s*[:;\-\>\<\[\]]\///;
797   $data->{rest} =~ s/\s*[:;\-\>\<\[\]]\/$//;
798
799   unless (defined $data->{dateForEntry} and $data->{dateForEntry} !~ /^\s*$/) {
800     ($data->{dateForEntry}, $data->{rest}) =
801       _RemoveDateFromStringStart($data->{rest}, \&ParseDate);
802   }
803   return $data;
804 }
805 ###############################################################################
806 sub checkIfLineIsTime ($$$$) {
807   my($self, $timeStamp, $user, $originalLine) = @_;
808
809   my $userHandle = $self->getDatabase()->getUserHandle($user);
810
811   my $data = $self->_buildTimeData($timeStamp, $userHandle, $originalLine);
812
813   return (keys %{$data->{timeInfo}} > 0);
814
815 }
816 ###############################################################################
817 # timeStamp is assumed to be truly unique.  If it isn't, prefix it with
818 # something before you call this method!
819
820 sub parseLine ($$$$$) {
821   my($self, $timeStamp, $user, $originalLine, $forceParsing) = @_;
822
823   $forceParsing = 0 unless defined $forceParsing;
824
825   # If the user does not exist, bail out
826   my $userHandle = $self->getDatabase()->getUserHandle($user);
827
828   return new Tree::Simple(
829           new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
830                   object => new SFLC::TimeTracker::Output::Statement(
831                   string => "", status => "EMPTY LINE", type => "")))
832     unless defined $originalLine and $originalLine !~ /^\s*$/;
833
834   return ($forceParsing ? ( new Tree::Simple(
835           new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
836                   object => new SFLC::TimeTracker::Output::Statement(
837                       string => "I am sorry, but I just don't know you well " .
838                                     "enough to store time for you.",
839                                   status => "UNKNOWN USER", type => ""))))
840           : undef)
841       if (not defined $userHandle);
842
843
844   # check to see if this is a line we've been told to ignore.
845
846   return ($forceParsing ? (new Tree::Simple(
847           new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
848                   object => new SFLC::TimeTracker::Output::Statement(
849                   string => "", status => "IGNORED", type => ""))))
850           : undef)
851     if $self->getDatabase()->checkIgnoreLines($userHandle, $originalLine);
852
853
854   my $data = $self->_buildTimeData($timeStamp, $userHandle, $originalLine);
855
856   # If the user wanted us to, we should now process each time data entry
857   # we were able to find, and build the return objects.
858
859   my($retTree, $leafEdge);
860   $retTree = $leafEdge = new Tree::Simple(
861           new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
862                        object => new SFLC::TimeTracker::Output::Dummy(
863                                   string => "", status => "DUMMY", type => ""),
864                        edge => 'proceed'));
865
866   foreach my $key (keys %{$data->{timeInfo}}) {
867     my $timeInfo = $data->{timeInfo}{$key};
868     if (not defined $timeInfo->{time}) {
869       $timeInfo->{time} = "";
870     } elsif ($timeInfo->{rest} =~ /^\s*$/
871              and $data->{rest} !~ /^\s*$/) {
872       $timeInfo->{rest} = $data->{rest};
873       $data->{rest} = "";
874     }
875     # FIXME: CONFIG_OPTIONS
876     my $str =  ($user =~ /^\s*(?:user2)/) ? "minutes" : "hours";
877     $timeInfo->{time} =~
878       s/(^|\s+)ASSUMED_UNIT_UNQUALIFIED(\s+|$)/$1$str$2/g;
879
880     my $prevData = {};
881     $prevData->{pendingUserEntries} =
882       $self->getDatabase()->getPendingEntriesByUserHandle($userHandle);
883     $prevData->{userHandle} = $userHandle;
884 #    $prevData->{categoryParser} = "OldCategoryQuestion";
885 #    $prevData->{categoryParser} = "BasicLawyer";
886 #    $prevData->{categoryParser} = "AdminAssume";
887     $prevData->{categoryParser} =
888       $self->getDatabase()->getUserConfigValue($userHandle, 'categoryParser');
889
890     my(@operationParsed) =
891       $self->_processOperation($timeStamp, $data->{dateForEntry},
892                                $prevData, $timeInfo);
893
894     my($resolutionTree, $entryResolver, $note);
895     if ($timeInfo->{operation} !~ /^pop|stop/) {
896       ($resolutionTree, $entryResolver, $note) =
897         $self->_determineCategory($userHandle, $timeInfo->{rest},
898                                   $prevData->{categoryParser}, $timeStamp);
899     }
900     foreach my $item (@operationParsed) {
901       my($status, $entries) = @{$item};
902       if ($status =~
903             /^(TIME\s+INVALID|NOTHING\s+STARTED|INTERNAL\s+ERROR|DUMMY)/) {
904         my $shortStat = $1;
905         $shortStat =~ s/\s+/ /g;
906         my $shortendLine = (length($originalLine) <= $SHORTEN_LINE_FOR_REOUTPUT)
907           ? $originalLine
908           : ( (substr($originalLine, 0, $SHORTEN_LINE_FOR_REOUTPUT)) . " ...");
909
910         my $invalid = new Tree::Simple(
911                    new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
912                            object => new SFLC::TimeTracker::Output::Statement(
913                                   string =>
914                                     sprintf($ERROR_MESSAGES{$status},
915                                             $originalLine, $status),
916                                   status => $status, type => ""),
917                            edge => 'proceed'));
918         $leafEdge->addChild($invalid);
919       } else {
920         foreach my $key (sort keys %{$entries}) {
921           foreach my $entry (@{$entries->{$key}}) {
922
923             my $object = new SFLC::TimeTracker::Output::Entry(
924                                   entry => $entry,
925                                   status => $status, type => $key);
926             if ($entry->needs('category')) {
927               confess("Woah! $entry->{id} needs a category, but there is no" .
928                     " entryResolver!") unless defined $entryResolver;
929               $entryResolver->addEntry($object);
930               $entry->set('note', $note) if defined $note;
931             } else {
932               $leafEdge->addChild(new Tree::Simple(new
933                    SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
934                            object => $object, edge => 'proceed')));
935             }
936           }
937         }
938       }
939     }
940     $leafEdge->addChild($resolutionTree) if (defined $resolutionTree);
941   }
942   if ($data->{rest} !~ /^\s*$/) {
943     if ( (not $retTree->isLeaf()) or $forceParsing) {
944       my(@sibs) = $leafEdge->getAllChildren();
945       my $string = (@sibs > 0) ?
946         "(Also, heard you say \"$data->{rest}\", but found no time entry.)" :
947         "Heard you say \"$data->{rest}\", but found no time entry.";
948
949       $leafEdge->addChild(new Tree::Simple(
950           new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode(
951                            object => new SFLC::TimeTracker::Output::Statement(
952                                   string => $string,
953                                   status => "UNPARSEABLE", type => ""),
954                            edge => 'proceed')));
955     }
956   }
957
958   return ( (not $retTree->isLeaf()) or $forceParsing) ? $retTree : undef;
959 }
960 ###############################################################################
961 # _determineCategory should return a list of two items.  The first should be
962 #  the root node of the resolution tree, and the second should be the leaf node
963 #   with the edge of "category" to that tree.
964
965 sub _determineCategory ($$$$) {
966   my($self, $userHandle, $categoryLine, $categoryParserName, $timeStamp) = @_;
967
968   my $parser = new SFLC::TimeTracker::Input::CategoryParser($categoryParserName,
969                                                   userHandle => $userHandle);
970   my($a,$b) = $parser->parse("File new entry", $categoryLine, $timeStamp);
971   my $note = $parser->get('note');
972   return($a, $b, $note);
973
974 #    $self->getDatabase()->addCategory($categoryCandidate);
975 #    $timeInfo->{categoryID} = $categoryCandidate;
976   }
977
978 ###############################################################################
979 1;
980 __END__
981
982 #
983 # Local variables:
984 # compile-command: "perl -I ../../../Modules -c Input.pm"
985 # End:
Note: See TracBrowser for help on using the browser.

SFLC Main Page

[frdm] Support SFLC