Skip to content

Commit

Permalink
Implement CustomRelTime
Browse files Browse the repository at this point in the history
for #25
  • Loading branch information
dustin committed Jul 21, 2016
1 parent b54b505 commit 3143592
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 145 deletions.
118 changes: 49 additions & 69 deletions times.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import (

// Seconds-based time units
const (
Minute = 60
Hour = 60 * Minute
Day = 24 * Hour
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
Expand All @@ -25,46 +23,45 @@ func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}

// Magnitude stores one magnitude and the output format for this magnitude of relative time.
type Magnitude struct {
d time.Duration
format string
divby time.Duration
}

// NewMagnitude returns a Magnitude object.
// 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.
//
// d is the max number value of relative time for this magnitude.
// divby is the divisor to turn input number value into expected unit.
// format is the expected output format string.
// 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.
//
// Also refer to RelTimeMagnitudes for examples.
func NewMagnitude(d time.Duration, format string, divby time.Duration) Magnitude {
return Magnitude{
d: d,
format: format,
divby: divby,
}
// 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 = []Magnitude{
NewMagnitude(time.Second, "now", time.Second),
NewMagnitude(2*time.Second, "1 second %s", time.Second),
NewMagnitude(Minute*time.Second, "%d seconds %s", time.Second),
NewMagnitude(2*Minute*time.Second, "1 minute %s", time.Second),
NewMagnitude(Hour*time.Second, "%d minutes %s", Minute*time.Second),
NewMagnitude(2*Hour*time.Second, "1 hour %s", time.Second),
NewMagnitude(Day*time.Second, "%d hours %s", Hour*time.Second),
NewMagnitude(2*Day*time.Second, "1 day %s", time.Second),
NewMagnitude(Week*time.Second, "%d days %s", Day*time.Second),
NewMagnitude(2*Week*time.Second, "1 week %s", time.Second),
NewMagnitude(Month*time.Second, "%d weeks %s", Week*time.Second),
NewMagnitude(2*Month*time.Second, "1 month %s", time.Second),
NewMagnitude(Year*time.Second, "%d months %s", Month*time.Second),
NewMagnitude(18*Month*time.Second, "1 year %s", time.Second),
NewMagnitude(2*Year*time.Second, "2 years %s", time.Second),
NewMagnitude(LongTime*time.Second, "%d years %s", Year*time.Second),
NewMagnitude(math.MaxInt64, "a long while %s", time.Second),
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.
Expand All @@ -75,62 +72,45 @@ var defaultMagnitudes = []Magnitude{
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
return RelTimeMagnitudes(a, b, albl, blbl, defaultMagnitudes)
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}

// RelTimeMagnitudes accepts a magnitudes parameter to allow custom defined units and output format.
// CustomRelTime formats a time into a relative string.
//
// example:
// magitudes:
// {
// NewMagnitude(time.Second, "now", time.Second),
// NewMagnitude(60*time.Second, "%d seconds %s", time.Second),
// NewMagnitude(120*time.Second,"a minute %s", time.Second),
// NewMagnitude(360*time.Second, "%d minutes %s", 60*time.Second),
// }
// albl: earlier
// blbl: later
// 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.
//
// b - a output
// -130*time.Second 2 minutes later
// 0 now
// 30*time.Second 30 seconds earlier
// 80*time.Second a minute earlier
// 340*time.Second 5 minutes earlier
// 400*time.Second undefined
func RelTimeMagnitudes(a, b time.Time, albl, blbl string, magnitudes []Magnitude) string {
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)

after := a.After(b)
if after {
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}

n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].d >= diff
return magnitudes[i].D >= diff
})

if n >= len(magnitudes) {
return "undefined"
}

mag := magnitudes[n]
args := []interface{}{}
escaped := false
for _, ch := range mag.format {
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.divby)
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.format, args...)
return fmt.Sprintf(mag.Format, args...)
}
123 changes: 47 additions & 76 deletions times_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,59 @@ import (
)

