diff --git a/main.go b/main.go index cb42203..ad87716 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package human_duration import ( "fmt" "math" + "regexp" "strings" "time" ) @@ -18,16 +19,60 @@ const ( Year = "year" ) +func precisionToDuration(precision string) time.Duration { + switch precision { + case Second: + return time.Second + case Minute: + return time.Minute + case Hour: + return time.Hour + case Day: + return time.Hour * 24 + case Year: + return time.Hour * 24 * 365 + default: + return time.Nanosecond + } +} + // String converts duration to human readable format, according to precision. -// Example: -// fmt.Println(human_duration.String(time.Hour*24), human_duration.Hour) func String(duration time.Duration, precision string) string { + return StringCeiling(duration, precision, "") +} + +func StringCeiling(duration time.Duration, precision, ceiling string) string { + return StringCeilingPadded(duration, precision, ceiling, false) +} + +func StringCeilingPadded(duration time.Duration, precision, ceiling string, padded bool) string { years := int64(duration.Hours() / 24 / 365) days := int64(math.Mod(float64(int64(duration.Hours()/24)), 365)) hours := int64(math.Mod(duration.Hours(), 24)) minutes := int64(math.Mod(duration.Minutes(), 60)) seconds := int64(math.Mod(duration.Seconds(), 60)) + switch ceiling { + case Second: + seconds = int64(duration.Seconds()) + minutes = 0 + hours = 0 + days = 0 + years = 0 + case Minute: + minutes = int64(duration.Minutes()) + hours = 0 + days = 0 + years = 0 + case Hour: + hours = int64(duration.Hours()) + days = 0 + years = 0 + case Day: + days = int64(float64(int64(duration.Hours() / 24))) + years = 0 + } + chunks := []struct { singularName string amount int64 @@ -41,32 +86,48 @@ func String(duration time.Duration, precision string) string { parts := []string{} preciseEnough := false + isLeading := true + + unpaddedNumberFormat := "%d" + paddedNumberFormat := "%02d" for _, chunk := range chunks { if preciseEnough { continue } + if chunk.singularName == precision || chunk.singularName+"s" == precision { preciseEnough = true } + + numberFormat := unpaddedNumberFormat + if chunk.amount > 0 && isLeading { + isLeading = false + } else if padded { + numberFormat = paddedNumberFormat + } + switch chunk.amount { case 0: continue case 1: - parts = append(parts, fmt.Sprintf("%d %s", chunk.amount, chunk.singularName)) + parts = append(parts, fmt.Sprintf(numberFormat+" %s", chunk.amount, chunk.singularName)) default: - parts = append(parts, fmt.Sprintf("%d %ss", chunk.amount, chunk.singularName)) + parts = append(parts, fmt.Sprintf(numberFormat+" %ss", chunk.amount, chunk.singularName)) } } return strings.Join(parts, " ") } -// String converts duration to a shortened human readable format, according to precision. -// Example: -// fmt.Println(human_duration.ShortString(time.Hour*24), human_duration.Hour) +// ShortString converts duration to a shortened human readable format, according to precision. func ShortString(duration time.Duration, precision string) string { str := String(duration, precision) + str = shorten(str) + return str +} + +func shorten(str string) string { str = strings.Replace(str, " ", "", -1) str = strings.Replace(str, "years", "y", 1) str = strings.Replace(str, "year", "y", 1) @@ -80,3 +141,20 @@ func ShortString(duration time.Duration, precision string) string { str = strings.Replace(str, "second", "s", 1) return str } + +var trailingColon = regexp.MustCompile(`:$`) + +// Timestamp converts duration to a common timestamp format, often used for videos. +func Timestamp(interval time.Duration, precision string) string { + if precisionToDuration(precision) > time.Hour { + precision = "hours" + } + + str := shorten(StringCeilingPadded(interval, precision, "hour", true)) + str = strings.Replace(str, "h", ":", 1) + str = strings.Replace(str, "m", ":", 1) + str = strings.Replace(str, "s", "", 1) + str = trailingColon.ReplaceAllString(str, "") + + return str +} diff --git a/main_test.go b/main_test.go index 0cccc8e..0838009 100644 --- a/main_test.go +++ b/main_test.go @@ -23,6 +23,7 @@ func ExampleString() { // Output: 1 year 8 hours 33 minutes 24 seconds // 1 year 33 days 1 hour 1 minute 1 second + } func TestString(t *testing.T) { @@ -124,6 +125,24 @@ func TestString(t *testing.T) { } } +func ExampleStringCeiling() { + duration := time.Hour*24 + time.Hour*2 + time.Minute*33 + time.Second*24 + fmt.Println(StringCeiling(duration, Second, Hour)) + + // Output: 26 hours 33 minutes 24 seconds +} + +func ExampleShortString() { + day := time.Hour * 24 + year := day * 365 + + duration := 2*year + 2*day + 2*time.Minute + 2*time.Second + + fmt.Println(ShortString(duration, Second)) + + // Output: 2y2d2m2s +} + func TestShortString(t *testing.T) { day := time.Hour * 24 year := day * 365 @@ -152,3 +171,49 @@ func TestShortString(t *testing.T) { }) } } + +func ExampleTimestamp() { + duration := (25 * time.Hour) + (20 * time.Minute) + (14 * time.Second) + + fmt.Println(Timestamp(duration, "second")) + fmt.Println(Timestamp(duration, "minute")) + + // Output: 25:20:14 + // 25:20 +} + +func TestTimestamp(t *testing.T) { + data := []fixture{ + { + duration: time.Minute + time.Second, + precision: Second, + result: "1:01", + }, + { + duration: (20 * time.Minute) + (14 * time.Second), + precision: Second, + result: "20:14", + }, + { + duration: time.Hour + (20 * time.Minute) + (14 * time.Second), + precision: Second, + result: "1:20:14", + }, + { + duration: (25 * time.Hour) + (20 * time.Minute) + (14 * time.Second), + precision: Second, + result: "25:20:14", + }, + } + + for _, fixture := range data { + f := fixture + t.Run(f.result+" "+f.duration.String(), func(t *testing.T) { + t.Parallel() + result := Timestamp(f.duration, f.precision) + if result != f.result { + t.Errorf("got %s, want %s", result, f.result) + } + }) + } +}