Rounding out library
This commit is contained in:
210
invoice.go
210
invoice.go
@@ -1,18 +1,26 @@
|
||||
// Package invoicetxt implements structs ad routines for working with invoice.txt files
|
||||
package invoicetxt
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An invoice.txt entry looks like:
|
||||
// <invoiceId> <hours> <rate> [retainerHours] [retainerRate] [retainerRollover] <contexts> <projects> <tags>
|
||||
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"`
|
||||
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"`
|
||||
RetainerRollover float64 `json:"retainerRollover,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
|
||||
@@ -23,13 +31,64 @@ func ParseInvoice(text string) (*Invoice, error) {
|
||||
}
|
||||
invoice.Original = strings.Trim(text, "\t\n\r")
|
||||
parts := getParts(invoice.Original)
|
||||
var pIdx int
|
||||
for pIdx = range parts {
|
||||
if partIsSpecial(parts[pIdx]) {
|
||||
break
|
||||
id, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing invoice id: %w", err)
|
||||
}
|
||||
invoice.ID = id
|
||||
hr, err := strconv.ParseFloat(parts[1], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing hours: %w", err)
|
||||
}
|
||||
invoice.Hours = hr
|
||||
rt, err := strconv.ParseFloat(parts[2], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing rate: %w", err)
|
||||
}
|
||||
invoice.Rate = rt
|
||||
lpI := 3
|
||||
var hitS bool
|
||||
for i := 3; i < len(parts); i++ {
|
||||
if partIsSpecial(parts[i]) {
|
||||
hitS = true
|
||||
}
|
||||
if !hitS {
|
||||
wrk, err := strconv.ParseFloat(parts[i], 64)
|
||||
switch i {
|
||||
case 3: // retainer hours
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing retainer hours: %w", err)
|
||||
}
|
||||
invoice.RetainerHours = wrk
|
||||
case 4: // retainer rate
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing retainer rate: %w", err)
|
||||
}
|
||||
invoice.RetainerRate = wrk
|
||||
case 5: // retainer rollover
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing retainer rollover: %w", err)
|
||||
}
|
||||
invoice.RetainerRollover = wrk
|
||||
}
|
||||
continue
|
||||
}
|
||||
// We're in the 'special' parts
|
||||
switch parts[i][0] {
|
||||
case '@': // context
|
||||
invoice.Contexts = append(invoice.Contexts, parts[i][1:])
|
||||
case '+': // project
|
||||
invoice.Projects = append(invoice.Projects, parts[i][1:])
|
||||
default:
|
||||
if strings.Contains(parts[i], ":") {
|
||||
// Additional Tags
|
||||
tagPts := strings.Split(parts[i], ":")
|
||||
if tagPts[0] != "" && tagPts[1] != "" {
|
||||
invoice.AdditionalTags[tagPts[0]] = tagPts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &invoice, err
|
||||
}
|
||||
|
||||
@@ -37,10 +96,10 @@ func partIsSpecial(pt string) bool {
|
||||
if len(pt) == 0 {
|
||||
return false
|
||||
}
|
||||
return pt[0] == '#' || pt[0] == '@'
|
||||
return pt[0] == '#' || pt[0] == '@' || strings.Contains(pts, ":")
|
||||
}
|
||||
|
||||
// getParts parses the text from 'text' pu
|
||||
// getParts parses the text from 'text' pulling out each part.
|
||||
// Generally it just splits everything on spaces, except if we're in a double-quote
|
||||
func getParts(text string) []string {
|
||||
var ret []string
|
||||
@@ -73,3 +132,120 @@ func getParts(text string) []string {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// String returns a complete invoice string in invoice.txt format.
|
||||
//
|
||||
// Contexts, ProjectTags, and Additional Tags are alphabetically sorted,
|
||||
// and appended at the end in the following order:
|
||||
// Contexts, ProjectTags, Tags
|
||||
//
|
||||
// For example:
|
||||
// 315 32.5 100 25 90 @personal +invoicetxt rate:100 retainer:25 retainer_rate:95 retainer_rollover:5
|
||||
func (invoice Invoice) String() string {
|
||||
text := fmt.Sprintf("%d %.2f %.2f", invoice.ID, invoice.Hours, invoice.Rate)
|
||||
var sTxt string
|
||||
if len(invoice.Contexts) > 0 {
|
||||
sort.Strings(invoice.Contexts)
|
||||
for _, c := range invoice.Contexts {
|
||||
sTxt = fmt.Sprintf("%s @%s", c)
|
||||
}
|
||||
}
|
||||
if len(invoice.Projects) > 0 {
|
||||
sort.Strings(invoice.Projects)
|
||||
for _, p := range invoice.Projects {
|
||||
sTxt = fmt.Sprintf("%s +%s", p)
|
||||
}
|
||||
}
|
||||
if len(invoice.AdditionalTags) > 0 {
|
||||
keys := make([]string, 0, len(invoice.AdditionalTags))
|
||||
for key := range invoice.AdditionalTags {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
v := invoice.AdditionalTags[key]
|
||||
if strings.Contains(key, " ") {
|
||||
key = fmt.Sprintf("\"%s\"", key)
|
||||
}
|
||||
if strings.Contains(v, " ") {
|
||||
v = fmt.Sprintf("\"%s\"", v)
|
||||
}
|
||||
sTxt = fmt.Sprintf("%s %s:%s", sTxt, key, v)
|
||||
}
|
||||
}
|
||||
if invoice.RetainerHours == 0 {
|
||||
return fmt.Sprintf("%s %s", text, sTxt)
|
||||
}
|
||||
text = fmt.Sprintf("%s %.2f", text, invoice.RetainerHours)
|
||||
if invoice.RetainerRate == 0 {
|
||||
return fmt.Sprintf("%s %s", text, sTxt)
|
||||
}
|
||||
text = fmt.Sprintf("%s %.2f", text, invoice.RetainerRate)
|
||||
if invoice.RetainerRollover == 0 {
|
||||
return fmt.Sprintf("%s %s", text, sTxt)
|
||||
}
|
||||
text = fmt.Sprintf("%s %.2f", text, invoice.RetainerRollover)
|
||||
return fmt.Sprintf("%s %s", text, sTxt)
|
||||
}
|
||||
|
||||
func (invoice *Invoice) HasContext(context string) bool {
|
||||
for _, v := range invoice.Contexts {
|
||||
if v == context {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (invoice *Invoice) GetContextsString() string {
|
||||
var text string
|
||||
sort.Strings(invoice.Contexts)
|
||||
for _, context := range invoice.Contexts {
|
||||
text = fmt.Sprintf("%s %s", context)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (invoice *Invoice) HasProject(project string) bool {
|
||||
for _, v := range invoice.Projects {
|
||||
if v == project {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (invoice *Invoice) GetProjectsString() string {
|
||||
var text string
|
||||
sort.Strings(invoice.Projects)
|
||||
for _, project := range invoice.Projects {
|
||||
text = fmt.Sprintf("%s %s", project)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (invoice *Invoice) SetTag(name, val string) {
|
||||
invoice.AdditionalTags[name] = val
|
||||
}
|
||||
|
||||
func (invoice *Invoice) HasTag(name string) bool {
|
||||
_, ok := invoice.AdditionalTags[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (invoice *Invoice) GetTag(name string) string {
|
||||
return invoice.AdditionalTags[name]
|
||||
}
|
||||
|
||||
func (invoice *Invoice) GetTagsString() string {
|
||||
var text string
|
||||
keys := make([]string, 0, len(invoice.AdditionalTags))
|
||||
for key := range invoice.AdditionalTags {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
text = fmt.Sprintf("%s %s:%s", text, key, invoice.AdditionnalTags[key])
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user