1package parser23import (4 "bufio"5 "errors"6 "io"7 "os"8 "regexp"9 "strconv"10 "time"11)1213const lineFormat = "^\\+?(..*) ([0-9][0-9][0-9][0-9]) ([0-9][0-9][0-9][0-9]) (..*)$"1415const (16 layoutEnv = "TRACKTIME_FORMAT"17 defaultLayout = "02.01.2006"18)1920type Entry struct {21 Date time.Time22 Duration time.Duration23 Description string24 BonusWork bool25}2627type Parser struct {28 validLine *regexp.Regexp29 layout string30 lineNum uint31}3233// https://en.wikipedia.org/wiki/24-hour_clock#Military_time34func (p *Parser) militaryTime(time int) (int, error) {35 hours := time / 10036 minutes := time % 1003738 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 }4344 return (hours * 60) + minutes, nil45}4647func (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 }5253 durStart, err := strconv.Atoi(matches[2])54 if err != nil {55 return false, "", 0, 0, "", err56 }57 durEnd, err := strconv.Atoi(matches[3])58 if err != nil {59 return false, "", 0, 0, "", err60 }6162 // If the line starts with a “+” character, then count this as63 // bonus working time (e.g. working on the weekend).64 bonusEntry := matches[0][0] == '+'6566 return bonusEntry, matches[1], durStart, durEnd, matches[4], nil67}6869func (p *Parser) parseEntry(line string) (*Entry, error) {70 bonus, date, durStart, durEnd, desc, err := p.getFields(line)71 if err != nil {72 return nil, err73 }7475 etime, err := time.Parse(p.layout, date)76 if err != nil {77 return nil, err78 }7980 if durStart >= durEnd {81 return nil, errors.New("invalid duration")82 }83 start, err := p.militaryTime(durStart)84 if err != nil {85 return nil, err86 }87 end, err := p.militaryTime(durEnd)88 if err != nil {89 return nil, err90 }9192 // Add start duration to entry date, allows reconstructing the93 // absolute start time (and end time) of the given entry.94 etime = etime.Add(time.Duration(start) * time.Minute)9596 duration := time.Duration(end-start) * time.Minute97 return &Entry{etime, duration, desc, bonus}, nil98}99100func (p *Parser) ParseEntries(fn string, r io.Reader) ([]*Entry, error) {101 var entries []*Entry102103 // Reset line number information104 p.lineNum = 0105106 scanner := bufio.NewScanner(r)107 for scanner.Scan() {108 p.lineNum++109 line := scanner.Text()110111 entry, err := p.parseEntry(line)112 if err != nil {113 return entries, ParserError{fn, p.lineNum, err.Error()}114 }115116 entries = append(entries, entry)117 }118119 err := scanner.Err()120 if err != nil {121 return entries, err122 }123124 return entries, nil125}126127func NewParser(layout string) *Parser {128 validLine := regexp.MustCompile(lineFormat)129 return &Parser{validLine, layout, 0}130}131132func DefaultTimeFormat() string {133 dateLayout := os.Getenv(layoutEnv)134 if dateLayout == "" {135 dateLayout = defaultLayout136 }137138 return dateLayout139}