diff options
author | Özgür Kesim <oec@kesim.org> | 2024-11-26 19:40:11 +0100 |
---|---|---|
committer | Özgür Kesim <oec@kesim.org> | 2024-11-26 19:40:11 +0100 |
commit | 866c44aa7ac7a82c70084179a036fb7b35c75f75 (patch) | |
tree | 49a5438595f5231705850fa5ad5787809b408a34 /zeitgeist.go | |
parent | d564211dbbef53f8e41067bfafff9948c83b975c (diff) |
progress
Diffstat (limited to 'zeitgeist.go')
-rw-r--r-- | zeitgeist.go | 181 |
1 files changed, 141 insertions, 40 deletions
diff --git a/zeitgeist.go b/zeitgeist.go index 7cdc968..c161fa0 100644 --- a/zeitgeist.go +++ b/zeitgeist.go @@ -104,27 +104,45 @@ func (d Date) Year() int { return (time.Time(d)).Year() } +func (a Date) After(b Date) bool { + return (time.Time(a)).After(time.Time(b)) +} + type Amount float64 const ( - Days = 215 - DaysInMonth = float64(215) / 12 + Days Amount = Amount(215) + DaysInMonth = Amount(Days) / 12 + HoursInDay = Amount(8) ) func (u Amount) String() string { - return fmt.Sprintf("%5.2f", u) + d := u / HoursInDay + m := d / DaysInMonth + h := u - Amount(int(d))*HoursInDay + r := "" + if int(m) > 0 { + r = fmt.Sprintf("%dm", int(m)) + } + if int(d) > 0 { + r += fmt.Sprintf("%dd", int(d)) + } + if h > 0 { + r += fmt.Sprintf("%dh", int(h)) + } + return r } func (b Amount) AsHour() string { - return b.String() + "h" + return fmt.Sprintf("%5.2fh", b) } func (b Amount) AsDay() string { - return (b / 8).String() + "d" + return fmt.Sprintf("%5.2fd", b/HoursInDay) } func (b Amount) AsMonth() string { - return (b / 8 / Amount(DaysInMonth)).String() + "m" + return fmt.Sprintf("%5.2fm", b/HoursInDay/Amount(DaysInMonth)) } func (u *Amount) UnmarshalText(b []byte) error { @@ -136,15 +154,15 @@ func (u *Amount) parseString(s string) error { if e != nil { return fmt.Errorf("not a float: %v", s) } + *u = Amount(f) switch s[len(s)-1] { case 'm', 'M': - f *= DaysInMonth + *u *= DaysInMonth fallthrough case 'd', 'D': - f *= 8 + *u *= HoursInDay fallthrough case 'h', 'H': - *u = Amount(f) default: return fmt.Errorf("unknown unit in: %v", s) } @@ -158,10 +176,11 @@ type Task struct { } type Entry struct { - User string - WP string - Task string - Amount Amount + User string + WP string + Task string + Amount Amount + Comment string } type Deliverable struct { @@ -177,9 +196,16 @@ func main() { if e != nil { log.Fatalf("error loading project: %v", e) } + if p.Start.After(p.End) { + log.Fatalf("start '%s' after end '%s'", p.Start, p.End) + } + if p.Timeline == nil { p.Timeline = make(Timeline) } + if p.Planned == nil { + p.Planned = make(Timeline) + } e = p.readPlans(*fl_plandir) if e != nil { @@ -215,7 +241,7 @@ func (p *Project) readPlans(dir string) error { if len(dir) == 0 { dir = p.Planning } - glob := dir + "/*.plan" + glob := filepath.Join(dir, "*.plan") matches, e := filepath.Glob(glob) if e != nil { return fmt.Errorf("filepath.Glob(%q): %w", glob, e) @@ -235,6 +261,7 @@ func (p *Project) readPlans(dir string) error { scanner := bufio.NewScanner(f) nr := 1 last := time.Time{} + for scanner.Scan() { // # Ignore line // # Tab-separated fields @@ -247,24 +274,57 @@ func (p *Project) readPlans(dir string) error { continue } - parts := min2w.Split(scanner.Text(), 3) + parts := min2w.Split(scanner.Text(), 4) 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) - } - if last.After(d) { - return fmt.Errorf("file %q, line %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) + var isrange bool + var r [2]time.Time + var e error + + r[0], e = time.Parse(time.DateOnly, parts[0]) + if e == nil { + r[1] = r[0].AddDate(0, 0, 1) + } else { + r[0], e = time.Parse("2006-01", parts[0]) + if e == nil { + slog.Info("Month found", "date", parts[0]) + r[1] = r[0].AddDate(0, 1, 0) + isrange = 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) + } + 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]) + } + } + 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]) + } + slog.Info("Found range", "start", r[0], "end", r[1]) + isrange = true + } } - 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) + + if !isrange { + if last.After(r[0]) { + return fmt.Errorf("file %q, line %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]) + } + 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) + } + } else { + + // TODO: check range soundness } wps := strings.Split(parts[1], ":") @@ -278,24 +338,50 @@ func (p *Project) readPlans(dir string) error { } var amount Amount + var days Amount + 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]) } - entry := Entry{ - User: parts[1], - WP: wps[0], - Task: wps[1], - Amount: amount} - - entries := p.Timeline[Date(d)] - entries = append(entries, entry) - p.Timeline[Date(d)] = entries + 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 + } + days += HoursInDay + + a := amount + if amount-HoursInDay > 0 { + a = HoursInDay + amount -= a + } else { + a = amount + amount = 0 + + } + + if a > 0 { + entry := Entry{ + User: parts[1], + WP: wps[0], + Task: wps[1], + Amount: a, + Comment: parts[3]} + + entries := p.Timeline[Date(d)] + entries = append(entries, entry) + p.Planned[Date(d)] = entries + } + } + if amount != 0 { + return fmt.Errorf("file %q, line %d: too many hours allocated (%s), maximum %s possible", name, nr, parts[2], days) + } nr += 1 } - } return nil } @@ -304,7 +390,7 @@ func (p *Project) readTimelogs(dir string) error { if len(dir) == 0 { dir = p.Timelogs } - glob := dir + "/*.timelog" + glob := filepath.Join(dir, "*.timelog") matches, e := filepath.Glob(glob) if e != nil { return fmt.Errorf("filepath.Glob(%q): %w", glob, e) @@ -323,7 +409,7 @@ func (p *Project) readTimelogs(dir string) error { if e != nil { return fmt.Errorf("fmt.Sscanf(%q): %w", s, e) } else if n != 2 { - return fmt.Errorf("file %q not in the format <year>.<user>.timelog") + return fmt.Errorf("file %q not in the format <year>.<user>.timelog", name) } } @@ -435,6 +521,8 @@ func (p *Project) printReport(name string) { type FullDate struct { Date + Planned Entries + Worked Entries IsWorkday bool IsHoliday bool } @@ -448,10 +536,23 @@ func (p *Project) IsHoliday(d Date) bool { return ok } +func (f *FullDate) Difference() Amount { + return f.Planned.Total() - f.Worked.Total() +} + func (p *Project) Days() []FullDate { dates := []FullDate{} for d := time.Time(p.Start); d.Before(time.Time(p.End)); d = d.AddDate(0, 0, 1) { - dates = append(dates, FullDate{Date(d), Date(d).IsWorkday(), p.IsHoliday(Date(d))}) + d := Date(d) + + dates = append(dates, + FullDate{ + Date: d, + Planned: p.Planned[d], + Worked: p.Timeline[d], + IsWorkday: d.IsWorkday(), + IsHoliday: p.IsHoliday(d), + }) } return dates } |