Skip to content

Commit 89b6d3d

Browse files
committed
Humanize Time
1 parent b633f91 commit 89b6d3d

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

humanize/time.go

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package humanize
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"sort"
7+
"time"
8+
)
9+
10+
const (
11+
Day = 24 * time.Hour
12+
Week = 7 * Day
13+
Month = 30 * Day
14+
Year = 12 * Month
15+
LongTime = 37 * Year
16+
)
17+
18+
// Format the time into a relative string from now. E.g. "5 minutes ago" or "5 minutes from now".
19+
// Inspiration from: https://github.com/dustin/go-humanize/blob/master/times.go
20+
func Time(ts time.Time) string {
21+
return RelativeTime(ts, time.Now(), "ago", "from now", defaultMagnitudes)
22+
}
23+
24+
// A TimePeriod struct contains a relative time point at which the relative duration
25+
// format will switch to a new format string. E.g. if the duration (D) is 1 minute,
26+
// then we would express a relative duration as "x seconds ago/from now" for any
27+
// duration within the time period.
28+
//
29+
// The format string must contain a single %s verb, which will be replaced with the
30+
// appropriate time direction (e.g. "ago" or "from now") and a %d which will be replaced
31+
// with the quantity of time periods.
32+
type TimePeriod struct {
33+
D time.Duration // The length of the time period
34+
Format string // The format string to use for the time period
35+
Den time.Duration // The denominator to use to determine the number of periods
36+
}
37+
38+
var defaultMagnitudes = []TimePeriod{
39+
{time.Second, "now", time.Second},
40+
{2 * time.Second, "1 second %s", 1},
41+
{time.Minute, "%d seconds %s", time.Second},
42+
{2 * time.Minute, "1 minute %s", 1},
43+
{time.Hour, "%d minutes %s", time.Minute},
44+
{2 * time.Hour, "1 hour %s", 1},
45+
{Day, "%d hours %s", time.Hour},
46+
{2 * Day, "1 day %s", 1},
47+
{Week, "%d days %s", Day},
48+
{2 * Week, "1 week %s", 1},
49+
{Month, "%d weeks %s", Week},
50+
{2 * Month, "1 month %s", 1},
51+
{Year, "%d months %s", Month},
52+
{18 * Month, "1 year %s", 1},
53+
{2 * Year, "2 years %s", 1},
54+
{LongTime, "%d years %s", Year},
55+
{math.MaxInt64, "a long while %s", 1},
56+
}
57+
58+
// Since returns a string representing the time elapsed between t0 and t1. You must
59+
// also pass in a label for the time period, e.g. "ago" or "earlier" to describe the
60+
// amount of time that has passed. If t1 is before t0, you should use Until.
61+
func Since(t0, t1 time.Time, label string) string {
62+
return RelativeTime(t0, t1, label, "", defaultMagnitudes)
63+
}
64+
65+
// Until returns a string representing the time that will elapse between t0 and t1. You
66+
// must also pass in a label for the time period, e.g. "from now" or "later" to describe
67+
// the amount of time that will pass. If t1 is before t0, you should use Since.
68+
func Until(t0, t1 time.Time, label string) string {
69+
return RelativeTime(t0, t1, "", label, defaultMagnitudes)
70+
}
71+
72+
func RelativeTime(t0, t1 time.Time, earlier, later string, periods []TimePeriod) string {
73+
// Determine which the direction of time based on the order of t0 and t1
74+
var (
75+
diff time.Duration
76+
label string
77+
)
78+
79+
if t1.After(t0) {
80+
diff = t1.Sub(t0)
81+
label = earlier
82+
} else {
83+
diff = t0.Sub(t1)
84+
label = later
85+
}
86+
87+
// Find the period that best describes the time difference
88+
n := sort.Search(len(periods), func(i int) bool {
89+
return periods[i].D > diff
90+
})
91+
92+
// If the diff is greater than any of the periods, use the last period
93+
if n >= len(periods) {
94+
n = len(periods) - 1
95+
}
96+
97+
// Prepare the format string
98+
mag := periods[n]
99+
args := []interface{}{}
100+
101+
// Check the characters for each format string to order the arguments correctly
102+
escaped := false
103+
for _, char := range mag.Format {
104+
if char == '%' {
105+
escaped = true
106+
continue
107+
}
108+
109+
if escaped {
110+
switch char {
111+
case 's':
112+
args = append(args, label)
113+
case 'd':
114+
args = append(args, diff/mag.Den)
115+
}
116+
escaped = false
117+
}
118+
}
119+
120+
return fmt.Sprintf(mag.Format, args...)
121+
}

humanize/time_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package humanize_test
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"go.rtnl.ai/x/assert"
11+
"go.rtnl.ai/x/humanize"
12+
)
13+
14+
func TestTime(t *testing.T) {
15+
tests := []struct {
16+
time time.Time
17+
suffix string
18+
}{
19+
{time.Now().Add(-265 * time.Second), "ago"},
20+
{time.Now().Add(265 * time.Second), "from now"},
21+
}
22+
23+
for i, tc := range tests {
24+
dur := humanize.Time(tc.time)
25+
assert.True(t, strings.HasSuffix(dur, tc.suffix), "test case %d failed", i)
26+
}
27+
}
28+
29+
func TestRelativeTime(t *testing.T) {
30+
t1 := time.Date(2021, 1, 21, 21, 21, 21, 0, time.UTC)
31+
durations := []struct {
32+
dur time.Duration
33+
fmt string
34+
}{
35+
{1500 * time.Millisecond, "1 second %s"},
36+
{31527 * time.Millisecond, "31 seconds %s"},
37+
{70 * time.Second, "1 minute %s"},
38+
{831 * time.Second, "13 minutes %s"},
39+
{98 * time.Minute, "1 hour %s"},
40+
{192 * time.Minute, "3 hours %s"},
41+
{32 * time.Hour, "1 day %s"},
42+
{98 * time.Hour, "4 days %s"},
43+
{9 * humanize.Day, "1 week %s"},
44+
{18 * humanize.Day, "2 weeks %s"},
45+
{6 * humanize.Week, "1 month %s"},
46+
{36 * humanize.Week, "8 months %s"},
47+
{14 * humanize.Month, "1 year %s"},
48+
{69 * humanize.Month, "5 years %s"},
49+
{180 * humanize.Year, "a long while %s"},
50+
{time.Duration(math.MaxInt64), "a long while %s"},
51+
}
52+
53+
t.Run("Since", func(t *testing.T) {
54+
assert.Equal(t, "now", humanize.Since(t1, t1, "ago"), "now test 1 failed")
55+
assert.Equal(t, "now", humanize.Since(t1.Add(-500*time.Millisecond), t1, "ago"), "now test 2 failed")
56+
57+
for i, tc := range durations {
58+
t0 := t1.Add(-1 * tc.dur)
59+
assert.Equal(t, fmt.Sprintf(tc.fmt, "ago"), humanize.Since(t0, t1, "ago"), "test case %d failed", i)
60+
}
61+
})
62+
63+
t.Run("Until", func(t *testing.T) {
64+
assert.Equal(t, "now", humanize.Since(t1, t1, "from now"), "now test 1 failed")
65+
assert.Equal(t, "now", humanize.Since(t1.Add(500*time.Millisecond), t1, "from now"), "now test 2 failed")
66+
67+
for i, tc := range durations {
68+
t0 := t1.Add(tc.dur)
69+
assert.Equal(t, fmt.Sprintf(tc.fmt, "from now"), humanize.Until(t0, t1, "from now"), "test case %d failed", i)
70+
}
71+
})
72+
}

0 commit comments

Comments
 (0)