16039b350d
Before this fix, the parsing code gave an error like this parsing "2022-08-02 07:00:00" as fs.Time failed: expected newline This was due to the Scan call failing to read all the data. This patch fixes that, and redoes the tests
224 lines
5.3 KiB
Go
224 lines
5.3 KiB
Go
package fs
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Duration is a time.Duration with some more parsing options
|
|
type Duration time.Duration
|
|
|
|
// DurationOff is the default value for flags which can be turned off
|
|
const DurationOff = Duration((1 << 63) - 1)
|
|
|
|
// Turn Duration into a string
|
|
func (d Duration) String() string {
|
|
if d == DurationOff {
|
|
return "off"
|
|
}
|
|
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
|
|
}
|
|
}
|
|
return time.Duration(d).String()
|
|
}
|
|
|
|
// IsSet returns if the duration is != DurationOff
|
|
func (d Duration) IsSet() bool {
|
|
return d != DurationOff
|
|
}
|
|
|
|
// 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},
|
|
}
|
|
|
|
// parse the age as suffixed ages
|
|
func parseDurationSuffixes(age string) (time.Duration, error) {
|
|
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
|
|
}
|
|
|
|
// 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 date as time in various date formats
|
|
func parseTimeDates(date string) (t time.Time, err error) {
|
|
var instant time.Time
|
|
for _, timeFormat := range timeFormats {
|
|
instant, err = time.ParseInLocation(timeFormat, date, time.Local)
|
|
if err == nil {
|
|
return instant, nil
|
|
}
|
|
}
|
|
return t, err
|
|
}
|
|
|
|
// parse the age as time before the epoch in various date formats
|
|
func parseDurationDates(age string, epoch time.Time) (d time.Duration, err error) {
|
|
instant, err := parseTimeDates(age)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
|
|
return epoch.Sub(instant), nil
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
|
|
d, err = parseDurationDates(age, getNow())
|
|
if err == nil {
|
|
return d, nil
|
|
}
|
|
|
|
return d, err
|
|
}
|
|
|
|
// 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, timeNowFunc)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 {
|
|
return "Duration"
|
|
}
|
|
|
|
// 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
|
|
})
|
|
}
|
|
|
|
// Scan implements the fmt.Scanner interface
|
|
func (d *Duration) Scan(s fmt.ScanState, ch rune) error {
|
|
token, err := s.Token(true, func(rune) bool { return true })
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return d.Set(string(token))
|
|
}
|