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}
 25
 26type Parser struct {
 27	validLine *regexp.Regexp
 28	layout    string
 29	lineNum   uint
 30}
 31
 32// https://en.wikipedia.org/wiki/24-hour_clock#Military_time
 33func (p *Parser) militaryTime(time int) (int, error) {
 34	hours := time / 100
 35	minutes := time % 100
 36
 37	if hours > 24 {
 38		return 0, errors.New("invalid hour in duration")
 39	} else if minutes >= 60 {
 40		return 0, errors.New("invalid minute in duration")
 41	}
 42
 43	return (hours * 60) + minutes, nil
 44}
 45
 46func (p *Parser) getFields(line string) (string, int, int, string, error) {
 47	matches := p.validLine.FindStringSubmatch(line)
 48	if matches == nil {
 49		return "", 0, 0, "", errors.New("line does not match format")
 50	}
 51
 52	durStart, err := strconv.Atoi(matches[2])
 53	if err != nil {
 54		return "", 0, 0, "", err
 55	}
 56	durEnd, err := strconv.Atoi(matches[3])
 57	if err != nil {
 58		return "", 0, 0, "", err
 59	}
 60
 61	return matches[1], durStart, durEnd, matches[4], nil
 62}
 63
 64func (p *Parser) parseEntry(line string) (*Entry, error) {
 65	date, durStart, durEnd, desc, err := p.getFields(line)
 66	if err != nil {
 67		return nil, err
 68	}
 69
 70	etime, err := time.Parse(p.layout, date)
 71	if err != nil {
 72		return nil, err
 73	}
 74
 75	if durStart >= durEnd {
 76		return nil, errors.New("invalid duration")
 77	}
 78	start, err := p.militaryTime(durStart)
 79	if err != nil {
 80		return nil, err
 81	}
 82	end, err := p.militaryTime(durEnd)
 83	if err != nil {
 84		return nil, err
 85	}
 86
 87	// Add start duration to entry date, allows reconstructing the
 88	// absolute start time (and end time) of the given entry.
 89	etime = etime.Add(time.Duration(start) * time.Minute)
 90
 91	duration := time.Duration(end-start) * time.Minute
 92	return &Entry{etime, duration, desc}, nil
 93}
 94
 95func (p *Parser) ParseEntries(fn string, r io.Reader) ([]*Entry, error) {
 96	var entries []*Entry
 97
 98	// Reset line number information
 99	p.lineNum = 0
100
101	scanner := bufio.NewScanner(r)
102	for scanner.Scan() {
103		p.lineNum++
104		line := scanner.Text()
105
106		entry, err := p.parseEntry(line)
107		if err != nil {
108			return entries, ParserError{fn, p.lineNum, err.Error()}
109		}
110
111		entries = append(entries, entry)
112	}
113
114	err := scanner.Err()
115	if err != nil {
116		return entries, err
117	}
118
119	return entries, nil
120}
121
122func NewParser(layout string) *Parser {
123	validLine := regexp.MustCompile(lineFormat)
124	return &Parser{validLine, layout, 0}
125}
126
127func DefaultTimeFormat() string {
128	dateLayout := os.Getenv(layoutEnv)
129	if dateLayout == "" {
130		dateLayout = defaultLayout
131	}
132
133	return dateLayout
134}