1// Package durafmt formats time.Duration into a human readable format. 2package durafmt 3 4import ( 5 "errors" 6 "fmt" 7 "regexp" 8 "strconv" 9 "strings" 10 "time" 11) 12 13var ( 14 units, _ = DefaultUnitsCoder.Decode("year,week,day,hour,minute,second,millisecond,microsecond") 15 unitsShort = []string{"y", "w", "d", "h", "m", "s", "ms", "µs"} 16) 17 18// Durafmt holds the parsed duration and the original input duration. 19type Durafmt struct { 20 duration time.Duration 21 input string // Used as reference. 22 limitN int // Non-zero to limit only first N elements to output. 23 limitUnit string // Non-empty to limit max unit 24} 25 26// LimitToUnit sets the output format, you will not have unit bigger than the UNIT specified. UNIT = "" means no restriction. 27func (d *Durafmt) LimitToUnit(unit string) *Durafmt { 28 d.limitUnit = unit 29 return d 30} 31 32// LimitFirstN sets the output format, outputing only first N elements. n == 0 means no limit. 33func (d *Durafmt) LimitFirstN(n int) *Durafmt { 34 d.limitN = n 35 return d 36} 37 38func (d *Durafmt) Duration() time.Duration { 39 return d.duration 40} 41 42// Parse creates a new *Durafmt struct, returns error if input is invalid. 43func Parse(dinput time.Duration) *Durafmt { 44 input := dinput.String() 45 return &Durafmt{dinput, input, 0, ""} 46} 47 48// ParseShort creates a new *Durafmt struct, short form, returns error if input is invalid. 49// It's shortcut for `Parse(dur).LimitFirstN(1)` 50func ParseShort(dinput time.Duration) *Durafmt { 51 input := dinput.String() 52 return &Durafmt{dinput, input, 1, ""} 53} 54 55// ParseString creates a new *Durafmt struct from a string. 56// returns an error if input is invalid. 57func ParseString(input string) (*Durafmt, error) { 58 if input == "0" || input == "-0" { 59 return nil, errors.New("durafmt: missing unit in duration " + input) 60 } 61 duration, err := time.ParseDuration(input) 62 if err != nil { 63 return nil, err 64 } 65 return &Durafmt{duration, input, 0, ""}, nil 66} 67 68// ParseStringShort creates a new *Durafmt struct from a string, short form 69// returns an error if input is invalid. 70// It's shortcut for `ParseString(durStr)` and then calling `LimitFirstN(1)` 71func ParseStringShort(input string) (*Durafmt, error) { 72 if input == "0" || input == "-0" { 73 return nil, errors.New("durafmt: missing unit in duration " + input) 74 } 75 duration, err := time.ParseDuration(input) 76 if err != nil { 77 return nil, err 78 } 79 return &Durafmt{duration, input, 1, ""}, nil 80} 81 82// String parses d *Durafmt into a human readable duration with default units. 83func (d *Durafmt) String() string { 84 return d.Format(units) 85} 86 87// Format parses d *Durafmt into a human readable duration with units. 88func (d *Durafmt) Format(units Units) string { 89 var duration string 90 91 // Check for minus durations. 92 if string(d.input[0]) == "-" { 93 duration += "-" 94 d.duration = -d.duration 95 } 96 97 var microseconds int64 98 var milliseconds int64 99 var seconds int64 100 var minutes int64 101 var hours int64 102 var days int64 103 var weeks int64 104 var years int64 105 var shouldConvert = false 106 107 remainingSecondsToConvert := int64(d.duration / time.Microsecond) 108 109 // Convert duration. 110 if d.limitUnit == "" { 111 shouldConvert = true 112 } 113 114 if d.limitUnit == "years" || shouldConvert { 115 years = remainingSecondsToConvert / (365 * 24 * 3600 * 1000000) 116 remainingSecondsToConvert -= years * 365 * 24 * 3600 * 1000000 117 shouldConvert = true 118 } 119 120 if d.limitUnit == "weeks" || shouldConvert { 121 weeks = remainingSecondsToConvert / (7 * 24 * 3600 * 1000000) 122 remainingSecondsToConvert -= weeks * 7 * 24 * 3600 * 1000000 123 shouldConvert = true 124 } 125 126 if d.limitUnit == "days" || shouldConvert { 127 days = remainingSecondsToConvert / (24 * 3600 * 1000000) 128 remainingSecondsToConvert -= days * 24 * 3600 * 1000000 129 shouldConvert = true 130 } 131 132 if d.limitUnit == "hours" || shouldConvert { 133 hours = remainingSecondsToConvert / (3600 * 1000000) 134 remainingSecondsToConvert -= hours * 3600 * 1000000 135 shouldConvert = true 136 } 137 138 if d.limitUnit == "minutes" || shouldConvert { 139 minutes = remainingSecondsToConvert / (60 * 1000000) 140 remainingSecondsToConvert -= minutes * 60 * 1000000 141 shouldConvert = true 142 } 143 144 if d.limitUnit == "seconds" || shouldConvert { 145 seconds = remainingSecondsToConvert / 1000000 146 remainingSecondsToConvert -= seconds * 1000000 147 shouldConvert = true 148 } 149 150 if d.limitUnit == "milliseconds" || shouldConvert { 151 milliseconds = remainingSecondsToConvert / 1000 152 remainingSecondsToConvert -= milliseconds * 1000 153 } 154 155 microseconds = remainingSecondsToConvert 156 157 // Create a map of the converted duration time. 158 durationMap := []int64{ 159 microseconds, 160 milliseconds, 161 seconds, 162 minutes, 163 hours, 164 days, 165 weeks, 166 years, 167 } 168 169 // Construct duration string. 170 for i, u := range units.Units() { 171 v := durationMap[7-i] 172 strval := strconv.FormatInt(v, 10) 173 switch { 174 // add to the duration string if v > 1. 175 case v > 1: 176 duration += strval + " " + u.Plural + " " 177 // remove the plural 's', if v is 1. 178 case v == 1: 179 duration += strval + " " + u.Singular + " " 180 // omit any value with 0s or 0. 181 case d.duration.String() == "0" || d.duration.String() == "0s": 182 pattern := fmt.Sprintf("^-?0%s$", unitsShort[i]) 183 isMatch, err := regexp.MatchString(pattern, d.input) 184 if err != nil { 185 return "" 186 } 187 if isMatch { 188 duration += strval + " " + u.Plural 189 } 190 191 // omit any value with 0. 192 case v == 0: 193 continue 194 } 195 } 196 // trim any remaining spaces. 197 duration = strings.TrimSpace(duration) 198 199 // if more than 2 spaces present return the first 2 strings 200 // if short version is requested 201 if d.limitN > 0 { 202 parts := strings.Split(duration, " ") 203 if len(parts) > d.limitN*2 { 204 duration = strings.Join(parts[:d.limitN*2], " ") 205 } 206 } 207 208 return duration 209} 210 211func (d *Durafmt) InternationalString() string { 212 var duration string 213 214 // Check for minus durations. 215 if string(d.input[0]) == "-" { 216 duration += "-" 217 d.duration = -d.duration 218 } 219 220 var microseconds int64 221 var milliseconds int64 222 var seconds int64 223 var minutes int64 224 var hours int64 225 var days int64 226 var weeks int64 227 var years int64 228 var shouldConvert = false 229 230 remainingSecondsToConvert := int64(d.duration / time.Microsecond) 231 232 // Convert duration. 233 if d.limitUnit == "" { 234 shouldConvert = true 235 } 236 237 if d.limitUnit == "years" || shouldConvert { 238 years = remainingSecondsToConvert / (365 * 24 * 3600 * 1000000) 239 remainingSecondsToConvert -= years * 365 * 24 * 3600 * 1000000 240 shouldConvert = true 241 } 242 243 if d.limitUnit == "weeks" || shouldConvert { 244 weeks = remainingSecondsToConvert / (7 * 24 * 3600 * 1000000) 245 remainingSecondsToConvert -= weeks * 7 * 24 * 3600 * 1000000 246 shouldConvert = true 247 } 248 249 if d.limitUnit == "days" || shouldConvert { 250 days = remainingSecondsToConvert / (24 * 3600 * 1000000) 251 remainingSecondsToConvert -= days * 24 * 3600 * 1000000 252 shouldConvert = true 253 } 254 255 if d.limitUnit == "hours" || shouldConvert { 256 hours = remainingSecondsToConvert / (3600 * 1000000) 257 remainingSecondsToConvert -= hours * 3600 * 1000000 258 shouldConvert = true 259 } 260 261 if d.limitUnit == "minutes" || shouldConvert { 262 minutes = remainingSecondsToConvert / (60 * 1000000) 263 remainingSecondsToConvert -= minutes * 60 * 1000000 264 shouldConvert = true 265 } 266 267 if d.limitUnit == "seconds" || shouldConvert { 268 seconds = remainingSecondsToConvert / 1000000 269 remainingSecondsToConvert -= seconds * 1000000 270 shouldConvert = true 271 } 272 273 if d.limitUnit == "milliseconds" || shouldConvert { 274 milliseconds = remainingSecondsToConvert / 1000 275 remainingSecondsToConvert -= milliseconds * 1000 276 } 277 278 microseconds = remainingSecondsToConvert 279 280 // Create a map of the converted duration time. 281 durationMap := map[string]int64{ 282 "µs": microseconds, 283 "ms": milliseconds, 284 "s": seconds, 285 "m": minutes, 286 "h": hours, 287 "d": days, 288 "w": weeks, 289 "y": years, 290 } 291 292 // Construct duration string. 293 for i := range units.Units() { 294 u := unitsShort[i] 295 v := durationMap[u] 296 strval := strconv.FormatInt(v, 10) 297 switch { 298 // add to the duration string if v > 0. 299 case v > 0: 300 duration += strval + " " + u + " " 301 // omit any value with 0. 302 case d.duration.String() == "0": 303 pattern := fmt.Sprintf("^-?0%s$", unitsShort[i]) 304 isMatch, err := regexp.MatchString(pattern, d.input) 305 if err != nil { 306 return "" 307 } 308 if isMatch { 309 duration += strval + " " + u 310 } 311 312 // omit any value with 0. 313 case v == 0: 314 continue 315 } 316 } 317 // trim any remaining spaces. 318 duration = strings.TrimSpace(duration) 319 320 // if more than 2 spaces present return the first 2 strings 321 // if short version is requested 322 if d.limitN > 0 { 323 parts := strings.Split(duration, " ") 324 if len(parts) > d.limitN*2 { 325 duration = strings.Join(parts[:d.limitN*2], " ") 326 } 327 } 328 329 return duration 330} 331