|
@@ -0,0 +1,177 @@
|
|
|
+package humanize
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "git.nspix.com/golang/kos/util/bs"
|
|
|
+ "math"
|
|
|
+ "sort"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ Day = 24 * time.Hour
|
|
|
+ Week = 7 * Day
|
|
|
+ Month = 30 * Day
|
|
|
+ Year = 12 * Month
|
|
|
+ LongTime = 37 * Year
|
|
|
+)
|
|
|
+
|
|
|
+// 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 := []interface{}{}
|
|
|
+ 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...)
|
|
|
+}
|
|
|
+
|
|
|
+type Time struct {
|
|
|
+ tm time.Time
|
|
|
+}
|
|
|
+
|
|
|
+func Now() Time {
|
|
|
+ return Time{tm: time.Now()}
|
|
|
+}
|
|
|
+
|
|
|
+func WrapTime(t time.Time) Time {
|
|
|
+ return Time{tm: t}
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) Add(d Duration) Time {
|
|
|
+ t.tm = t.tm.Add(d.Duration())
|
|
|
+ return t
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) AddDuration(d time.Duration) Time {
|
|
|
+ t.tm = t.tm.Add(d)
|
|
|
+ return t
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) After(u Time) bool {
|
|
|
+ return t.tm.After(u.tm)
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) AfterTime(u time.Time) bool {
|
|
|
+ return t.tm.After(u)
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) Sub(u Time) Duration {
|
|
|
+ return Duration(t.tm.Sub(u.tm))
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) SubTime(u time.Time) Duration {
|
|
|
+ return Duration(t.tm.Sub(u))
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) Time() time.Time {
|
|
|
+ return t.tm
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) String() string {
|
|
|
+ return t.tm.Format(time.DateTime)
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) MarshalJSON() ([]byte, error) {
|
|
|
+ s := `"` + t.tm.Format(time.DateTime) + `"`
|
|
|
+ return bs.StringToBytes(s), nil
|
|
|
+}
|
|
|
+
|
|
|
+func (t Time) Ago() string {
|
|
|
+ return RelTime(t.tm, time.Now(), "ago", "from now")
|
|
|
+}
|
|
|
+
|
|
|
+func (t *Time) UnmarshalJSON(buf []byte) (err error) {
|
|
|
+ buf = bytes.TrimFunc(buf, func(r rune) bool {
|
|
|
+ if r == '"' {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ })
|
|
|
+ t.tm, err = time.ParseInLocation(time.DateTime, bs.BytesToString(buf), time.Local)
|
|
|
+ return err
|
|
|
+}
|