diff --git a/THIRD_PARTY_NOTICES b/THIRD_PARTY_NOTICES
index f78ec6b..29225ab 100644
--- a/THIRD_PARTY_NOTICES
+++ b/THIRD_PARTY_NOTICES
@@ -1,9 +1,24 @@
-### Third-Party Libraries
+## Third-Party Notices
-#### Library: scte35-go
+### scte35-go
- License: Apache License 2.0
- URL: https://github.com/Comcast/scte35-go/tree/main
- Copyright: None explicitly stated.
- Notes: This library does not include a copyright notice, but it is licensed under the Apache License 2.0.
This library is used in compliance with the Apache License, Version 2.0.
+
+### chrono
+- Copyright (c) 2020 Joe Mann
+- Licensed under the MIT License
+- Source: https://github.com/go-chrono/chrono/tree/master
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
diff --git a/go.mod b/go.mod
index 4068889..5d30ca7 100644
--- a/go.mod
+++ b/go.mod
@@ -8,5 +8,6 @@ require github.com/Comcast/scte35-go v1.4.6
require (
github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 // indirect
+ github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6 // indirect
golang.org/x/text v0.16.0 // indirect
)
diff --git a/go.sum b/go.sum
index 75a13bc..ebbdccd 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 h1:XNtOMwxmV2PI/vuTH
github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883/go.mod h1:9IjZnSQGh45J46HHS45pxuMJ6WFTtSXbaX0FoHDvxh8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6 h1:bZajBUDqyayXRqKAD/wX8AVPOeuFvwLAwTZFCvWnohs=
+github.com/go-chrono/chrono v0.0.0-20250124203826-0422557264a6/go.mod h1:uTWQdzrjtft2vWY+f+KQ9e3DXHsP0SzhE5SLIicFo08=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
diff --git a/mpd/duration.go b/mpd/duration.go
index 8ebc870..be973c7 100644
--- a/mpd/duration.go
+++ b/mpd/duration.go
@@ -5,202 +5,59 @@ package mpd
import (
"encoding/xml"
"errors"
- "fmt"
- "regexp"
- "strconv"
- "strings"
"time"
+
+ "github.com/go-chrono/chrono"
)
type Duration time.Duration
-var (
- rStart = "^P" // Must start with a 'P'
- rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years
- rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
- rHours = "(\\d+H)?" // Hours
- rMinutes = "(\\d+M)?" // Minutes
- rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal)
- rEnd = ")?$" // end of regex must close "T" capture group
-)
-
-var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd)
+var unsupportedFormatErr = errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]")
-func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
+func (d *Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
return xml.Attr{Name: name, Value: d.String()}, nil
}
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
- dur, err := ParseDuration(attr.Value)
+ duration, err := ParseDuration(attr.Value)
if err != nil {
return err
}
- *d = Duration(dur)
+ *d = Duration(duration)
return nil
}
-// String renders a Duration in XML Duration Data Type format
+// String parses the duration into a string with the use of the chrono library.
func (d *Duration) String() string {
- // Largest time is 2540400h10m10.000000000s
- var buf [32]byte
- w := len(buf)
-
- u := uint64(*d)
- neg := *d < 0
- if neg {
- u = -u
- }
-
- if u < uint64(time.Second) {
- // Special case: if duration is smaller than a second,
- // use smaller units, like 1.2ms
- var prec int
- w--
- buf[w] = 'S'
- w--
- if u == 0 {
- return "PT0S"
- }
- /*
- switch {
- case u < uint64(Millisecond):
- // print microseconds
- prec = 3
- // U+00B5 'µ' micro sign == 0xC2 0xB5
- w-- // Need room for two bytes.
- copy(buf[w:], "µ")
- default:
- // print milliseconds
- prec = 6
- buf[w] = 'm'
- }
- */
- w, u = fmtFrac(buf[:w], u, prec)
- w = fmtInt(buf[:w], u)
- } else {
- w--
- buf[w] = 'S'
-
- w, u = fmtFrac(buf[:w], u, 9)
-
- // u is now integer seconds
- w = fmtInt(buf[:w], u%60)
- u /= 60
-
- // u is now integer minutes
- if u > 0 {
- w--
- buf[w] = 'M'
- w = fmtInt(buf[:w], u%60)
- u /= 60
-
- // u is now integer hours
- // Stop at hours because days can be different lengths.
- if u > 0 {
- w--
- buf[w] = 'H'
- w = fmtInt(buf[:w], u)
- }
- }
- }
-
- if neg {
- w--
- buf[w] = '-'
- }
-
- return "PT" + string(buf[w:])
-}
-
-// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
-// tail of buf, omitting trailing zeros. it omits the decimal
-// point too when the fraction is 0. It returns the index where the
-// output bytes begin and the value v/10**prec.
-func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
- // Omit trailing zeros up to and including decimal point.
- w := len(buf)
- print := false
- for i := 0; i < prec; i++ {
- digit := v % 10
- print = print || digit != 0
- if print {
- w--
- buf[w] = byte(digit) + '0'
- }
- v /= 10
+ if d == nil {
+ return "PT0S"
}
- if print {
- w--
- buf[w] = '.'
- }
- return w, v
-}
-// fmtInt formats v into the tail of buf.
-// It returns the index where the output begins.
-func fmtInt(buf []byte, v uint64) int {
- w := len(buf)
- if v == 0 {
- w--
- buf[w] = '0'
- } else {
- for v > 0 {
- w--
- buf[w] = byte(v%10) + '0'
- v /= 10
- }
- }
- return w
+ return chrono.DurationOf(chrono.Extent(*d)).String()
}
+// ParseDuration converts the given string into a time.Duration with the use of
+// the chrono library. The function doesn't allow the use of negative durations,
+// decimal valued periods, or the use of the year, month, or week units as they
+// don't make sense.
func ParseDuration(str string) (time.Duration, error) {
- if len(str) < 3 {
- return 0, errors.New("At least one number and designator are required")
- }
-
- if strings.Contains(str, "-") {
- return 0, errors.New("Duration cannot be negative")
- }
-
- // Check that only the parts we expect exist and that everything's in the correct order
- if !xmlDurationRegex.Match([]byte(str)) {
- return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]")
- }
-
- var parts = xmlDurationRegex.FindStringSubmatch(str)
- var total time.Duration
-
- if parts[1] != "" {
- days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
- if err != nil {
- return 0, fmt.Errorf("Error parsing Days: %s", err)
- }
- total += time.Duration(days) * time.Hour * 24
+ period, duration, err := chrono.ParseDuration(str)
+ if err != nil {
+ return 0, unsupportedFormatErr
}
- if parts[2] != "" {
- hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
- if err != nil {
- return 0, fmt.Errorf("Error parsing Hours: %s", err)
- }
- total += time.Duration(hours) * time.Hour
+ hasDecimalDays := period.Days != float32(int64(period.Days))
+ hasUnsupportedUnits := period.Years+period.Months+period.Years > 0
+ if hasDecimalDays || hasUnsupportedUnits {
+ return 0, unsupportedFormatErr
}
- if parts[3] != "" {
- mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
- if err != nil {
- return 0, fmt.Errorf("Error parsing Minutes: %s", err)
- }
- total += time.Duration(mins) * time.Minute
- }
+ durationDays := chrono.Extent(period.Days) * 24 * chrono.Hour
+ totalDur := duration.Add(chrono.DurationOf(durationDays))
- if parts[4] != "" {
- secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
- if err != nil {
- return 0, fmt.Errorf("Error parsing Seconds: %s", err)
- }
- total += time.Duration(secs * float64(time.Second))
+ if totalDur.Compare(chrono.Duration{}) == -1 {
+ return 0, errors.New("duration cannot be negative")
}
- return total, nil
+ return time.Duration(totalDur.Nanoseconds()), nil
}
diff --git a/mpd/duration_test.go b/mpd/duration_test.go
index edd29b6..a7c931d 100644
--- a/mpd/duration_test.go
+++ b/mpd/duration_test.go
@@ -10,15 +10,19 @@ import (
func TestDuration(t *testing.T) {
in := map[string]string{
+ "0.5ms": "PT0.0005S",
+ "7ms": "PT0.007S",
"0s": "PT0S",
"6m16s": "PT6M16S",
"1.97s": "PT1.97S",
}
for ins, ex := range in {
- timeDur, err := time.ParseDuration(ins)
- require.NoError(t, err)
- dur := Duration(timeDur)
- require.EqualString(t, ex, dur.String())
+ t.Run(ins, func(t *testing.T) {
+ timeDur, err := time.ParseDuration(ins)
+ require.NoError(t, err)
+ dur := Duration(timeDur)
+ require.EqualString(t, ex, dur.String())
+ })
}
}
@@ -34,27 +38,31 @@ func TestParseDuration(t *testing.T) {
"PT20M": (20 * time.Minute).Seconds(),
"PT1M30.5S": (time.Minute + 30*time.Second + 500*time.Millisecond).Seconds(),
"PT1004199059S": (1004199059 * time.Second).Seconds(),
+ "PT2M1H": (time.Minute*2 + time.Hour).Seconds(),
}
for ins, ex := range in {
- act, err := ParseDuration(ins)
- require.NoError(t, err, ins)
- require.EqualFloat64(t, ex, act.Seconds(), ins)
+ t.Run(ins, func(t *testing.T) {
+ act, err := ParseDuration(ins)
+ require.NoError(t, err, ins)
+ require.EqualFloat64(t, ex, act.Seconds(), ins)
+ })
}
}
func TestParseBadDurations(t *testing.T) {
in := map[string]string{
- "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)
- "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)
- "P15.5D": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
- "P2H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
- "2DT1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
- "PT2M1H": `Duration must be in the format: P[nD][T[nH][nM][nS]]`, // Hours must appear before Minutes
- "P": `At least one number and designator are required`, // At least one number and designator are required
- "-P20H": `Duration cannot be negative`, // Negative duration doesn't make sense
+ "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)
+ "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)
+ "P15.5D": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // Only seconds can be expressed as a decimal
+ "P2H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "T" must be present to separate days and hours
+ "2DT1H": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // "P" must always be present
+ "P": `duration must be in the format: P[nD][T[nH][nM][nS]]`, // At least one number and designator are required
+ "-PT20H": `duration cannot be negative`, // Negative duration doesn't make sense
}
for ins, msg := range in {
- _, err := ParseDuration(ins)
- require.EqualError(t, err, msg, fmt.Sprintf("Expected an error for: %s", ins))
+ t.Run(ins, func(t *testing.T) {
+ _, err := ParseDuration(ins)
+ require.EqualError(t, err, msg, fmt.Sprintf("Expected an error for: %s", ins))
+ })
}
}
diff --git a/mpd/fixtures/newperiod.mpd b/mpd/fixtures/newperiod.mpd
index 6f6482d..8a934b1 100644
--- a/mpd/fixtures/newperiod.mpd
+++ b/mpd/fixtures/newperiod.mpd
@@ -8,7 +8,7 @@
-
+