1package main
2
3import (
4 "github.com/nmeum/tracktime/parser"
5
6 "flag"
7 "fmt"
8 "log"
9 "os"
10 "time"
11)
12
13type TrackedDate struct {
14 Duration time.Duration
15 BonusWork bool
16}
17
18const (
19 DAY = 'd'
20 WEEK = 'w'
21 MONTH = 'm'
22)
23
24var (
25 goal = flag.Int("h", 8, "hours per interval")
26 interval = flag.String("i", "d", "interval for working hours")
27 seconds = flag.Bool("s", false, "output duration in seconds")
28)
29
30var dateLayout string
31
32func max(a, b int) int {
33 if a > b {
34 return a
35 } else {
36 return b
37 }
38}
39
40func durationString(duration time.Duration) string {
41 if *seconds {
42 return fmt.Sprintf("%v", duration.Seconds())
43 } else {
44 return duration.String()
45 }
46}
47
48func intervalString(date time.Time) string {
49 if *interval == "" {
50 fmt.Fprintf(os.Stderr, "invalid interval\n")
51 os.Exit(1)
52 }
53
54 switch (*interval)[0] {
55 case DAY:
56 return date.Format(dateLayout)
57 case WEEK:
58 year, week := date.ISOWeek()
59 return fmt.Sprintf("W%v %v", week, year)
60 case MONTH:
61 year := date.Year()
62 return fmt.Sprintf("%s %v", date.Month(), year)
63 default:
64 fmt.Fprintf(os.Stderr, "unsupported interval: %q\n", *interval)
65 os.Exit(2)
66 }
67
68 panic("unreachable")
69}
70
71func handleEntries(entries []*parser.Entry) {
72 var keys []string
73 var maxdurlen int
74
75 workmap := make(map[string]TrackedDate)
76 for _, entry := range entries {
77 key := intervalString(entry.Date)
78
79 e, ok := workmap[key]
80 if !ok {
81 keys = append(keys, key)
82 } else if e.BonusWork && !entry.BonusWork {
83 fmt.Fprintf(os.Stderr, "WARNING: Only some entries for %v are bonus hours\n", entry.Date.String())
84 }
85 workmap[key] = TrackedDate{e.Duration + entry.Duration, entry.BonusWork}
86
87 // Date should always have the same width. Only duration
88 // requires padding based on the maximum duration length.
89 maxdurlen = max(maxdurlen, len(fmt.Sprintf("%v", e.Duration)))
90 }
91
92 var delta, goalHours time.Duration
93 goalHours = time.Duration(*goal) * time.Hour
94
95 // Output in same order as specified in input file
96 for _, key := range keys {
97 e := workmap[key]
98 hours := e.Duration
99
100 if e.BonusWork {
101 delta += hours
102 } else {
103 delta += (hours - goalHours)
104 }
105
106 // Output should always be aligned at the pipe character.
107 fmt.Printf("%v %*v | %v\n", key, maxdurlen, hours, durationString(delta))
108 }
109}
110
111func main() {
112 log.SetFlags(log.Lshortfile)
113 flag.Parse()
114
115 if flag.NArg() != 1 {
116 fmt.Fprintf(os.Stderr, "specify a file to parse\n")
117 os.Exit(1)
118 }
119
120 fp := flag.Arg(0)
121 file, err := os.Open(fp)
122 if err != nil {
123 log.Fatal(err)
124 }
125 defer file.Close()
126
127 dateLayout = parser.DefaultTimeFormat()
128 p := parser.NewParser(dateLayout)
129
130 entries, err := p.ParseEntries(fp, file)
131 if err != nil {
132 log.Fatal(err)
133 }
134
135 handleEntries(entries)
136}