tracktime

Small utility for tracking working hours in a plain text file

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

  1package parser
  2
  3import (
  4	"bufio"
  5	"errors"
  6	"io"
  7	"os"
  8	"regexp"
  9	"strconv"
 10	"time"
 11)
 12
 13const lineFormat = "^\\+?(..*)	([0-9][0-9][0-9][0-9])	([0-9][0-9][0-9][0-9])	(..*)$"
 14
 15const (
 16	layoutEnv     = "TRACKTIME_FORMAT"
 17	defaultLayout = "02.01.2006"
 18)
 19
 20type Entry struct {
 21	Date        time.Time
 22	Duration    time.Duration
 23	Description string
 24	BonusWork   bool
 25}
 26
 27type Parser struct {
 28	validLine *regexp.Regexp
 29	layout    string
 30	lineNum   uint
 31}
 32
 33// https://en.wikipedia.org/wiki/24-hour_clock#Military_time
 34func (p *Parser) militaryTime(time int) (int, error) {
 35	hours := time / 100
 36	minutes := time % 100
 37
 38	if hours > 24 {
 39		return 0, errors.New("invalid hour in duration")
 40	} else if minutes >= 60 {
 41		return 0, errors.New("invalid minute in duration")
 42	}
 43
 44	return (hours * 60) + minutes, nil
 45}
 46
 47func (p *Parser) getFields(line string) (bool, string, int, int, string, error) {
 48	matches := p.validLine.FindStringSubmatch(line)
 49	if matches == nil {
 50		return false, "", 0, 0, "", errors.New("line does not match format")
 51	}
 52
 53	durStart, err := strconv.Atoi(matches[2])
 54	if err != nil {
 55		return false, "", 0, 0, "", err
 56	}
 57	durEnd, err := strconv.Atoi(matches[3])
 58	if err != nil {
 59		return false, "", 0, 0, "", err
 60	}
 61
 62	// If the line starts with a “+” character, then count this as
 63	// bonus working time (e.g. working on the weekend).
 64	bonusEntry := matches[0][0] == '+'
 65
 66	return bonusEntry, matches[1], durStart, durEnd, matches[4], nil
 67}
 68
 69func (p *Parser) parseEntry(line string) (*Entry, error) {
 70	bonus, date, durStart, durEnd, desc, err := p.getFields(line)
 71	if err != nil {
 72		return nil, err
 73	}
 74
 75	etime, err := time.Parse(p.layout, date)
 76	if err != nil {
 77		return nil, err
 78	}
 79
 80	if durStart >= durEnd {
 81		return nil, errors.New("invalid duration")
 82	}
 83	start, err := p.militaryTime(durStart)
 84	if err != nil {
 85		return nil, err
 86	}
 87	end, err := p.militaryTime(durEnd)
 88	if err != nil {
 89		return nil, err
 90	}
 91
 92	// Add start duration to entry date, allows reconstructing the
 93	// absolute start time (and end time) of the given entry.
 94	etime = etime.Add(time.Duration(start) * time.Minute)
 95
 96	duration := time.Duration(end-start) * time.Minute
 97	return &Entry{etime, duration, desc, bonus}, nil
 98}
 99
100func (p *Parser) ParseEntries(fn string, r io.Reader) ([]*Entry, error) {
101	var entries []*Entry
102
103	// Reset line number information
104	p.lineNum = 0
105
106	scanner := bufio.NewScanner(r)
107	for scanner.Scan() {
108		p.lineNum++
109		line := scanner.Text()
110
111		entry, err := p.parseEntry(line)
112		if err != nil {
113			return entries, ParserError{fn, p.lineNum, err.Error()}
114		}
115
116		entries = append(entries, entry)
117	}
118
119	err := scanner.Err()
120	if err != nil {
121		return entries, err
122	}
123
124	return entries, nil
125}
126
127func NewParser(layout string) *Parser {
128	validLine := regexp.MustCompile(lineFormat)
129	return &Parser{validLine, layout, 0}
130}
131
132func DefaultTimeFormat() string {
133	dateLayout := os.Getenv(layoutEnv)
134	if dateLayout == "" {
135		dateLayout = defaultLayout
136	}
137
138	return dateLayout
139}