From a267d301d4200343d1e09c8f163c9f8bcb603183 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 25 Jan 2018 09:52:31 -0600 Subject: [PATCH] Add export utility with support for FHG timesheets (CSV) --- cmd/gime-export/main.go | 268 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 cmd/gime-export/main.go diff --git a/cmd/gime-export/main.go b/cmd/gime-export/main.go new file mode 100644 index 0000000..a0c6010 --- /dev/null +++ b/cmd/gime-export/main.go @@ -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 +}