tracktime

Small utility for tracking working hours in a plain text file

git clone https://git.8pit.net/tracktime.git

  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}