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}