summaryrefslogtreecommitdiff
path: root/zeitgeist.go
diff options
context:
space:
mode:
Diffstat (limited to 'zeitgeist.go')
-rw-r--r--zeitgeist.go84
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{