func TestPast(t *testing.T) {
now := time.Now().Unix()
now := time.Now()
testList{
{"now", Time(time.Unix(now, 0)), "now"},
{"1 second ago", Time(time.Unix(now-1, 0)), "1 second ago"},
{"12 seconds ago", Time(time.Unix(now-12, 0)), "12 seconds ago"},
{"30 seconds ago", Time(time.Unix(now-30, 0)), "30 seconds ago"},
{"45 seconds ago", Time(time.Unix(now-45, 0)), "45 seconds ago"},
{"1 minute ago", Time(time.Unix(now-63, 0)), "1 minute ago"},
{"15 minutes ago", Time(time.Unix(now-15*Minute, 0)), "15 minutes ago"},
{"1 hour ago", Time(time.Unix(now-63*Minute, 0)), "1 hour ago"},
{"2 hours ago", Time(time.Unix(now-2*Hour, 0)), "2 hours ago"},
{"21 hours ago", Time(time.Unix(now-21*Hour, 0)), "21 hours ago"},
{"1 day ago", Time(time.Unix(now-26*Hour, 0)), "1 day ago"},
{"2 days ago", Time(time.Unix(now-49*Hour, 0)), "2 days ago"},
{"3 days ago", Time(time.Unix(now-3*Day, 0)), "3 days ago"},
{"1 week ago (1)", Time(time.Unix(now-7*Day, 0)), "1 week ago"},
{"1 week ago (2)", Time(time.Unix(now-12*Day, 0)), "1 week ago"},
{"2 weeks ago", Time(time.Unix(now-15*Day, 0)), "2 weeks ago"},
{"1 month ago", Time(time.Unix(now-39*Day, 0)), "1 month ago"},
{"3 months ago", Time(time.Unix(now-99*Day, 0)), "3 months ago"},
{"1 year ago (1)", Time(time.Unix(now-365*Day, 0)), "1 year ago"},
{"1 year ago (1)", Time(time.Unix(now-400*Day, 0)), "1 year ago"},
{"2 years ago (1)", Time(time.Unix(now-548*Day, 0)), "2 years ago"},
{"2 years ago (2)", Time(time.Unix(now-725*Day, 0)), "2 years ago"},
{"2 years ago (3)", Time(time.Unix(now-800*Day, 0)), "2 years ago"},
{"3 years ago", Time(time.Unix(now-3*Year, 0)), "3 years ago"},
{"long ago", Time(time.Unix(now-LongTime, 0)), "a long while ago"},
{"now", Time(now), "now"},
{"1 second ago", Time(now.Add(-1 * time.Second)), "1 second ago"},
{"12 seconds ago", Time(now.Add(-12 * time.Second)), "12 seconds ago"},
{"30 seconds ago", Time(now.Add(-30 * time.Second)), "30 seconds ago"},
{"45 seconds ago", Time(now.Add(-45 * time.Second)), "45 seconds ago"},
{"1 minute ago", Time(now.Add(-63 * time.Second)), "1 minute ago"},
{"15 minutes ago", Time(now.Add(-15 * time.Minute)), "15 minutes ago"},
{"1 hour ago", Time(now.Add(-63 * time.Minute)), "1 hour ago"},
{"2 hours ago", Time(now.Add(-2 * time.Hour)), "2 hours ago"},
{"21 hours ago", Time(now.Add(-21 * time.Hour)), "21 hours ago"},
{"1 day ago", Time(now.Add(-26 * time.Hour)), "1 day ago"},
{"2 days ago", Time(now.Add(-49 * time.Hour)), "2 days ago"},
{"3 days ago", Time(now.Add(-3 * Day)), "3 days ago"},
{"1 week ago (1)", Time(now.Add(-7 * Day)), "1 week ago"},
{"1 week ago (2)", Time(now.Add(-12 * Day)), "1 week ago"},
{"2 weeks ago", Time(now.Add(-15 * Day)), "2 weeks ago"},
{"1 month ago", Time(now.Add(-39 * Day)), "1 month ago"},
{"3 months ago", Time(now.Add(-99 * Day)), "3 months ago"},
{"1 year ago (1)", Time(now.Add(-365 * Day)), "1 year ago"},
{"1 year ago (1)", Time(now.Add(-400 * Day)), "1 year ago"},
{"2 years ago (1)", Time(now.Add(-548 * Day)), "2 years ago"},
{"2 years ago (2)", Time(now.Add(-725 * Day)), "2 years ago"},
{"2 years ago (3)", Time(now.Add(-800 * Day)), "2 years ago"},
{"3 years ago", Time(now.Add(-3 * Year)), "3 years ago"},
{"long ago", Time(now.Add(-LongTime)), "a long while ago"},
}.validate(t)
}

