package humanize import ( "fmt" "github.com/kovidgoyal/kitty/tools/wcswidth" "math" "sort" "strings" "time" ) // Seconds-based time units const ( Day = 24 * time.Hour Week = 7 * Day Month = 30 * Day Year = 12 * Month LongTime = 37 * Year ) // Time formats a time into a relative string. // // Time(someT) -> "3 weeks ago" func Time(then time.Time) string { return RelTime(then, time.Now(), "ago", "from now") } // A RelTimeMagnitude struct contains a relative time point at which // the relative format of time will switch to a new format string. A // slice of these in ascending order by their "D" field is passed to // CustomRelTime to format durations. // // The Format field is a string that may contain a "%s" which will be // replaced with the appropriate signed label (e.g. "ago" or "from // now") and a "%d" that will be replaced by the quantity. // // The DivBy field is the amount of time the time difference must be // divided by in order to display correctly. // // e.g. if D is 2*time.Minute and you want to display "%d minutes %s" // DivBy should be time.Minute so whatever the duration is will be // expressed in minutes. type RelTimeMagnitude struct { D time.Duration Format string DivBy time.Duration } var defaultMagnitudes = []RelTimeMagnitude{ {time.Second, "now", time.Second}, {2 * time.Second, "1 second %s", 1}, {time.Minute, "%d seconds %s", time.Second}, {2 * time.Minute, "1 minute %s", 1}, {time.Hour, "%d minutes %s", time.Minute}, {2 * time.Hour, "1 hour %s", 1}, {Day, "%d hours %s", time.Hour}, {2 * Day, "1 day %s", 1}, {Week, "%d days %s", Day}, {2 * Week, "1 week %s", 1}, {Month, "%d weeks %s", Week}, {2 * Month, "1 month %s", 1}, {Year, "%d months %s", Month}, {18 * Month, "1 year %s", 1}, {2 * Year, "2 years %s", 1}, {LongTime, "%d years %s", Year}, {math.MaxInt64, "a long while %s", 1}, } // RelTime formats a time into a relative string. // // It takes two times and two labels. In addition to the generic time // delta string (e.g. 5 minutes), the labels are used applied so that // the label corresponding to the smaller time is applied. // // RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" func RelTime(a, b time.Time, albl, blbl string) string { return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) } // CustomRelTime formats a time into a relative string. // // It takes two times two labels and a table of relative time formats. // In addition to the generic time delta string (e.g. 5 minutes), the // labels are used applied so that the label corresponding to the // smaller time is applied. func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { lbl := albl diff := b.Sub(a) if a.After(b) { lbl = blbl diff = a.Sub(b) } n := sort.Search(len(magnitudes), func(i int) bool { return magnitudes[i].D > diff }) if n >= len(magnitudes) { n = len(magnitudes) - 1 } mag := magnitudes[n] args := []any{} escaped := false for _, ch := range mag.Format { if escaped { switch ch { case 's': args = append(args, lbl) case 'd': args = append(args, diff/mag.DivBy) } escaped = false } else { escaped = ch == '%' } } return fmt.Sprintf(mag.Format, args...) } func optional_cut(x string, sep string) (string, string) { a, b, found := strings.Cut(x, sep) if found { return a, b } return "00", a } func zero_pad(x string) string { if len(x) < 2 { x = strings.Repeat("0", 2-len(x)) + x } return x } // Render the duration in exactly 8 visual chars func ShortDuration(val time.Duration) (ans string) { if val >= time.Second { if val > Day { days := int(val.Hours() / 24) if days > 99 { ans = `∞` } else { if days == 1 { ans = ">1 day" } else { ans = fmt.Sprintf(">%d days", int(days)) } } } else { ans = val.String() hr, rest := optional_cut(ans, `h`) min, rest := optional_cut(rest, `m`) secs, _, _ := strings.Cut(rest, ".") secs = strings.Replace(secs, `s`, ``, 1) ans = zero_pad(hr) + `:` + zero_pad(min) + `:` + zero_pad(secs) } } else { ans = "<1 sec" } if w := wcswidth.Stringwidth(ans); w < 8 { ans = strings.Repeat(" ", 8-w) + ans } else if w > 8 { ans = wcswidth.TruncateToVisualLength(ans, 8) } return }