Add export utility with support for FHG timesheets (CSV)
This commit is contained in:
parent
39dc32a5b9
commit
a267d301d4
268
cmd/gime-export/main.go
Normal file
268
cmd/gime-export/main.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user