Skip to content

Commit

Permalink
exfmt: make duration formatting more customizable
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Sep 27, 2024
1 parent 1734c3c commit 000d30f
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 13 deletions.
58 changes: 45 additions & 13 deletions exfmt/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,67 @@ import (
var Day = 24 * time.Hour
var Week = 7 * Day

func pluralize(value int, unit string) string {
if value == 1 {
return "1 " + unit
type Pluralizer func(int) string

func Pluralizable(unit string) Pluralizer {
return func(value int) string {
if value == 1 {
return "1 " + unit
}
return fmt.Sprintf("%d %ss", value, unit)
}
}

func NonPluralizable(unit string) Pluralizer {
return func(value int) string {
return fmt.Sprintf("%d %s", value, unit)
}
return fmt.Sprintf("%d %ss", value, unit)
}

func appendDurationPart(time, unit time.Duration, name string, parts *[]string) (remainder time.Duration) {
func Duration(d time.Duration) string {
return DurationCustom(d, nil, Week, Day, time.Hour, time.Minute, time.Second)
}

func appendDurationPart(time, unit time.Duration, name Pluralizer, parts *[]string) (remainder time.Duration) {
if time < unit {
return time
}
value := int(time / unit)
remainder = time % unit
*parts = append(*parts, pluralize(value, name))
*parts = append(*parts, name(value))
return
}

func Duration(d time.Duration) string {
var DefaultDurationUnitNames = map[time.Duration]Pluralizer{
Week: Pluralizable("week"),
Day: Pluralizable("day"),
time.Hour: Pluralizable("hour"),
time.Minute: Pluralizable("minute"),
time.Second: Pluralizable("second"),
time.Millisecond: NonPluralizable("ms"),
time.Microsecond: NonPluralizable("µs"),
time.Nanosecond: NonPluralizable("ns"),
}

func DurationCustom(d time.Duration, names map[time.Duration]Pluralizer, units ...time.Duration) string {
if d < 0 {
panic(errors.New("exfmt.Duration: negative duration"))
} else if d < time.Second {
} else if len(units) == 0 {
panic(errors.New("exfmt.Duration: no units provided"))
} else if d < units[len(units)-1] {
return "now"
}
if names == nil {
names = DefaultDurationUnitNames
}
parts := make([]string, 0, 2)
d = appendDurationPart(d, Week, "week", &parts)
d = appendDurationPart(d, Day, "day", &parts)
d = appendDurationPart(d, time.Hour, "hour", &parts)
d = appendDurationPart(d, time.Minute, "minute", &parts)
_ = appendDurationPart(d, time.Second, "second", &parts)
for _, unit := range units {
name, ok := names[unit]
if !ok {
panic(fmt.Errorf("exfmt.Duration: no name for unit %q", unit))
}
d = appendDurationPart(d, unit, name, &parts)
}
if len(parts) > 2 {
parts[0] = strings.Join(parts[:len(parts)-1], ", ")
parts[1] = parts[len(parts)-1]
Expand Down
6 changes: 6 additions & 0 deletions exfmt/duration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ func TestFormatDuration_PanicNegative(t *testing.T) {
assert.Panics(t, func() { exfmt.Duration(-time.Second) })
assert.Panics(t, func() { exfmt.Duration(-exfmt.Week) })
}

func TestFormatDuration_Custom(t *testing.T) {
assert.Equal(t, "90 days", exfmt.DurationCustom(90*exfmt.Day, nil, exfmt.Day))
assert.Equal(t, "2160 hours", exfmt.DurationCustom(90*exfmt.Day, nil, time.Hour))
assert.Equal(t, "2160 hours", exfmt.DurationCustom(90*exfmt.Day+59*time.Minute, nil, time.Hour))
}

0 comments on commit 000d30f

Please sign in to comment.