@@ -5,59 +5,202 @@ package mpd
55import (
66 "encoding/xml"
77 "errors"
8+ "fmt"
9+ "regexp"
10+ "strconv"
11+ "strings"
812 "time"
9- 10- "github.com/go-chrono/chrono"
1113)
1214
1315type Duration time.Duration
1416
15- var unsupportedFormatErr = errors .New ("duration must be in the format: P[nD][T[nH][nM][nS]]" )
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 )
1628
17- func (d * Duration ) MarshalXMLAttr (name xml.Name ) (xml.Attr , error ) {
29+ func (d Duration ) MarshalXMLAttr (name xml.Name ) (xml.Attr , error ) {
1830 return xml.Attr {Name : name , Value : d .String ()}, nil
1931}
2032
2133func (d * Duration ) UnmarshalXMLAttr (attr xml.Attr ) error {
22- duration , err := ParseDuration (attr .Value )
34+ dur , err := ParseDuration (attr .Value )
2335 if err != nil {
2436 return err
2537 }
26- * d = Duration (duration )
38+ * d = Duration (dur )
2739 return nil
2840}
2941
30- // String parses the duration into a string with the use of the chrono library.
42+ // String renders a Duration in XML Duration Data Type format
3143func (d * Duration ) String () string {
32- if d == nil {
33- return "PT0S"
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
34131 }
132+ if print {
133+ w --
134+ buf [w ] = '.'
135+ }
136+ return w , v
137+ }
35138
36- return chrono .DurationOf (chrono .Extent (* d )).String ()
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
37154}
38155
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.
43156func ParseDuration (str string ) (time.Duration , error ) {
44- period , duration , err := chrono .ParseDuration (str )
45- if err != nil {
46- return 0 , unsupportedFormatErr
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]]" )
47168 }
48169
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
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
53179 }
54180
55- durationDays := chrono .Extent (period .Days ) * 24 * chrono .Hour
56- totalDur := duration .Add (chrono .DurationOf (durationDays ))
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
187+ }
188+ 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+ }
57196
58- if totalDur .Compare (chrono.Duration {}) == - 1 {
59- return 0 , errors .New ("duration cannot be negative" )
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 ))
60203 }
61204
62- return time . Duration ( totalDur . Nanoseconds ()) , nil
205+ return total , nil
63206}
0 commit comments