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 }