// Package invoicetxt implements structs ad routines for working with invoice.txt files package invoicetxt import "strings" type Invoice struct { ID int `json:"id"` // Invoice id Original string `json:"original"` // original raw invoice text Hours float64 `json:"hours"` Rate float64 `json:"rate"` RetainerHours float64 `json:"retainerHours,omitempty"` RetainerRate float64 `json:"retainerRate,omitempty"` Contexts []string `json:"contexts"` Projects []string `json:"projects"` AdditionalTags map[string]string `json:"additionalTags"` } // ParseInvoice parses the input test string into an Invoice struct func ParseInvoice(text string) (*Invoice, error) { var err error invoice := Invoice{ AdditionalTags: make(map[string]string), } invoice.Original = strings.Trim(text, "\t\n\r") parts := getParts(invoice.Original) var pIdx int for pIdx = range parts { if partIsSpecial(parts[pIdx]) { break } } return &invoice, err } func partIsSpecial(pt string) bool { if len(pt) == 0 { return false } return pt[0] == '#' || pt[0] == '@' } // getParts parses the text from 'text' pu // Generally it just splits everything on spaces, except if we're in a double-quote func getParts(text string) []string { var ret []string var wrk string quoteStart := -1 for i := range text { if quoteStart >= 0 { if text[i] == '"' { quoteStart = -1 ret = append(ret, wrk) } else { wrk = wrk + string(text[i]) } } else { switch text[i] { case '"': quoteStart = i case ' ': if len(wrk) > 0 { ret = append(ret, wrk) } wrk = "" default: wrk = wrk + string(text[i]) } } } if len(wrk) > 0 { ret = append(ret, wrk) } return ret }