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