|
1 |
| -// based on code from golang src/time/time.go |
2 |
| - |
3 | 1 | package mpd
|
4 | 2 |
|
5 | 3 | import (
|
6 | 4 | "encoding/xml"
|
7 | 5 | "errors"
|
| 6 | + "fmt" |
| 7 | + "regexp" |
| 8 | + "strconv" |
| 9 | + "strings" |
8 | 10 | "time"
|
9 |
| - |
10 |
| - "github.com/go-chrono/chrono" |
11 | 11 | )
|
12 | 12 |
|
| 13 | +// Duration is an extension of the original time.Duration. This type is used to |
| 14 | +// re-format the String() output to support the ISO 8601 duration standard. And |
| 15 | +// add the MarshalXMLAttr and UnmarshalXMLAttr functions. |
13 | 16 | type Duration time.Duration
|
14 | 17 |
|
15 |
| -var unsupportedFormatErr = errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]") |
| 18 | +var ( |
| 19 | + rStart = "^P" // Must start with a 'P' |
| 20 | + rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years |
| 21 | + rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T' |
| 22 | + rHours = "(\\d+H)?" // Hours |
| 23 | + rMinutes = "(\\d+M)?" // Minutes |
| 24 | + rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal) |
| 25 | + rEnd = ")?$" // end of regex must close "T" capture group |
| 26 | +) |
| 27 | + |
| 28 | +var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd) |
16 | 29 |
|
17 | 30 | func (d *Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
|
18 | 31 | return xml.Attr{Name: name, Value: d.String()}, nil
|
19 | 32 | }
|
20 | 33 |
|
21 | 34 | func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
|
22 |
| - duration, err := ParseDuration(attr.Value) |
| 35 | + dur, err := ParseDuration(attr.Value) |
23 | 36 | if err != nil {
|
24 | 37 | return err
|
25 | 38 | }
|
26 |
| - *d = Duration(duration) |
| 39 | + *d = Duration(dur) |
27 | 40 | return nil
|
28 | 41 | }
|
29 | 42 |
|
30 |
| -// String parses the duration into a string with the use of the chrono library. |
| 43 | +// String returns a string representing the duration in the form "PT72H3M0.5S". |
| 44 | +// Leading zero units are omitted. The zero duration formats as PT0S. |
| 45 | +// Based on src/time/time.go's time.Duration.String function. |
31 | 46 | func (d *Duration) String() string {
|
| 47 | + // This is inlinable to take advantage of "function outlining". |
| 48 | + // Thus, the caller can decide whether a string must be heap allocated. |
| 49 | + var arr [32]byte |
| 50 | + |
32 | 51 | if d == nil {
|
33 | 52 | return "PT0S"
|
34 | 53 | }
|
35 | 54 |
|
36 |
| - return chrono.DurationOf(chrono.Extent(*d)).String() |
| 55 | + n := d.format(&arr) |
| 56 | + return "PT" + string(arr[n:]) |
37 | 57 | }
|
38 | 58 |
|
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. |
43 |
| -func ParseDuration(str string) (time.Duration, error) { |
44 |
| - period, duration, err := chrono.ParseDuration(str) |
45 |
| - if err != nil { |
46 |
| - return 0, unsupportedFormatErr |
| 59 | +// format formats the representation of d into the end of buf and returns the |
| 60 | +// offset of the first character. This function is modified to use the iso 1801 |
| 61 | +// duration standard. This standard only uses the "H", "M", "S" characters. |
| 62 | +// // Based on src/time/time.go's time.Duration.Format function. |
| 63 | +func (d *Duration) format(buf *[32]byte) int { |
| 64 | + // Largest time is 2540400h10m10.000000000s |
| 65 | + w := len(buf) |
| 66 | + |
| 67 | + u := uint64(*d) |
| 68 | + neg := *d < 0 |
| 69 | + if neg { |
| 70 | + u = -u |
| 71 | + } |
| 72 | + |
| 73 | + w-- |
| 74 | + buf[w] = 'S' |
| 75 | + |
| 76 | + w, u = fmtFrac(buf[:w], u, 9) |
| 77 | + |
| 78 | + // u is now integer seconds |
| 79 | + w = fmtInt(buf[:w], u%60) |
| 80 | + u /= 60 |
| 81 | + |
| 82 | + // u is now integer minutes |
| 83 | + if u > 0 { |
| 84 | + w-- |
| 85 | + buf[w] = 'M' |
| 86 | + w = fmtInt(buf[:w], u%60) |
| 87 | + u /= 60 |
| 88 | + |
| 89 | + // u is now integer hours |
| 90 | + // Stop at hours because days can be different lengths. |
| 91 | + if u > 0 { |
| 92 | + w-- |
| 93 | + buf[w] = 'H' |
| 94 | + w = fmtInt(buf[:w], u) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + if neg { |
| 99 | + w-- |
| 100 | + buf[w] = '-' |
| 101 | + } |
| 102 | + |
| 103 | + return w |
| 104 | +} |
| 105 | + |
| 106 | +// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the |
| 107 | +// tail of buf, omitting trailing zeros. it omits the decimal |
| 108 | +// point too when the fraction is 0. It returns the index where the |
| 109 | +// output bytes begin and the value v/10**prec. |
| 110 | +// Copied from src/time/time.go. |
| 111 | +func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { |
| 112 | + // Omit trailing zeros up to and including decimal point. |
| 113 | + w := len(buf) |
| 114 | + print := false |
| 115 | + for i := 0; i < prec; i++ { |
| 116 | + digit := v % 10 |
| 117 | + print = print || digit != 0 |
| 118 | + if print { |
| 119 | + w-- |
| 120 | + buf[w] = byte(digit) + '0' |
| 121 | + } |
| 122 | + v /= 10 |
| 123 | + } |
| 124 | + if print { |
| 125 | + w-- |
| 126 | + buf[w] = '.' |
47 | 127 | }
|
| 128 | + return w, v |
| 129 | +} |
48 | 130 |
|
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 |
| 131 | +// fmtInt formats v into the tail of buf. |
| 132 | +// It returns the index where the output begins. |
| 133 | +// Copied from src/time/time.go. |
| 134 | +func fmtInt(buf []byte, v uint64) int { |
| 135 | + w := len(buf) |
| 136 | + if v == 0 { |
| 137 | + w-- |
| 138 | + buf[w] = '0' |
| 139 | + } else { |
| 140 | + for v > 0 { |
| 141 | + w-- |
| 142 | + buf[w] = byte(v%10) + '0' |
| 143 | + v /= 10 |
| 144 | + } |
53 | 145 | }
|
| 146 | + return w |
| 147 | +} |
54 | 148 |
|
55 |
| - durationDays := chrono.Extent(period.Days) * 24 * chrono.Hour |
56 |
| - totalDur := duration.Add(chrono.DurationOf(durationDays)) |
| 149 | +func ParseDuration(str string) (time.Duration, error) { |
| 150 | + if len(str) < 3 { |
| 151 | + return 0, errors.New("at least one number and designator are required") |
| 152 | + } |
57 | 153 |
|
58 |
| - if totalDur.Compare(chrono.Duration{}) == -1 { |
| 154 | + if strings.Contains(str, "-") { |
59 | 155 | return 0, errors.New("duration cannot be negative")
|
60 | 156 | }
|
61 | 157 |
|
62 |
| - return time.Duration(totalDur.Nanoseconds()), nil |
| 158 | + // Check that only the parts we expect exist and that everything's in the correct order |
| 159 | + if !xmlDurationRegex.Match([]byte(str)) { |
| 160 | + return 0, errors.New("duration must be in the format: P[nD][T[nH][nM][nS]]") |
| 161 | + } |
| 162 | + |
| 163 | + var parts = xmlDurationRegex.FindStringSubmatch(str) |
| 164 | + var total time.Duration |
| 165 | + |
| 166 | + if parts[1] != "" { |
| 167 | + days, err := strconv.Atoi(strings.TrimRight(parts[1], "D")) |
| 168 | + if err != nil { |
| 169 | + return 0, fmt.Errorf("error parsing Days: %s", err) |
| 170 | + } |
| 171 | + total += time.Duration(days) * time.Hour * 24 |
| 172 | + } |
| 173 | + |
| 174 | + if parts[2] != "" { |
| 175 | + hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H")) |
| 176 | + if err != nil { |
| 177 | + return 0, fmt.Errorf("error parsing Hours: %s", err) |
| 178 | + } |
| 179 | + total += time.Duration(hours) * time.Hour |
| 180 | + } |
| 181 | + |
| 182 | + if parts[3] != "" { |
| 183 | + mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M")) |
| 184 | + if err != nil { |
| 185 | + return 0, fmt.Errorf("error parsing Minutes: %s", err) |
| 186 | + } |
| 187 | + total += time.Duration(mins) * time.Minute |
| 188 | + } |
| 189 | + |
| 190 | + if parts[4] != "" { |
| 191 | + secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64) |
| 192 | + if err != nil { |
| 193 | + return 0, fmt.Errorf("error parsing Seconds: %s", err) |
| 194 | + } |
| 195 | + total += time.Duration(secs * float64(time.Second)) |
| 196 | + } |
| 197 | + |
| 198 | + return total, nil |
63 | 199 | }
|
0 commit comments