Add export utility with support for FHG timesheets (CSV)

This commit is contained in:
Brian Buller 2018-01-25 09:52:31 -06:00
parent 39dc32a5b9
commit a267d301d4

268
cmd/gime-export/main.go Normal file
View File

@ -0,0 +1,268 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"git.bullercodeworks.com/brian/gime"
userConfig "github.com/br0xen/user-config"
)
const (
AppName = "gime"
AppVersion = 1
DefDBName = "gime.db"
DefArchDBName = "gimearch.db"
)
var validOperations map[string][]string
var opFuncs map[string]func([]string) int
var timeEntries *gime.TimeEntryCollection
var gdb *gime.GimeDB
var cfg *userConfig.Config
var fuzzyFormats []string
func main() {
var ret int
initialize()
var parms []string
if len(os.Args) > 1 {
parms = os.Args[1:]
parms[0] = matchParameter(parms[0])
} else {
// If no parameters were passed, 'help'
parms = append(parms, "help")
}
if fn, ok := opFuncs[parms[0]]; ok {
ret = fn(parms[1:])
} else {
fmt.Println("Unknown command")
ret = 1
}
os.Exit(ret)
}
func loadRecentTimeEntries() {
timeEntries = gdb.LoadTimeEntryCollection(gime.TypeRecent)
}
func cmdPrintFHGTimesheet(args []string) int {
loadRecentTimeEntries()
var beg, end time.Time
searchTags := []string{}
for _, opt := range args {
var tmpBeg, tmpEnd time.Time
// Do our best to figure out what timers the user wants to list
var err error
if strings.Contains(opt, "-") {
pts := strings.Split(opt, "-")
if len(pts[0]) > 0 {
// This should be the starting date
tmpBeg, err = parseFuzzyTime(pts[0])
if err != nil {
// We couldn't parse it as a time,
// Probably this is just a tag
searchTags = append(searchTags, opt)
continue
}
}
if len(pts[1]) > 0 {
// This should be the ending date
tmpEnd, err = parseFuzzyTime(pts[1])
if err != nil {
searchTags = append(searchTags, opt)
continue
}
}
} else {
// Tag filters
searchTags = append(searchTags, opt)
}
if !tmpBeg.IsZero() || !tmpEnd.IsZero() {
beg, end = tmpBeg, tmpEnd
}
}
if end.IsZero() {
end = time.Now()
}
timeSpanFilter := func(t *gime.TimeEntry) bool {
return t.GetStart().After(beg) && t.GetEnd().Before(end)
}
tagFilter := func(t *gime.TimeEntry) bool {
for i := range searchTags {
if !t.HasTag(searchTags[i]) {
return false
}
}
return true
}
compoundFilter := func(t *gime.TimeEntry) bool {
// Otherwise we want to filter timespan and tags
return timeSpanFilter(t) && tagFilter(t)
}
ttl := fmt.Sprintf("%s - %s", beg.Format("2006/01/02"), end.Format("2006/01/02"))
if len(searchTags) > 0 {
ttl = fmt.Sprint(ttl, " ", searchTags)
}
fmt.Println(ttl)
timerCollection := filterTimerCollection(timeEntries, compoundFilter)
dayTimes := make(map[string]float64)
for i := beg; i.Before(end); i = i.Add(time.Hour * 24) {
dayTimes[i.Format("2006/01/02")] = 0
}
for i := timerCollection.Length() - 1; i >= 0; i-- {
wrkTimer := timerCollection.Get(i)
dur := wrkTimer.GetEnd().Sub(wrkTimer.GetStart()).Round(time.Minute * 15)
dayTimes[wrkTimer.GetStart().Format("2006/01/02")] += (dur.Minutes() / 60)
}
out := [2]string{"", ""}
for i := beg; i.Before(end); i = i.Add(time.Hour * 24) {
dayString := i.Format("2006/01/02")
out[0] += fmt.Sprint(dayString, ",")
out[1] += fmt.Sprint(dayTimes[dayString], ",")
}
fmt.Println(out[0][:len(out[0])-1])
fmt.Println(out[1][:len(out[1])-1])
return 0
}
func initialize() {
var err error
validOperations = make(map[string][]string)
opFuncs = make(map[string]func([]string) int)
opFuncs["fhgts"] = cmdPrintFHGTimesheet
validOperations["fhgts"] = []string{
"fhgts [duration] [tags] - Output entries in Flint Hills Group timesheet format",
" (csv, daily totals, decimal format, 1/4 hour rounding)",
}
// Load the Config
cfg, err = userConfig.NewConfig(AppName)
if err != nil {
fmt.Println(err.Error())
fmt.Println("Creating new config")
cfg.Save()
}
// If dbdir isn't set, set it to the config directory
if cfg.Get("dbdir") == "" {
cfg.Set("dbdir", cfg.GetConfigPath()+"/")
}
// If dbname isn't set, set it to the default database filename
if cfg.Get("dbname") == "" {
cfg.Set("dbname", DefDBName)
}
// If dbarchivename isn't set, set it to the default archive database filename
if cfg.Get("dbarchname") == "" {
cfg.Set("dbarchname", DefArchDBName)
}
if gdb, err = gime.LoadDatabase(cfg.Get("dbdir"), cfg.Get("dbname"), cfg.Get("dbarchname")); err != nil {
fmt.Println("Error loading the database")
os.Exit(1)
}
fuzzyFormats = []string{
"1504",
"15:04", // Kitchen, 24hr
time.Kitchen,
time.RFC3339,
"2006-01-02T15:04:05", // RFC3339 without timezone
"2006-01-02T15:04", // RFC3339 without seconds or timezone
time.Stamp,
"02 Jan 06 15:04:05", // RFC822 with second
time.RFC822,
"01/02/2006 15:04", // U.S. Format
"01/02/2006 15:04:05", // U.S. Format with seconds
"01/02/06 15:04", // U.S. Format, short year
"01/02/06 15:04:05", // U.S. Format, short year, with seconds
"2006-01-02",
"2006-01-02 15:04",
"2006-01-02 15:04:05",
"20060102",
"20060102 15:04",
"20060102 15:04:05",
"20060102 1504",
"20060102 150405",
"20060102T15:04",
"20060102T15:04:05",
"20060102T1504",
"20060102T150405",
}
}
func matchParameter(in string) string {
var chkParms []string
for k := range validOperations {
chkParms = append(chkParms, k)
}
var nextParms []string
for i := range in {
for _, p := range chkParms {
if p[i] == in[i] {
nextParms = append(nextParms, p)
}
}
// If we get here and there is only one parameter left, return it
chkParms = nextParms
if len(nextParms) == 1 {
break
}
// Otherwise, loop
nextParms = []string{}
}
if len(chkParms) == 0 {
return ""
}
return chkParms[0]
}
func parseFuzzyTime(t string) (time.Time, error) {
var ret time.Time
var err error
for i := range fuzzyFormats {
ret, err = time.Parse(fuzzyFormats[i], t)
if err == nil {
// Make sure it's in the local timezone
tz := time.Now().Format("Z07:00")
t = ret.Format("2006-01-02T15:04:05") + tz
if ret, err = time.Parse(time.RFC3339, t); err != nil {
return ret, err
}
// Check for zero on year/mo/day
if ret.Year() == 0 && ret.Month() == time.January && ret.Day() == 1 {
ret = ret.AddDate(time.Now().Year(), int(time.Now().Month())-1, time.Now().Day()-1)
}
return ret, nil
}
}
return time.Time{}, errors.New("Unable to parse time: " + t)
}
// filterTimerCollection takes a collection and a function that it runs every entry through
// If the function returns true for the entry, it adds it to a new collection to be returned
func filterTimerCollection(c *gime.TimeEntryCollection, fn func(t *gime.TimeEntry) bool) *gime.TimeEntryCollection {
ret := new(gime.TimeEntryCollection)
for i := 0; i < c.Length(); i++ {
if fn(c.Get(i)) {
ret.Push(c.Get(i))
}
}
return ret
}