Skip to content

Commit

Permalink
Add Timestamp function
Browse files Browse the repository at this point in the history
Also adds Ceiling and Padded options, along with fixing doc examples
  • Loading branch information
davidbanham committed Nov 3, 2022
1 parent b99d807 commit da8d5da
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 7 deletions.
92 changes: 85 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package human_duration
import (
"fmt"
"math"
"regexp"
"strings"
"time"
)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
}
65 changes: 65 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit da8d5da

Please sign in to comment.