diff options
Diffstat (limited to 'zeitgeist.go')
-rw-r--r-- | zeitgeist.go | 84 |
1 files changed, 50 insertions, 34 deletions
diff --git a/zeitgeist.go b/zeitgeist.go index 869d062..a0e30d3 100644 --- a/zeitgeist.go +++ b/zeitgeist.go @@ -183,10 +183,13 @@ const ( ) func (u Amount) String() string { + if u < 1 { + return "0" + } + r := "" d := int(u / HoursInDay) m := d / DaysInMonth h := int(u) - d*HoursInDay - r := "" if m != 0 { r = fmt.Sprintf("%dM", m) d -= DaysInMonth * m @@ -332,10 +335,11 @@ func (p *Project) readPlans(dir string) error { defer f.Close() scanner := bufio.NewScanner(f) - nr := 1 + nr := 0 last := time.Time{} for scanner.Scan() { + nr += 1 // # Ignore line // # Tab-separated fields // 2024-02 wp2:short 1m [comment] @@ -348,10 +352,8 @@ func (p *Project) readPlans(dir string) error { } parts := min2w.Split(scanner.Text(), 4) - if len(parts) < 3 { - return fmt.Errorf("file %q, line %d: not enough tab-separated fields", name, nr) - } + var ismonth bool var isrange bool var r [2]time.Time var e error @@ -364,54 +366,67 @@ func (p *Project) readPlans(dir string) error { if e == nil { slog.Debug("Month found", "date", parts[0]) r[1] = r[0].AddDate(0, 1, 0) - isrange = true + ismonth = true } else { // try xxxx-yy-zz..xxxx-yy-zz ts := strings.Split(parts[0], "..") if len(ts) != 2 { - return fmt.Errorf("file %q, line %d: couldn't parse date: %w", name, nr, e) + return fmt.Errorf("file %s:%d: couldn't parse date: %w", name, nr, e) } for i := range r { r[i], e = time.Parse(time.DateOnly, ts[i]) if e != nil { - return fmt.Errorf("file %q, line %d: invalid range: %s", name, nr, parts[0]) + return fmt.Errorf("file %s:%d: invalid range: %s", name, nr, parts[0]) } } + if r[0].Equal(r[1]) || r[0].After(r[1]) { - return fmt.Errorf("file %q, line %d: invalid range: %s", name, nr, parts[0]) + return fmt.Errorf("file %s:%d: invalid range: %s", name, nr, parts[0]) } + + // r[1] shall mark the first day _after_ the range + r[1] = r[1].AddDate(0, 0, 1) + slog.Debug("Found range", "start", r[0], "end", r[1]) isrange = true } } if last.After(r[0]) || last.After(r[1]) { - return fmt.Errorf("file %q, line %d: dates not monotonic: %s before %s", name, nr, parts[0], Date(last)) + return fmt.Errorf("file %s:%d: dates not monotonic: %s before %s", name, nr, parts[0], Date(last)) } - if !isrange { + if !isrange && !ismonth { if last.After(r[0]) { - return fmt.Errorf("file %q, line %d: dates not monotonic, %s older", name, nr, Date(r[0])) + return fmt.Errorf("file %s:%d: dates not monotonic, %s older", name, nr, Date(r[0])) } if !Date(r[0]).IsWorkday() { - return fmt.Errorf("file %q, line %d: date is a weekend: %v", name, nr, r[0]) + return fmt.Errorf("file %s:%d: date is a weekend: %v", name, nr, r[0]) } if h, found := p.Holidays[Date(r[0])]; found { - return fmt.Errorf("file %q, line %d: date %v is a holiday: %s", name, nr, Date(r[0]), h) + return fmt.Errorf("file %s:%d: date %v is a holiday: %s", name, nr, Date(r[0]), h) } } else { // TODO: check range soundness } + // Remember the last beginning date. + last = r[0] + + if len(parts) < 3 { + // Acceptible date entry, but no further content? Fine, just ignore and move on. + continue + } + wps := strings.Split(parts[1], ":") if len(wps) < 2 { - return fmt.Errorf("file %q, line %d: no such workpackage:task %q", name, nr, parts[1]) + return fmt.Errorf("file %s:%d: no such workpackage:task %q", name, nr, parts[1]) } if wp, ok := p.Workpackages[wps[0]]; !ok { - return fmt.Errorf("file %q, line %d: no such workpackage %q", name, nr, wps[0]) + return fmt.Errorf("file %s:%d: no such workpackage %q", name, nr, wps[0]) } else if _, ok := wp.Tasks[wps[1]]; !ok { - return fmt.Errorf("file %q, line %d: no such task %q in %q", name, nr, wps[1], wps[0]) + return fmt.Errorf("file %s:%d: no such task %q in %q", name, nr, wps[1], wps[0]) } var amount Amount @@ -420,11 +435,10 @@ func (p *Project) readPlans(dir string) error { e = (&amount).parseString(parts[2]) // TODO: parse "*8h" etc. if e != nil { - return fmt.Errorf("file %q, line %d: not acceptible budget: %q", name, nr, parts[2]) + return fmt.Errorf("file %s:%d: not acceptible budget: %q", name, nr, parts[2]) } for d := r[0]; !d.Equal(r[1]); d = d.AddDate(0, 0, 1) { - last = d if !Date(d).IsWorkday() || p.IsHoliday(Date(d)) { continue } @@ -454,10 +468,10 @@ func (p *Project) readPlans(dir string) error { } } if amount != 0 { - return fmt.Errorf("file %q, line %d: too many hours allocated (%s), maximum %s possible", name, nr, parts[2], days) + slog.Info("oops", "amount", amount) + return fmt.Errorf("file %s:%d: too many hours allocated (%q), maximum %s possible, rest: %5.2f", name, nr, parts[2], days, amount) } - nr += 1 } } return nil @@ -511,47 +525,49 @@ func (p *Project) readTimelogs(dir string) error { // # Tab-separated fields // 2024-12-11 wp2:short 8h [comment] var line = scanner.Text() - if strings.HasPrefix(line, "#") { + if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" { continue } parts := min2w.Split(scanner.Text(), 3) - if len(parts) < 3 { - return fmt.Errorf("file %q, line %d: not enough tab-separated fields", name, nr) - } d, e := time.Parse(time.DateOnly, parts[0]) if e != nil { - return fmt.Errorf("file %q, line %d: couldn't parse date: %w", name, nr, e) + return fmt.Errorf("file %s:%d: couldn't parse date: %w", name, nr, e) } if d.Year() != int(year) { - return fmt.Errorf("file %q, line %d: wrong year: %d", name, nr, d.Year()) + return fmt.Errorf("file %s:%d: wrong year: %d", name, nr, d.Year()) } if last.After(d) { - return fmt.Errorf("file %q, line %d: dates not monotonic, %s older", name, nr, Date(d)) + return fmt.Errorf("file %s:%d: dates not monotonic, %s older", name, nr, Date(d)) } last = d if !Date(d).IsWorkday() { - return fmt.Errorf("file %q, line %d: date is a weekend: %v", name, nr, d) + return fmt.Errorf("file %s:%d: date is a weekend: %v", name, nr, d) } if h, found := p.Holidays[Date(d)]; found { - return fmt.Errorf("file %q, line %d: date %v is a holiday: %s", name, nr, Date(d), h) + return fmt.Errorf("file %s:%d: date %v is a holiday: %s", name, nr, Date(d), h) + } + + if len(parts) < 3 { + // Acceptible date entry, but no further content? Fine, just ignore. + continue } wps := strings.Split(parts[1], ":") if len(wps) < 2 { - return fmt.Errorf("file %q, line %d: no such workpackage:task %q", name, nr, parts[1]) + return fmt.Errorf("file %s:%d: no such workpackage:task %q", name, nr, parts[1]) } if wp, ok := p.Workpackages[wps[0]]; !ok { - return fmt.Errorf("file %q, line %d: no such workpackage %q", name, nr, wps[0]) + return fmt.Errorf("file %s:%d: no such workpackage %q", name, nr, wps[0]) } else if _, ok := wp.Tasks[wps[1]]; !ok { - return fmt.Errorf("file %q, line %d: no such task %q in %q", name, nr, wps[1], wps[0]) + return fmt.Errorf("file %s:%d: no such task %q in %q", name, nr, wps[1], wps[0]) } var amount Amount e = (&amount).parseString(parts[2]) if e != nil { - return fmt.Errorf("file %q, line %d: not acceptible budget: %q", name, nr, parts[2]) + return fmt.Errorf("file %s:%d: not acceptible budget: %q", name, nr, parts[2]) } entry := Entry{ |