@@ -5,202 +5,59 @@ package mpd
55import (
66 "encoding/xml"
77 "errors"
8- "fmt"
9- "regexp"
10- "strconv"
11- "strings"
128 "time"
9+
10+ "github.com/go-chrono/chrono"
1311)
1412
1513type Duration time.Duration
1614
17- var (
18- rStart = "^P" // Must start with a 'P'
19- rDays = "(\\ d+D)?" // We only allow Days for durations, not Months or Years
20- rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
21- rHours = "(\\ d+H)?" // Hours
22- rMinutes = "(\\ d+M)?" // Minutes
23- rSeconds = "([\\ d.]+S)?" // Seconds (Potentially decimal)
24- rEnd = ")?$" // end of regex must close "T" capture group
25- )
26-
27- var xmlDurationRegex = regexp .MustCompile (rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd )
15+ var unsupportedFormatErr = errors .New ("duration must be in the format: P[nD][T[nH][nM][nS]]" )
2816
29- func (d Duration ) MarshalXMLAttr (name xml.Name ) (xml.Attr , error ) {
17+ func (d * Duration ) MarshalXMLAttr (name xml.Name ) (xml.Attr , error ) {
3018 return xml.Attr {Name : name , Value : d .String ()}, nil
3119}
3220
3321func (d * Duration ) UnmarshalXMLAttr (attr xml.Attr ) error {
34- dur , err := ParseDuration (attr .Value )
22+ duration , err := ParseDuration (attr .Value )
3523 if err != nil {
3624 return err
3725 }
38- * d = Duration (dur )
26+ * d = Duration (duration )
3927 return nil
4028}
4129
42- // String renders a Duration in XML Duration Data Type format
30+ // String parses the duration into a string with the use of the chrono library.
4331func (d * Duration ) String () string {
44- // Largest time is 2540400h10m10.000000000s
45- var buf [32 ]byte
46- w := len (buf )
47-
48- u := uint64 (* d )
49- neg := * d < 0
50- if neg {
51- u = - u
52- }
53-
54- if u < uint64 (time .Second ) {
55- // Special case: if duration is smaller than a second,
56- // use smaller units, like 1.2ms
57- var prec int
58- w --
59- buf [w ] = 'S'
60- w --
61- if u == 0 {
62- return "PT0S"
63- }
64- /*
65- switch {
66- case u < uint64(Millisecond):
67- // print microseconds
68- prec = 3
69- // U+00B5 'µ' micro sign == 0xC2 0xB5
70- w-- // Need room for two bytes.
71- copy(buf[w:], "µ")
72- default:
73- // print milliseconds
74- prec = 6
75- buf[w] = 'm'
76- }
77- */
78- w , u = fmtFrac (buf [:w ], u , prec )
79- w = fmtInt (buf [:w ], u )
80- } else {
81- w --
82- buf [w ] = 'S'
83-
84- w , u = fmtFrac (buf [:w ], u , 9 )
85-
86- // u is now integer seconds
87- w = fmtInt (buf [:w ], u % 60 )
88- u /= 60
89-
90- // u is now integer minutes
91- if u > 0 {
92- w --
93- buf [w ] = 'M'
94- w = fmtInt (buf [:w ], u % 60 )
95- u /= 60
96-
97- // u is now integer hours
98- // Stop at hours because days can be different lengths.
99- if u > 0 {
100- w --
101- buf [w ] = 'H'
102- w = fmtInt (buf [:w ], u )
103- }
104- }
105- }
106-
107- if neg {
108- w --
109- buf [w ] = '-'
110- }
111-
112- return "PT" + string (buf [w :])
113- }
114-
115- // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
116- // tail of buf, omitting trailing zeros. it omits the decimal
117- // point too when the fraction is 0. It returns the index where the
118- // output bytes begin and the value v/10**prec.
119- func fmtFrac (buf []byte , v uint64 , prec int ) (nw int , nv uint64 ) {
120- // Omit trailing zeros up to and including decimal point.
121- w := len (buf )
122- print := false
123- for i := 0 ; i < prec ; i ++ {
124- digit := v % 10
125- print = print || digit != 0
126- if print {
127- w --
128- buf [w ] = byte (digit ) + '0'
129- }
130- v /= 10
32+ if d == nil {
33+ return "PT0S"
13134 }
132- if print {
133- w --
134- buf [w ] = '.'
135- }
136- return w , v
137- }
13835
139- // fmtInt formats v into the tail of buf.
140- // It returns the index where the output begins.
141- func fmtInt (buf []byte , v uint64 ) int {
142- w := len (buf )
143- if v == 0 {
144- w --
145- buf [w ] = '0'
146- } else {
147- for v > 0 {
148- w --
149- buf [w ] = byte (v % 10 ) + '0'
150- v /= 10
151- }
152- }
153- return w
36+ return chrono .DurationOf (chrono .Extent (* d )).String ()
15437}
15538
39+ // ParseDuration converts the given string into a time.Duration with the use of
40+ // the chrono library. The function doesn't allow the use of negative durations,
41+ // decimal valued periods, or the use of the year, month, or week units as they
42+ // don't make sense.
15643func ParseDuration (str string ) (time.Duration , error ) {
157- if len (str ) < 3 {
158- return 0 , errors .New ("At least one number and designator are required" )
159- }
160-
161- if strings .Contains (str , "-" ) {
162- return 0 , errors .New ("Duration cannot be negative" )
163- }
164-
165- // Check that only the parts we expect exist and that everything's in the correct order
166- if ! xmlDurationRegex .Match ([]byte (str )) {
167- return 0 , errors .New ("Duration must be in the format: P[nD][T[nH][nM][nS]]" )
168- }
169-
170- var parts = xmlDurationRegex .FindStringSubmatch (str )
171- var total time.Duration
172-
173- if parts [1 ] != "" {
174- days , err := strconv .Atoi (strings .TrimRight (parts [1 ], "D" ))
175- if err != nil {
176- return 0 , fmt .Errorf ("Error parsing Days: %s" , err )
177- }
178- total += time .Duration (days ) * time .Hour * 24
44+ period , duration , err := chrono .ParseDuration (str )
45+ if err != nil {
46+ return 0 , unsupportedFormatErr
17947 }
18048
181- if parts [2 ] != "" {
182- hours , err := strconv .Atoi (strings .TrimRight (parts [2 ], "H" ))
183- if err != nil {
184- return 0 , fmt .Errorf ("Error parsing Hours: %s" , err )
185- }
186- total += time .Duration (hours ) * time .Hour
49+ hasDecimalDays := period .Days != float32 (int64 (period .Days ))
50+ hasUnsupportedUnits := period .Years + period .Months + period .Years > 0
51+ if hasDecimalDays || hasUnsupportedUnits {
52+ return 0 , unsupportedFormatErr
18753 }
18854
189- if parts [3 ] != "" {
190- mins , err := strconv .Atoi (strings .TrimRight (parts [3 ], "M" ))
191- if err != nil {
192- return 0 , fmt .Errorf ("Error parsing Minutes: %s" , err )
193- }
194- total += time .Duration (mins ) * time .Minute
195- }
55+ durationDays := chrono .Extent (period .Days ) * 24 * chrono .Hour
56+ totalDur := duration .Add (chrono .DurationOf (durationDays ))
19657
197- if parts [4 ] != "" {
198- secs , err := strconv .ParseFloat (strings .TrimRight (parts [4 ], "S" ), 64 )
199- if err != nil {
200- return 0 , fmt .Errorf ("Error parsing Seconds: %s" , err )
201- }
202- total += time .Duration (secs * float64 (time .Second ))
58+ if totalDur .Compare (chrono.Duration {}) == - 1 {
59+ return 0 , errors .New ("duration cannot be negative" )
20360 }
20461
205- return total , nil
62+ return time . Duration ( totalDur . Nanoseconds ()) , nil
20663}
0 commit comments