package util import ( "bufio" "errors" "fmt" "os" "sort" "strings" "time" timertxt "git.bullercodeworks.com/brian/go-timertxt" "github.com/spf13/viper" ) const ( ROUND_UP = 1 ROUND_EITHER = 0 ROUND_DOWN = -1 ) func PromptUser(text string) string { var resp string fmt.Println(text) scanner := bufio.NewScanner(os.Stdin) if scanner.Scan() { resp = scanner.Text() } return resp } // 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 } 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 } } func GetRoundToDuration() time.Duration { var dur time.Duration dur, _ = time.ParseDuration(viper.GetString("roundto")) return dur } func DurationToDecimal(dur time.Duration) float64 { mins := dur.Minutes() - (dur.Hours() * 60) return dur.Hours() + (mins / 60) } func AddDurations(dur1, dur2 time.Duration) time.Duration { return time.Duration(int64(dur1) + int64(dur2)) } func SubDurations(dur1, dur2 time.Duration) time.Duration { return time.Duration(int64(dur1) - int64(dur2)) } // GetContextsFromSlice pulls all '@' (contexts) out of the // string slice and return those contexts and the remaining // strings from the slice func GetContextsFromSlice(args []string) ([]string, []string) { return SplitSlice(args, func(v string) bool { return strings.HasPrefix(v, "@") }) } // GetProjectsFromSlice pulls all '+' (projects) out of the // string slice and return those projects and the remaining // strings from the slice func GetProjectsFromSlice(args []string) ([]string, []string) { return SplitSlice(args, func(v string) bool { return strings.HasPrefix(v, "+") }) } // GetAdditionalTagsFromSlice pulls all '*:*' (tags) out of the // string slice and returns those tags and the remaining // strings from the slice func GetAdditionalTagsFromSlice(args []string) ([]string, []string) { return SplitSlice(args, func(v string) bool { return strings.Contains(v, ":") }) } func SplitSlice(args []string, predicate func(string) bool) ([]string, []string) { var rem1, rem2 []string for _, v := range args { if predicate(v) { rem1 = append(rem1, v) } else { rem2 = append(rem2, v) } } return rem1, rem2 } 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) } 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", } func TimerToFriendlyString(t *timertxt.Timer) string { var start, end, contexts, projects, tags string start = t.StartDate.Format(FriendlyFormatForTime(t.StartDate)) if t.FinishDate.IsZero() { end = "**:**" } else { end = t.FinishDate.Format(FriendlyFormatForTime(t.FinishDate)) } for _, v := range t.Contexts { contexts += "@" + v + " " } for _, v := range t.Projects { projects += "+" + v + " " } for k, v := range t.AdditionalTags { tags = fmt.Sprintf("%s%s:%s ", tags, k, v) } var dur time.Duration if t.FinishDate.IsZero() { dur = time.Now().Sub(t.StartDate) } else { dur = t.FinishDate.Sub(t.StartDate) } dur = Round(dur) return fmt.Sprintf("% 2d. %s - %s [ %s] [ %s] [ %s] %s ( %.2f )", t.Id, start, end, contexts, projects, tags, t.Notes, DurationToDecimal(dur)) } func FriendlyFormatForTime(t time.Time) string { 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" } // TimeToFriendlyString returns an easier to read version of the time // giving enough details that the user should be fine inferring the rest func TimeToFriendlyString(t time.Time) string { return t.Format(FriendlyFormatForTime(t)) } func SinceToString(tm time.Time) string { return DiffToString(tm, time.Now()) } func DiffToString(tm1, tm2 time.Time) string { ret := "" yr, mo, dy, hr, mn, sc := Diff(tm1, tm2) 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 } func PadRight(st string, l int) string { for len(st) < l { st = st + " " } return st } func PadLeft(st string, l int) string { for len(st) < l { st = " " + st } return st } func Diff(a, b time.Time) (year, month, day, hour, min, sec int) { 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 } 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()) } 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 } func SortedTagKeyList(m map[string]string) []string { var ret []string for k := range m { ret = append(ret, k) } sort.Strings(ret) return ret } func StringSliceContains(sl []string, val string) bool { for i := range sl { if sl[i] == val { return true } } return false }