1 package duration
2
3 import (
4 "errors"
5 "fmt"
6 "math"
7 "strconv"
8 "time"
9 "unicode"
10 )
11
12
13 type Duration struct {
14 Years float64
15 Months float64
16 Weeks float64
17 Days float64
18 Hours float64
19 Minutes float64
20 Seconds float64
21 Negative bool
22 }
23
24 const (
25 parsingPeriod = iota
26 parsingTime
27
28 hoursPerDay = 24
29 hoursPerWeek = hoursPerDay * 7
30 hoursPerMonth = hoursPerYear / 12
31 hoursPerYear = hoursPerDay * 365
32
33 nsPerSecond = 1000000000
34 nsPerMinute = nsPerSecond * 60
35 nsPerHour = nsPerMinute * 60
36 nsPerDay = nsPerHour * hoursPerDay
37 nsPerWeek = nsPerHour * hoursPerWeek
38 nsPerMonth = nsPerHour * hoursPerMonth
39 nsPerYear = nsPerHour * hoursPerYear
40 )
41
42 var (
43
44 ErrUnexpectedInput = errors.New("unexpected input")
45 )
46
47
48
49 func Parse(d string) (*Duration, error) {
50 state := parsingPeriod
51 duration := &Duration{}
52 num := ""
53 var err error
54
55 for _, char := range d {
56 switch char {
57 case '-':
58 duration.Negative = true
59 case 'P':
60 state = parsingPeriod
61 case 'T':
62 state = parsingTime
63 case 'Y':
64 if state != parsingPeriod {
65 return nil, ErrUnexpectedInput
66 }
67
68 duration.Years, err = strconv.ParseFloat(num, 64)
69 if err != nil {
70 return nil, err
71 }
72 num = ""
73 case 'M':
74 if state == parsingPeriod {
75 duration.Months, err = strconv.ParseFloat(num, 64)
76 if err != nil {
77 return nil, err
78 }
79 num = ""
80 } else if state == parsingTime {
81 duration.Minutes, err = strconv.ParseFloat(num, 64)
82 if err != nil {
83 return nil, err
84 }
85 num = ""
86 }
87 case 'W':
88 if state != parsingPeriod {
89 return nil, ErrUnexpectedInput
90 }
91
92 duration.Weeks, err = strconv.ParseFloat(num, 64)
93 if err != nil {
94 return nil, err
95 }
96 num = ""
97 case 'D':
98 if state != parsingPeriod {
99 return nil, ErrUnexpectedInput
100 }
101
102 duration.Days, err = strconv.ParseFloat(num, 64)
103 if err != nil {
104 return nil, err
105 }
106 num = ""
107 case 'H':
108 if state != parsingTime {
109 return nil, ErrUnexpectedInput
110 }
111
112 duration.Hours, err = strconv.ParseFloat(num, 64)
113 if err != nil {
114 return nil, err
115 }
116 num = ""
117 case 'S':
118 if state != parsingTime {
119 return nil, ErrUnexpectedInput
120 }
121
122 duration.Seconds, err = strconv.ParseFloat(num, 64)
123 if err != nil {
124 return nil, err
125 }
126 num = ""
127 default:
128 if unicode.IsNumber(char) || char == '.' {
129 num += string(char)
130 continue
131 }
132
133 return nil, ErrUnexpectedInput
134 }
135 }
136
137 return duration, nil
138 }
139
140
141
142
143 func FromTimeDuration(d time.Duration) *Duration {
144 duration := &Duration{}
145 if d == 0 {
146 return duration
147 }
148
149 if d < 0 {
150 d = -d
151 duration.Negative = true
152 }
153
154 if d.Hours() >= hoursPerYear {
155 duration.Years = math.Floor(d.Hours() / hoursPerYear)
156 d -= time.Duration(duration.Years) * nsPerYear
157 }
158 if d.Hours() >= hoursPerMonth {
159 duration.Months = math.Floor(d.Hours() / hoursPerMonth)
160 d -= time.Duration(duration.Months) * nsPerMonth
161 }
162 if d.Hours() >= hoursPerWeek {
163 duration.Weeks = math.Floor(d.Hours() / hoursPerWeek)
164 d -= time.Duration(duration.Weeks) * nsPerWeek
165 }
166 if d.Hours() >= hoursPerDay {
167 duration.Days = math.Floor(d.Hours() / hoursPerDay)
168 d -= time.Duration(duration.Days) * nsPerDay
169 }
170 if d.Hours() >= 1 {
171 duration.Hours = math.Floor(d.Hours())
172 d -= time.Duration(duration.Hours) * nsPerHour
173 }
174 if d.Minutes() >= 1 {
175 duration.Minutes = math.Floor(d.Minutes())
176 d -= time.Duration(duration.Minutes) * nsPerMinute
177 }
178 duration.Seconds = d.Seconds()
179
180 return duration
181 }
182
183
184
185
186
187 func Format(d time.Duration) string {
188 return FromTimeDuration(d).String()
189 }
190
191
192
193
194 func (duration *Duration) ToTimeDuration() time.Duration {
195 var timeDuration time.Duration
196
197
198 if duration.Years != 0 {
199 timeDuration += time.Duration(math.Round(duration.Years * nsPerYear))
200 }
201 if duration.Months != 0 {
202 timeDuration += time.Duration(math.Round(duration.Months * nsPerMonth))
203 }
204 if duration.Weeks != 0 {
205 timeDuration += time.Duration(math.Round(duration.Weeks * nsPerWeek))
206 }
207 if duration.Days != 0 {
208 timeDuration += time.Duration(math.Round(duration.Days * nsPerDay))
209 }
210 if duration.Hours != 0 {
211 timeDuration += time.Duration(math.Round(duration.Hours * nsPerHour))
212 }
213 if duration.Minutes != 0 {
214 timeDuration += time.Duration(math.Round(duration.Minutes * nsPerMinute))
215 }
216 if duration.Seconds != 0 {
217 timeDuration += time.Duration(math.Round(duration.Seconds * nsPerSecond))
218 }
219 if duration.Negative {
220 timeDuration = -timeDuration
221 }
222
223 return timeDuration
224 }
225
226
227 func (duration *Duration) String() string {
228 d := "P"
229 hasTime := false
230
231 appendD := func(designator string, value float64, isTime bool) {
232 if !hasTime && isTime {
233 d += "T"
234 hasTime = true
235 }
236
237 d += strconv.FormatFloat(value, 'f', -1, 64) + designator
238 }
239
240 if duration.Years != 0 {
241 appendD("Y", duration.Years, false)
242 }
243
244 if duration.Months != 0 {
245 appendD("M", duration.Months, false)
246 }
247
248 if duration.Weeks != 0 {
249 appendD("W", duration.Weeks, false)
250 }
251
252 if duration.Days != 0 {
253 appendD("D", duration.Days, false)
254 }
255
256 if duration.Hours != 0 {
257 appendD("H", duration.Hours, true)
258 }
259
260 if duration.Minutes != 0 {
261 appendD("M", duration.Minutes, true)
262 }
263
264 if duration.Seconds != 0 {
265 appendD("S", duration.Seconds, true)
266 }
267
268
269 if d == "P" {
270 d += "T0S"
271 }
272
273 if duration.Negative {
274 return "-" + d
275 }
276
277 return d
278 }
279
280 func (duration Duration) MarshalJSON() ([]byte, error) {
281 return []byte("\"" + duration.String() + "\""), nil
282 }
283
284 func (duration *Duration) UnmarshalJSON(source []byte) error {
285 strVal := string(source)
286 if len(strVal) < 2 {
287 return fmt.Errorf("invalid ISO 8601 duration: %s", strVal)
288 }
289 strVal = strVal[1 : len(strVal)-1]
290
291 if strVal == "null" {
292 return nil
293 }
294
295 parsed, err := Parse(strVal)
296 if err != nil {
297 return fmt.Errorf("invalid ISO 8601 duration: %s", strVal)
298 }
299 *duration = *parsed
300 return nil
301 }
302
View as plain text