func TestFuture(t *testing.T) {
now := time.Now().Unix()
// add 1 second offset for test time to balance decimal fraction of time.Now()
offset := int64(time.Second)
// Add a little time so that these things properly line up in
// the future.
now := time.Now().Add(time.Millisecond * 250)
testList{
{"now", Time(time.Unix(now, 0)), "now"},
{"1 second from now", Time(time.Unix(now+1, offset)), "1 second from now"},
{"12 seconds from now", Time(time.Unix(now+12, offset)), "12 seconds from now"},
{"30 seconds from now", Time(time.Unix(now+30, offset)), "30 seconds from now"},
{"45 seconds from now", Time(time.Unix(now+45, offset)), "45 seconds from now"},
{"15 minutes from now", Time(time.Unix(now+15*Minute, offset)), "15 minutes from now"},
{"2 hours from now", Time(time.Unix(now+2*Hour, offset)), "2 hours from now"},
{"21 hours from now", Time(time.Unix(now+21*Hour, offset)), "21 hours from now"},
{"1 day from now", Time(time.Unix(now+26*Hour, offset)), "1 day from now"},
{"2 days from now", Time(time.Unix(now+49*Hour, offset)), "2 days from now"},
{"3 days from now", Time(time.Unix(now+3*Day, offset)), "3 days from now"},
{"1 week from now (1)", Time(time.Unix(now+7*Day, offset)), "1 week from now"},
{"1 week from now (2)", Time(time.Unix(now+12*Day, offset)), "1 week from now"},
{"2 weeks from now", Time(time.Unix(now+15*Day, offset)), "2 weeks from now"},
{"1 month from now", Time(time.Unix(now+30*Day, offset)), "1 month from now"},
{"1 year from now", Time(time.Unix(now+365*Day, offset)), "1 year from now"},
{"2 years from now", Time(time.Unix(now+2*Year, offset)), "2 years from now"},
{"a while from now", Time(time.Unix(now+LongTime, offset)), "a long while from now"},
{"now", Time(now), "now"},
{"1 second from now", Time(now.Add(+1 * time.Second)), "1 second from now"},
{"12 seconds from now", Time(now.Add(+12 * time.Second)), "12 seconds from now"},
{"30 seconds from now", Time(now.Add(+30 * time.Second)), "30 seconds from now"},
{"45 seconds from now", Time(now.Add(+45 * time.Second)), "45 seconds from now"},
{"15 minutes from now", Time(now.Add(+15 * time.Minute)), "15 minutes from now"},
{"2 hours from now", Time(now.Add(+2 * time.Hour)), "2 hours from now"},
{"21 hours from now", Time(now.Add(+21 * time.Hour)), "21 hours from now"},
{"1 day from now", Time(now.Add(+26 * time.Hour)), "1 day from now"},
{"2 days from now", Time(now.Add(+49 * time.Hour)), "2 days from now"},
{"3 days from now", Time(now.Add(+3 * Day)), "3 days from now"},
{"1 week from now (1)", Time(now.Add(+7 * Day)), "1 week from now"},
{"1 week from now (2)", Time(now.Add(+12 * Day)), "1 week from now"},
{"2 weeks from now", Time(now.Add(+15 * Day)), "2 weeks from now"},
{"1 month from now", Time(now.Add(+30 * Day)), "1 month from now"},
{"1 year from now", Time(now.Add(+365 * Day)), "1 year from now"},
{"2 years from now", Time(now.Add(+2 * Year)), "2 years from now"},
{"a while from now", Time(now.Add(+LongTime)), "a long while from now"},
}.validate(t)
}

Expand All @@ -71,32 +71,3 @@ func TestRange(t *testing.T) {
t.Errorf("Expected a long while from now, got %q", x)
}
}

func TestRelTimeMagnitudes(t *testing.T) {
magnitudes := []Magnitude{
NewMagnitude(1*time.Second, "now", time.Second),
NewMagnitude(2*time.Second, "1s %s", time.Second),
NewMagnitude(Minute*time.Second, "s %s", time.Second),
NewMagnitude(2*Minute*time.Second, "1m %s", time.Second),
NewMagnitude(Hour*time.Second, "%dm %s", Minute*time.Second),
NewMagnitude(2*Hour*time.Second, "1h %s", time.Second),
NewMagnitude(Day*time.Second, "%dh %s", Hour*time.Second),
NewMagnitude(2*Day*time.Second, "1D %s", time.Second),
NewMagnitude(Month*time.Second, "%dD %s", Day*time.Second),
NewMagnitude(2*Month*time.Second, "1M %s", time.Second),
NewMagnitude(Year*time.Second, "%dM %s", Month*time.Second),
NewMagnitude(18*Month*time.Second, "1Y %s", time.Second),
NewMagnitude(2*Year*time.Second, "2Y %s", time.Second),
}
now := time.Now().Unix()
timeNow := time.Unix(now, 0)
testList{
{"now", RelTimeMagnitudes(time.Unix(now, 0), timeNow, "ago", "later", magnitudes), "now"},
{"1 second from now", RelTimeMagnitudes(time.Unix(now+1, 1), timeNow, "ago", "later", magnitudes), "1s later"},
// Unit week has been removed from magnitudes
{"1 week ago", RelTimeMagnitudes(time.Unix(now-12*Day, 0), timeNow, "ago", "", magnitudes), "12D ago"},
{"3 months ago", RelTimeMagnitudes(time.Unix(now-99*Day, 0), timeNow, "ago", "later", magnitudes), "3M ago"},
{"1 year ago", RelTimeMagnitudes(time.Unix(now-365*Day, 0), timeNow, "", "later", magnitudes), "1Y "},
{"out of defined magnitudes", RelTimeMagnitudes(time.Unix(now+LongTime, 0), timeNow, "ago", "later", magnitudes), "undefined"},
}.validate(t)
}

0 comments on commit 3143592

Please sign in to comment.