2018-01-12 17:30:54 +01:00
|
|
|
package fs
|
|
|
|
|
|
|
|
import (
|
2018-05-14 17:32:27 +02:00
|
|
|
"fmt"
|
|
|
|
"math"
|
2018-01-12 17:30:54 +01:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Duration is a time.Duration with some more parsing options
|
|
|
|
type Duration time.Duration
|
|
|
|
|
2018-03-12 21:52:42 +01:00
|
|
|
// DurationOff is the default value for flags which can be turned off
|
|
|
|
const DurationOff = Duration((1 << 63) - 1)
|
|
|
|
|
2018-01-12 17:30:54 +01:00
|
|
|
// Turn Duration into a string
|
|
|
|
func (d Duration) String() string {
|
2018-03-12 21:52:42 +01:00
|
|
|
if d == DurationOff {
|
|
|
|
return "off"
|
|
|
|
}
|
2018-05-14 17:32:27 +02:00
|
|
|
for i := len(ageSuffixes) - 2; i >= 0; i-- {
|
|
|
|
ageSuffix := &ageSuffixes[i]
|
|
|
|
if math.Abs(float64(d)) >= float64(ageSuffix.Multiplier) {
|
|
|
|
timeUnits := float64(d) / float64(ageSuffix.Multiplier)
|
|
|
|
return strconv.FormatFloat(timeUnits, 'f', -1, 64) + ageSuffix.Suffix
|
|
|
|
}
|
|
|
|
}
|
2018-01-12 17:30:54 +01:00
|
|
|
return time.Duration(d).String()
|
|
|
|
}
|
|
|
|
|
2018-03-12 21:52:42 +01:00
|
|
|
// IsSet returns if the duration is != DurationOff
|
|
|
|
func (d Duration) IsSet() bool {
|
|
|
|
return d != DurationOff
|
|
|
|
}
|
|
|
|
|
2018-01-12 17:30:54 +01:00
|
|
|
// We use time conventions
|
|
|
|
var ageSuffixes = []struct {
|
|
|
|
Suffix string
|
|
|
|
Multiplier time.Duration
|
|
|
|
}{
|
|
|
|
{Suffix: "d", Multiplier: time.Hour * 24},
|
|
|
|
{Suffix: "w", Multiplier: time.Hour * 24 * 7},
|
|
|
|
{Suffix: "M", Multiplier: time.Hour * 24 * 30},
|
|
|
|
{Suffix: "y", Multiplier: time.Hour * 24 * 365},
|
|
|
|
|
|
|
|
// Default to second
|
|
|
|
{Suffix: "", Multiplier: time.Second},
|
|
|
|
}
|
|
|
|
|
2020-05-11 14:25:39 +02:00
|
|
|
// parse the age as suffixed ages
|
|
|
|
func parseDurationSuffixes(age string) (time.Duration, error) {
|
2018-01-12 17:30:54 +01:00
|
|
|
var period float64
|
|
|
|
|
|
|
|
for _, ageSuffix := range ageSuffixes {
|
|
|
|
if strings.HasSuffix(age, ageSuffix.Suffix) {
|
|
|
|
numberString := age[:len(age)-len(ageSuffix.Suffix)]
|
|
|
|
var err error
|
|
|
|
period, err = strconv.ParseFloat(numberString, 64)
|
|
|
|
if err != nil {
|
|
|
|
return time.Duration(0), err
|
|
|
|
}
|
|
|
|
period *= float64(ageSuffix.Multiplier)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return time.Duration(period), nil
|
|
|
|
}
|
|
|
|
|
2020-05-11 14:25:39 +02:00
|
|
|
// time formats to try parsing ages as - in order
|
|
|
|
var timeFormats = []string{
|
|
|
|
time.RFC3339,
|
|
|
|
"2006-01-02T15:04:05",
|
|
|
|
"2006-01-02 15:04:05",
|
|
|
|
"2006-01-02",
|
|
|
|
}
|
|
|
|
|
|
|
|
// parse the age as time before the epoch in various date formats
|
|
|
|
func parseDurationDates(age string, epoch time.Time) (t time.Duration, err error) {
|
|
|
|
var instant time.Time
|
|
|
|
for _, timeFormat := range timeFormats {
|
|
|
|
instant, err = time.Parse(timeFormat, age)
|
|
|
|
if err == nil {
|
|
|
|
return epoch.Sub(instant), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return t, err
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:50:16 +02:00
|
|
|
// parseDurationFromNow parses a duration string. Allows ParseDuration to match the time
|
|
|
|
// package and easier testing within the fs package.
|
|
|
|
func parseDurationFromNow(age string, getNow func() time.Time) (d time.Duration, err error) {
|
2020-05-11 14:25:39 +02:00
|
|
|
if age == "off" {
|
|
|
|
return time.Duration(DurationOff), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to parse as a time.Duration first
|
|
|
|
d, err = time.ParseDuration(age)
|
|
|
|
if err == nil {
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
d, err = parseDurationSuffixes(age)
|
|
|
|
if err == nil {
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:50:16 +02:00
|
|
|
d, err = parseDurationDates(age, getNow())
|
2020-05-11 14:25:39 +02:00
|
|
|
if err == nil {
|
|
|
|
return d, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return d, err
|
|
|
|
}
|
|
|
|
|
2020-09-07 01:50:16 +02:00
|
|
|
// ParseDuration parses a duration string. Accept ms|s|m|h|d|w|M|y suffixes. Defaults to second if not provided
|
|
|
|
func ParseDuration(age string) (time.Duration, error) {
|
|
|
|
return parseDurationFromNow(age, time.Now)
|
|
|
|
}
|
|
|
|
|
2019-07-01 13:09:19 +02:00
|
|
|
// ReadableString parses d into a human readable duration.
|
|
|
|
// Based on https://github.com/hako/durafmt
|
|
|
|
func (d Duration) ReadableString() string {
|
|
|
|
switch d {
|
|
|
|
case DurationOff:
|
|
|
|
return "off"
|
|
|
|
case 0:
|
|
|
|
return "0s"
|
|
|
|
}
|
|
|
|
|
|
|
|
readableString := ""
|
|
|
|
|
|
|
|
// Check for minus durations.
|
|
|
|
if d < 0 {
|
|
|
|
readableString += "-"
|
|
|
|
}
|
|
|
|
|
|
|
|
duration := time.Duration(math.Abs(float64(d)))
|
|
|
|
|
|
|
|
// Convert duration.
|
|
|
|
seconds := int64(duration.Seconds()) % 60
|
|
|
|
minutes := int64(duration.Minutes()) % 60
|
|
|
|
hours := int64(duration.Hours()) % 24
|
|
|
|
days := int64(duration/(24*time.Hour)) % 365 % 7
|
|
|
|
|
|
|
|
// Edge case between 364 and 365 days.
|
|
|
|
// We need to calculate weeks from what is left from years
|
|
|
|
leftYearDays := int64(duration/(24*time.Hour)) % 365
|
|
|
|
weeks := leftYearDays / 7
|
|
|
|
if leftYearDays >= 364 && leftYearDays < 365 {
|
|
|
|
weeks = 52
|
|
|
|
}
|
|
|
|
|
|
|
|
years := int64(duration/(24*time.Hour)) / 365
|
|
|
|
milliseconds := int64(duration/time.Millisecond) -
|
|
|
|
(seconds * 1000) - (minutes * 60000) - (hours * 3600000) -
|
|
|
|
(days * 86400000) - (weeks * 604800000) - (years * 31536000000)
|
|
|
|
|
|
|
|
// Create a map of the converted duration time.
|
|
|
|
durationMap := map[string]int64{
|
|
|
|
"ms": milliseconds,
|
|
|
|
"s": seconds,
|
|
|
|
"m": minutes,
|
|
|
|
"h": hours,
|
|
|
|
"d": days,
|
|
|
|
"w": weeks,
|
|
|
|
"y": years,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct duration string.
|
|
|
|
for _, u := range [...]string{"y", "w", "d", "h", "m", "s", "ms"} {
|
|
|
|
v := durationMap[u]
|
|
|
|
strval := strconv.FormatInt(v, 10)
|
|
|
|
if v == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
readableString += strval + u
|
|
|
|
}
|
|
|
|
|
|
|
|
return readableString
|
|
|
|
}
|
|
|
|
|
2018-01-12 17:30:54 +01:00
|
|
|
// Set a Duration
|
|
|
|
func (d *Duration) Set(s string) error {
|
|
|
|
duration, err := ParseDuration(s)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
*d = Duration(duration)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Type of the value
|
|
|
|
func (d Duration) Type() string {
|
2019-02-07 12:57:26 +01:00
|
|
|
return "Duration"
|
2018-01-12 17:30:54 +01:00
|
|
|
}
|
2018-05-14 17:32:27 +02:00
|
|
|
|
2020-12-11 18:48:09 +01:00
|
|
|
// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
|
|
|
|
func (d *Duration) UnmarshalJSON(in []byte) error {
|
|
|
|
return UnmarshalJSONFlag(in, d, func(i int64) error {
|
|
|
|
*d = Duration(i)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-05-14 17:32:27 +02:00
|
|
|
// Scan implements the fmt.Scanner interface
|
|
|
|
func (d *Duration) Scan(s fmt.ScanState, ch rune) error {
|
|
|
|
token, err := s.Token(true, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return d.Set(string(token))
|
|
|
|
}
|