Skip to content

Commit 990116d

Browse files
committed
Merge branch 'master' into add-attributes
# Conflicts: # mpd/mpd_test.go Fixup tests.
2 parents acfe4eb + 57e15b1 commit 990116d

File tree

8 files changed

+375
-325
lines changed

8 files changed

+375
-325
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ make examples-ondemand
5454

5555
### Dependencies
5656

57-
Tested on go 1.9.1.
57+
Tested on go 1.9.2.
5858

5959
### Build and run unit tests
6060

mpd/duration.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,41 @@ package mpd
44

55
import (
66
"encoding/xml"
7+
"errors"
8+
"fmt"
9+
"regexp"
10+
"strconv"
11+
"strings"
712
"time"
813
)
914

1015
type Duration time.Duration
1116

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)
28+
1229
func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
1330
return xml.Attr{name, d.String()}, nil
1431
}
1532

33+
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
34+
dur, err := parseDuration(attr.Value)
35+
if err != nil {
36+
return err
37+
}
38+
*d = Duration(dur)
39+
return nil
40+
}
41+
1642
// String renders a Duration in XML Duration Data Type format
1743
func (d *Duration) String() string {
1844
// Largest time is 2540400h10m10.000000000s
@@ -126,3 +152,55 @@ func fmtInt(buf []byte, v uint64) int {
126152
}
127153
return w
128154
}
155+
156+
func 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
179+
}
180+
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+
}
196+
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))
203+
}
204+
205+
return total, nil
206+
}

mpd/duration_test.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55
"time"
66

7-
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
88
)
99

1010
func TestDuration(t *testing.T) {
@@ -15,8 +15,45 @@ func TestDuration(t *testing.T) {
1515
}
1616
for ins, ex := range in {
1717
timeDur, err := time.ParseDuration(ins)
18-
assert.Equal(t, nil, err)
18+
require.Equal(t, nil, err)
1919
dur := Duration(timeDur)
20-
assert.Equal(t, ex, dur.String())
20+
require.Equal(t, ex, dur.String())
21+
}
22+
}
23+
24+
func TestParseDuration(t *testing.T) {
25+
in := map[string]float64{
26+
"PT0S": 0,
27+
"PT1M": 60,
28+
"PT2H": 7200,
29+
"PT6M16S": 376,
30+
"PT1.97S": 1.97,
31+
"PT1H2M3.456S": 3723.456,
32+
"P1DT2H": (26 * time.Hour).Seconds(),
33+
"PT20M": (20 * time.Minute).Seconds(),
34+
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
35+
"PT1004199059S": (1004199059 * time.Second).Seconds(),
36+
}
37+
for ins, ex := range in {
38+
act, err := parseDuration(ins)
39+
require.NoError(t, err, ins)
40+
require.Equal(t, ex, act.Seconds(), ins)
41+
}
42+
}
43+
44+
func TestParseBadDurations(t *testing.T) {
45+
in := map[string]string{
46+
"P20M": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Months (doesn't make sense when converting to duration)
47+
"P20Y": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // We don't allow Years (doesn't make sense when converting to duration)
48+
"P15.5D": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
49+
"P2H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
50+
"2DT1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
51+
"PT2M1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes
52+
"P": `At least one number and designator are required`, // At least one number and designator are required
53+
"-P20H": `Duration cannot be negative`, // Negative duration doesn't make sense
54+
}
55+
for ins, msg := range in {
56+
_, err := parseDuration(ins)
57+
require.EqualError(t, err, msg, "Expected an error for: %s", ins)
2158
}
2259
}

0 commit comments

Comments
 (0)