commit 0da41b23269ded78809a12a3b452f0733cdd2794 Author: Brian Buller Date: Thu Dec 4 11:50:04 2025 -0600 Initial Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..2aee750 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# go-invoicetxt + +A Go library for working with invoice.txt files. + +A invoice.txt file is a plain text file for invoicing purposes. It stems from the same motivations as todo.txt (http://todotxt.org/ ). + +This library is very similar to https://github.com/br0xen/go-todotxt, which is a fork of https://github.com/JamesClonk/go-todotxt. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..27f7afa --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.bullercodeworks.com/brian/go-invoicetxt + +go 1.25.5 diff --git a/invoice.go b/invoice.go new file mode 100644 index 0000000..4a44dda --- /dev/null +++ b/invoice.go @@ -0,0 +1,75 @@ +// 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 +}