2022-01-19 18:56:31 +00:00
|
|
|
package util
|
2019-02-21 02:44:07 +00:00
|
|
|
|
|
|
|
import (
|
2022-01-19 18:56:31 +00:00
|
|
|
"bufio"
|
2019-02-21 02:44:07 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-01-19 18:56:31 +00:00
|
|
|
"os"
|
2023-01-06 15:34:55 +00:00
|
|
|
"sort"
|
2019-02-21 02:44:07 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2021-03-02 17:10:29 +00:00
|
|
|
timertxt "git.bullercodeworks.com/brian/go-timertxt"
|
2022-01-19 18:56:31 +00:00
|
|
|
"github.com/spf13/viper"
|
2019-02-21 02:44:07 +00:00
|
|
|
)
|
|
|
|
|
2022-10-04 17:37:11 +00:00
|
|
|
const (
|
|
|
|
ROUND_UP = 1
|
|
|
|
ROUND_EITHER = 0
|
|
|
|
ROUND_DOWN = -1
|
|
|
|
)
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func PromptUser(text string) string {
|
|
|
|
var resp string
|
|
|
|
fmt.Println(text)
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
|
|
if scanner.Scan() {
|
|
|
|
resp = scanner.Text()
|
|
|
|
}
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2019-03-01 14:35:52 +00:00
|
|
|
// TimerToString takes a TimeEntry and gives a nicely formatted string
|
|
|
|
func TimerToString(t *timertxt.Timer) string {
|
|
|
|
var ret string
|
|
|
|
var end string
|
|
|
|
if t.StartsToday() {
|
|
|
|
ret = t.StartDate.Format("15:04 - ")
|
|
|
|
end = "**:**"
|
|
|
|
} else {
|
|
|
|
ret = t.StartDate.Format("2006/01/02 15:04:05 - ")
|
|
|
|
end = "**:**:**"
|
|
|
|
}
|
|
|
|
if !t.FinishDate.IsZero() {
|
|
|
|
if t.EndsToday() {
|
|
|
|
end = t.FinishDate.Format("15:04")
|
|
|
|
} else {
|
|
|
|
end = t.FinishDate.Format("2006/01/02 15:04:05")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ret += end
|
|
|
|
if len(t.Contexts) > 0 {
|
|
|
|
ret += " " + fmt.Sprint(t.Contexts)
|
|
|
|
}
|
|
|
|
if len(t.Projects) > 0 {
|
|
|
|
ret += " " + fmt.Sprint(t.Projects)
|
|
|
|
}
|
|
|
|
if len(t.AdditionalTags) > 0 {
|
|
|
|
ret += " [ "
|
|
|
|
for k, v := range t.AdditionalTags {
|
|
|
|
ret += k + ":" + v + " "
|
|
|
|
}
|
|
|
|
ret += "]"
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2022-10-04 17:37:11 +00:00
|
|
|
func Round(dur time.Duration) time.Duration {
|
|
|
|
roundDur := GetRoundToDuration()
|
|
|
|
wrk := dur.Round(roundDur)
|
|
|
|
switch GetRoundDirection() {
|
|
|
|
case ROUND_UP:
|
|
|
|
if wrk < dur {
|
|
|
|
return wrk + roundDur
|
|
|
|
}
|
|
|
|
case ROUND_DOWN:
|
|
|
|
if wrk > dur {
|
|
|
|
return wrk - roundDur
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return wrk
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetRoundDirection() int {
|
|
|
|
dir := viper.GetString("round")
|
|
|
|
switch dir {
|
|
|
|
case "up":
|
|
|
|
return ROUND_UP
|
|
|
|
case "down":
|
|
|
|
return ROUND_DOWN
|
|
|
|
default:
|
|
|
|
return ROUND_EITHER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-21 02:44:07 +00:00
|
|
|
func GetRoundToDuration() time.Duration {
|
|
|
|
var dur time.Duration
|
2022-01-19 18:56:31 +00:00
|
|
|
dur, _ = time.ParseDuration(viper.GetString("roundto"))
|
2019-02-21 02:44:07 +00:00
|
|
|
return dur
|
|
|
|
}
|
|
|
|
|
|
|
|
func DurationToDecimal(dur time.Duration) float64 {
|
|
|
|
mins := dur.Minutes() - (dur.Hours() * 60)
|
|
|
|
return dur.Hours() + (mins / 60)
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
// GetContextsFromSlice pulls all '@' (contexts) out of the
|
2019-02-21 02:44:07 +00:00
|
|
|
// string slice and return those contexts and the remaining
|
|
|
|
// strings from the slice
|
2022-01-19 18:56:31 +00:00
|
|
|
func GetContextsFromSlice(args []string) ([]string, []string) {
|
|
|
|
return SplitSlice(args, func(v string) bool {
|
2019-02-21 02:44:07 +00:00
|
|
|
return strings.HasPrefix(v, "@")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
// GetProjectsFromSlice pulls all '+' (projects) out of the
|
2019-02-21 02:44:07 +00:00
|
|
|
// string slice and return those projects and the remaining
|
|
|
|
// strings from the slice
|
2022-01-19 18:56:31 +00:00
|
|
|
func GetProjectsFromSlice(args []string) ([]string, []string) {
|
|
|
|
return SplitSlice(args, func(v string) bool {
|
2019-02-21 02:44:07 +00:00
|
|
|
return strings.HasPrefix(v, "+")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
// GetAdditionalTagsFromSlice pulls all '*:*' (tags) out of the
|
2021-03-02 17:05:44 +00:00
|
|
|
// string slice and returns those tags and the remaining
|
2019-03-01 14:35:52 +00:00
|
|
|
// strings from the slice
|
2022-01-19 18:56:31 +00:00
|
|
|
func GetAdditionalTagsFromSlice(args []string) ([]string, []string) {
|
|
|
|
return SplitSlice(args, func(v string) bool {
|
2019-03-01 14:35:52 +00:00
|
|
|
return strings.Contains(v, ":")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func SplitSlice(args []string, predicate func(string) bool) ([]string, []string) {
|
2019-02-21 02:44:07 +00:00
|
|
|
var rem1, rem2 []string
|
|
|
|
for _, v := range args {
|
|
|
|
if predicate(v) {
|
|
|
|
rem1 = append(rem1, v)
|
|
|
|
} else {
|
|
|
|
rem2 = append(rem2, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rem1, rem2
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func ParseFuzzyTime(t string) (time.Time, error) {
|
2019-02-21 02:44:07 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
var 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",
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func TimerToFriendlyString(t *timertxt.Timer) string {
|
2022-01-19 13:45:29 +00:00
|
|
|
var start, end, contexts, projects, tags string
|
2022-01-19 18:56:31 +00:00
|
|
|
start = t.StartDate.Format(FriendlyFormatForTime(t.StartDate))
|
2019-02-21 02:44:07 +00:00
|
|
|
if t.FinishDate.IsZero() {
|
|
|
|
end = "**:**"
|
|
|
|
} else {
|
2022-01-19 18:56:31 +00:00
|
|
|
end = t.FinishDate.Format(FriendlyFormatForTime(t.FinishDate))
|
2019-02-21 02:44:07 +00:00
|
|
|
}
|
|
|
|
for _, v := range t.Contexts {
|
|
|
|
contexts += "@" + v + " "
|
|
|
|
}
|
|
|
|
for _, v := range t.Projects {
|
|
|
|
projects += "+" + v + " "
|
|
|
|
}
|
2022-01-19 13:45:29 +00:00
|
|
|
for k, v := range t.AdditionalTags {
|
|
|
|
tags = fmt.Sprintf("%s%s:%s ", tags, k, v)
|
|
|
|
}
|
2019-02-26 15:41:00 +00:00
|
|
|
var dur time.Duration
|
|
|
|
if t.FinishDate.IsZero() {
|
|
|
|
dur = time.Now().Sub(t.StartDate)
|
|
|
|
} else {
|
|
|
|
dur = t.FinishDate.Sub(t.StartDate)
|
|
|
|
}
|
2022-10-04 17:37:11 +00:00
|
|
|
dur = Round(dur)
|
2022-01-19 13:45:29 +00:00
|
|
|
return fmt.Sprintf("% 2d. %s - %s [ %s] [ %s] [ %s] %s ( %.2f )", t.Id, start, end, contexts, projects, tags, t.Notes, DurationToDecimal(dur))
|
2019-02-21 02:44:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func FriendlyFormatForTime(t time.Time) string {
|
2019-02-21 02:44:07 +00:00
|
|
|
nowTime := time.Now()
|
|
|
|
if t.Year() != nowTime.Year() || t.Month() != nowTime.Month() {
|
|
|
|
return "2006-01-02 15:04"
|
|
|
|
} else if t.Day() != nowTime.Day() {
|
|
|
|
return "01/02 15:04"
|
|
|
|
}
|
|
|
|
return "15:04"
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
// TimeToFriendlyString returns an easier to read version of the time
|
2019-02-21 02:44:07 +00:00
|
|
|
// giving enough details that the user should be fine inferring the rest
|
2022-01-19 18:56:31 +00:00
|
|
|
func TimeToFriendlyString(t time.Time) string {
|
|
|
|
return t.Format(FriendlyFormatForTime(t))
|
2019-02-21 02:44:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func SinceToString(tm time.Time) string {
|
|
|
|
return DiffToString(tm, time.Now())
|
2019-02-21 02:44:07 +00:00
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func DiffToString(tm1, tm2 time.Time) string {
|
2019-02-21 02:44:07 +00:00
|
|
|
ret := ""
|
2022-01-19 18:56:31 +00:00
|
|
|
yr, mo, dy, hr, mn, sc := Diff(tm1, tm2)
|
2019-02-21 02:44:07 +00:00
|
|
|
higher := false
|
|
|
|
|
|
|
|
if yr > 0 {
|
|
|
|
ret += fmt.Sprintf("%4dy ", yr)
|
|
|
|
higher = true
|
|
|
|
}
|
|
|
|
if mo > 0 || higher {
|
|
|
|
ret += fmt.Sprintf("%2dm ", mo)
|
|
|
|
higher = true
|
|
|
|
}
|
|
|
|
if dy > 0 || higher {
|
|
|
|
ret += fmt.Sprintf("%2dd ", dy)
|
|
|
|
higher = true
|
|
|
|
}
|
|
|
|
if hr > 0 || higher {
|
|
|
|
ret += fmt.Sprintf("%2dh ", hr)
|
|
|
|
higher = true
|
|
|
|
}
|
|
|
|
if mn > 0 || higher {
|
|
|
|
ret += fmt.Sprintf("%2dm ", mn)
|
|
|
|
higher = true
|
|
|
|
}
|
|
|
|
if sc > 0 || higher {
|
|
|
|
ret += fmt.Sprintf("%2ds", sc)
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func PadRight(st string, l int) string {
|
2019-02-21 02:44:07 +00:00
|
|
|
for len(st) < l {
|
|
|
|
st = st + " "
|
|
|
|
}
|
|
|
|
return st
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func PadLeft(st string, l int) string {
|
2019-02-21 02:44:07 +00:00
|
|
|
for len(st) < l {
|
|
|
|
st = " " + st
|
|
|
|
}
|
|
|
|
return st
|
|
|
|
}
|
|
|
|
|
2022-01-19 18:56:31 +00:00
|
|
|
func Diff(a, b time.Time) (year, month, day, hour, min, sec int) {
|
2019-02-21 02:44:07 +00:00
|
|
|
if a.Location() != b.Location() {
|
|
|
|
b = b.In(a.Location())
|
|
|
|
}
|
|
|
|
if a.After(b) {
|
|
|
|
a, b = b, a
|
|
|
|
}
|
|
|
|
y1, M1, d1 := a.Date()
|
|
|
|
y2, M2, d2 := b.Date()
|
|
|
|
|
|
|
|
h1, m1, s1 := a.Clock()
|
|
|
|
h2, m2, s2 := b.Clock()
|
|
|
|
|
|
|
|
year = int(y2 - y1)
|
|
|
|
month = int(M2 - M1)
|
|
|
|
day = int(d2 - d1)
|
|
|
|
hour = int(h2 - h1)
|
|
|
|
min = int(m2 - m1)
|
|
|
|
sec = int(s2 - s1)
|
|
|
|
|
|
|
|
// Normalize negative values
|
|
|
|
if sec < 0 {
|
|
|
|
sec += 60
|
|
|
|
min--
|
|
|
|
}
|
|
|
|
if min < 0 {
|
|
|
|
min += 60
|
|
|
|
hour--
|
|
|
|
}
|
|
|
|
if hour < 0 {
|
|
|
|
hour += 24
|
|
|
|
day--
|
|
|
|
}
|
|
|
|
if day < 0 {
|
|
|
|
// days in month:
|
|
|
|
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
|
|
|
|
day += 32 - t.Day()
|
|
|
|
month--
|
|
|
|
}
|
|
|
|
if month < 0 {
|
|
|
|
month += 12
|
|
|
|
year--
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
2021-03-09 14:20:54 +00:00
|
|
|
|
|
|
|
func BeginningOfDay() time.Time {
|
|
|
|
now := time.Now()
|
|
|
|
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
|
|
}
|
|
|
|
|
|
|
|
func BeginningOfWeek() time.Time {
|
|
|
|
now := time.Now()
|
|
|
|
t := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
|
|
weekday := int(t.Weekday())
|
|
|
|
return t.AddDate(0, 0, -weekday)
|
|
|
|
}
|
|
|
|
|
|
|
|
func BeginningOfMonth() time.Time {
|
|
|
|
now := time.Now()
|
|
|
|
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
|
|
}
|
2022-01-20 14:51:39 +00:00
|
|
|
|
|
|
|
func BuildFilterFromArgs(args []string) func(*timertxt.Timer) bool {
|
|
|
|
start := time.Time{}
|
|
|
|
end := time.Now()
|
|
|
|
var contextFilters []string
|
|
|
|
var projectFilters []string
|
|
|
|
var allFilters []func(timertxt.Timer) bool
|
|
|
|
if len(args) > 0 {
|
|
|
|
contextFilters, args = GetContextsFromSlice(args)
|
|
|
|
projectFilters, args = GetProjectsFromSlice(args)
|
|
|
|
}
|
|
|
|
if len(args) > 0 {
|
|
|
|
var err error
|
|
|
|
if start, err = ParseFuzzyTime(args[0]); err != nil {
|
|
|
|
y, m, d := time.Now().Date()
|
|
|
|
start = time.Date(y, m, d, 0, 0, 0, 0, time.Now().Location())
|
|
|
|
} else {
|
|
|
|
args = args[1:]
|
|
|
|
}
|
|
|
|
if len(args) > 0 {
|
|
|
|
if end, err = ParseFuzzyTime(args[0]); err != nil {
|
|
|
|
y, m, d := time.Now().Date()
|
|
|
|
end = time.Date(y, m, d, 23, 59, 59, 0, time.Now().Location())
|
|
|
|
} else {
|
|
|
|
args = args[1:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
allFilters = append(allFilters, func(t timertxt.Timer) bool {
|
|
|
|
if t.StartDate.Before(end) && t.StartDate.After(start) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if t.FinishDate.Before(end) && t.FinishDate.After(start) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
if len(contextFilters) > 0 {
|
|
|
|
allFilters = append(allFilters, func(t timertxt.Timer) bool {
|
|
|
|
for _, v := range contextFilters {
|
|
|
|
v = strings.TrimPrefix(v, "@")
|
|
|
|
if !t.HasContext(v) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if len(projectFilters) > 0 {
|
|
|
|
allFilters = append(allFilters, func(t timertxt.Timer) bool {
|
|
|
|
for _, v := range projectFilters {
|
|
|
|
v = strings.TrimPrefix(v, "+")
|
|
|
|
if !t.HasProject(v) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
doFilters := func(t *timertxt.Timer) bool {
|
|
|
|
for _, v := range allFilters {
|
|
|
|
if !v(*t) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If we made it all the way down here, it matches
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return doFilters
|
|
|
|
}
|
2023-01-06 15:34:55 +00:00
|
|
|
|
|
|
|
func SortedTagKeyList(m map[string]string) []string {
|
|
|
|
var ret []string
|
|
|
|
for k := range m {
|
|
|
|
ret = append(ret, k)
|
|
|
|
}
|
|
|
|
sort.Strings(ret)
|
|
|
|
return ret
|
|
|
|
}
|