github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/parseduration.go (about)

     1  package fs
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  // Duration is a time.Duration with some more parsing options
    13  type Duration time.Duration
    14  
    15  // DurationOff is the default value for flags which can be turned off
    16  const DurationOff = Duration((1 << 63) - 1)
    17  
    18  // Turn Duration into a string
    19  func (d Duration) String() string {
    20  	if d == DurationOff {
    21  		return "off"
    22  	}
    23  	for i := len(ageSuffixes) - 2; i >= 0; i-- {
    24  		ageSuffix := &ageSuffixes[i]
    25  		if math.Abs(float64(d)) >= float64(ageSuffix.Multiplier) {
    26  			timeUnits := float64(d) / float64(ageSuffix.Multiplier)
    27  			return strconv.FormatFloat(timeUnits, 'f', -1, 64) + ageSuffix.Suffix
    28  		}
    29  	}
    30  	return time.Duration(d).String()
    31  }
    32  
    33  // IsSet returns if the duration is != DurationOff
    34  func (d Duration) IsSet() bool {
    35  	return d != DurationOff
    36  }
    37  
    38  // We use time conventions
    39  var ageSuffixes = []struct {
    40  	Suffix     string
    41  	Multiplier time.Duration
    42  }{
    43  	{Suffix: "d", Multiplier: time.Hour * 24},
    44  	{Suffix: "w", Multiplier: time.Hour * 24 * 7},
    45  	{Suffix: "M", Multiplier: time.Hour * 24 * 30},
    46  	{Suffix: "y", Multiplier: time.Hour * 24 * 365},
    47  
    48  	// Default to second
    49  	{Suffix: "", Multiplier: time.Second},
    50  }
    51  
    52  // parse the age as suffixed ages
    53  func parseDurationSuffixes(age string) (time.Duration, error) {
    54  	var period float64
    55  
    56  	for _, ageSuffix := range ageSuffixes {
    57  		if strings.HasSuffix(age, ageSuffix.Suffix) {
    58  			numberString := age[:len(age)-len(ageSuffix.Suffix)]
    59  			var err error
    60  			period, err = strconv.ParseFloat(numberString, 64)
    61  			if err != nil {
    62  				return time.Duration(0), err
    63  			}
    64  			period *= float64(ageSuffix.Multiplier)
    65  			break
    66  		}
    67  	}
    68  
    69  	return time.Duration(period), nil
    70  }
    71  
    72  // time formats to try parsing ages as - in order
    73  var timeFormats = []string{
    74  	time.RFC3339,
    75  	"2006-01-02T15:04:05",
    76  	"2006-01-02 15:04:05",
    77  	"2006-01-02",
    78  }
    79  
    80  // parse the date as time in various date formats
    81  func parseTimeDates(date string) (t time.Time, err error) {
    82  	var instant time.Time
    83  	for _, timeFormat := range timeFormats {
    84  		instant, err = time.ParseInLocation(timeFormat, date, time.Local)
    85  		if err == nil {
    86  			return instant, nil
    87  		}
    88  	}
    89  	return t, err
    90  }
    91  
    92  // parse the age as time before the epoch in various date formats
    93  func parseDurationDates(age string, epoch time.Time) (d time.Duration, err error) {
    94  	instant, err := parseTimeDates(age)
    95  	if err != nil {
    96  		return d, err
    97  	}
    98  
    99  	return epoch.Sub(instant), nil
   100  }
   101  
   102  // parseDurationFromNow parses a duration string. Allows ParseDuration to match the time
   103  // package and easier testing within the fs package.
   104  func parseDurationFromNow(age string, getNow func() time.Time) (d time.Duration, err error) {
   105  	if age == "off" {
   106  		return time.Duration(DurationOff), nil
   107  	}
   108  
   109  	// Attempt to parse as a time.Duration first
   110  	d, err = time.ParseDuration(age)
   111  	if err == nil {
   112  		return d, nil
   113  	}
   114  
   115  	d, err = parseDurationSuffixes(age)
   116  	if err == nil {
   117  		return d, nil
   118  	}
   119  
   120  	d, err = parseDurationDates(age, getNow())
   121  	if err == nil {
   122  		return d, nil
   123  	}
   124  
   125  	return d, err
   126  }
   127  
   128  // ParseDuration parses a duration string. Accept ms|s|m|h|d|w|M|y suffixes. Defaults to second if not provided
   129  func ParseDuration(age string) (time.Duration, error) {
   130  	return parseDurationFromNow(age, timeNowFunc)
   131  }
   132  
   133  // ReadableString parses d into a human-readable duration with units.
   134  // Examples: "3s", "1d2h23m20s", "292y24w3d23h47m16s".
   135  func (d Duration) ReadableString() string {
   136  	return d.readableString(0)
   137  }
   138  
   139  // ShortReadableString parses d into a human-readable duration with units.
   140  // This method returns it in short format, including the 3 most significant
   141  // units only, sacrificing precision if necessary. E.g. returns "292y24w3d"
   142  // instead of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s".
   143  func (d Duration) ShortReadableString() string {
   144  	return d.readableString(3)
   145  }
   146  
   147  // readableString parses d into a human-readable duration with units.
   148  // Parameter maxNumberOfUnits limits number of significant units to include,
   149  // sacrificing precision. E.g. with argument 3 it returns "292y24w3d" instead
   150  // of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s". Zero or
   151  // negative argument means include all.
   152  // Based on https://github.com/hako/durafmt
   153  func (d Duration) readableString(maxNumberOfUnits int) string {
   154  	switch d {
   155  	case DurationOff:
   156  		return "off"
   157  	case 0:
   158  		return "0s"
   159  	}
   160  
   161  	readableString := ""
   162  
   163  	// Check for minus durations.
   164  	if d < 0 {
   165  		readableString += "-"
   166  	}
   167  
   168  	duration := time.Duration(math.Abs(float64(d)))
   169  
   170  	// Convert duration.
   171  	seconds := int64(duration.Seconds()) % 60
   172  	minutes := int64(duration.Minutes()) % 60
   173  	hours := int64(duration.Hours()) % 24
   174  	days := int64(duration/(24*time.Hour)) % 365 % 7
   175  
   176  	// Edge case between 364 and 365 days.
   177  	// We need to calculate weeks from what is left from years
   178  	leftYearDays := int64(duration/(24*time.Hour)) % 365
   179  	weeks := leftYearDays / 7
   180  	if leftYearDays >= 364 && leftYearDays < 365 {
   181  		weeks = 52
   182  	}
   183  
   184  	years := int64(duration/(24*time.Hour)) / 365
   185  	milliseconds := int64(duration/time.Millisecond) -
   186  		(seconds * 1000) - (minutes * 60000) - (hours * 3600000) -
   187  		(days * 86400000) - (weeks * 604800000) - (years * 31536000000)
   188  
   189  	// Create a map of the converted duration time.
   190  	durationMap := map[string]int64{
   191  		"ms": milliseconds,
   192  		"s":  seconds,
   193  		"m":  minutes,
   194  		"h":  hours,
   195  		"d":  days,
   196  		"w":  weeks,
   197  		"y":  years,
   198  	}
   199  
   200  	// Construct duration string.
   201  	numberOfUnits := 0
   202  	for _, u := range [...]string{"y", "w", "d", "h", "m", "s", "ms"} {
   203  		v := durationMap[u]
   204  		strval := strconv.FormatInt(v, 10)
   205  		if v == 0 {
   206  			continue
   207  		}
   208  		readableString += strval + u
   209  		numberOfUnits++
   210  		if maxNumberOfUnits > 0 && numberOfUnits >= maxNumberOfUnits {
   211  			break
   212  		}
   213  	}
   214  
   215  	return readableString
   216  }
   217  
   218  // Set a Duration
   219  func (d *Duration) Set(s string) error {
   220  	duration, err := ParseDuration(s)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	*d = Duration(duration)
   225  	return nil
   226  }
   227  
   228  // Type of the value
   229  func (d Duration) Type() string {
   230  	return "Duration"
   231  }
   232  
   233  // UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
   234  func (d *Duration) UnmarshalJSON(in []byte) error {
   235  	// Check if the input is a string value.
   236  	if len(in) >= 2 && in[0] == '"' && in[len(in)-1] == '"' {
   237  		strVal := string(in[1 : len(in)-1]) // Remove the quotes
   238  
   239  		// Attempt to parse the string as a duration.
   240  		parsedDuration, err := ParseDuration(strVal)
   241  		if err != nil {
   242  			return err
   243  		}
   244  		*d = Duration(parsedDuration)
   245  		return nil
   246  	}
   247  	// Handle numeric values.
   248  	var i int64
   249  	err := json.Unmarshal(in, &i)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	*d = Duration(i)
   254  	return nil
   255  }
   256  
   257  // Scan implements the fmt.Scanner interface
   258  func (d *Duration) Scan(s fmt.ScanState, ch rune) error {
   259  	token, err := s.Token(true, func(rune) bool { return true })
   260  	if err != nil {
   261  		return err
   262  	}
   263  	return d.Set(string(token))
   264  }