summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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
commit866c44aa7ac7a82c70084179a036fb7b35c75f75 (patch)
tree49a5438595f5231705850fa5ad5787809b408a34
parentd564211dbbef53f8e41067bfafff9948c83b975c (diff)
progress
-rw-r--r--zeitgeist.go181
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
}