Adding upstream version 0.0~git20220703.02e7343.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
5031d60c3f
commit
e50fa89f15
4 changed files with 492 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Go xsd:duration
|
||||
|
||||
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.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module git.sr.ht/~mariusor/go-xsd-duration
|
||||
|
||||
go 1.14
|
244
xsd_duration.go
Normal file
244
xsd_duration.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
// Handles xsd:duration to time.Duration conversion
|
||||
|
||||
package xsd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Extending time constants with the values for day, week, month, year
|
||||
const (
|
||||
Day = time.Hour * 24
|
||||
// These values are not precise, probably why the time package does not implement them
|
||||
// We need them here because xsd:duration uses them
|
||||
Monthish = Day * 30
|
||||
Yearish = Day * 356
|
||||
)
|
||||
|
||||
// durationPair holds information about a pair of (uint/ufloat)(PeriodTag) values in a xsd:duration string
|
||||
type durationPair struct {
|
||||
v time.Duration
|
||||
typ byte
|
||||
}
|
||||
|
||||
func getTimeBaseDuration(b byte) time.Duration {
|
||||
switch b {
|
||||
case tagHour:
|
||||
return time.Hour
|
||||
case tagMinute:
|
||||
return time.Minute
|
||||
case tagSecond:
|
||||
return time.Second
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getDateBaseDuration(b byte) time.Duration {
|
||||
switch b {
|
||||
case tagYear:
|
||||
return Yearish
|
||||
case tagMonth:
|
||||
return Monthish
|
||||
case tagDay:
|
||||
return Day
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// validByteForFloats checks
|
||||
func validByteForFloats(b byte) bool {
|
||||
// + , - . 0 1 2 3 4 5 6 7 8 9
|
||||
return (b >= 43 && b <= 46) || (b >= 48 && b <= 57)
|
||||
}
|
||||
|
||||
func parseTagWithValue(data []byte, start, tagPos int, isTime bool) (*durationPair, error) {
|
||||
d := new(durationPair)
|
||||
d.typ = data[tagPos]
|
||||
if d.typ == tagSecond {
|
||||
// seconds can be represented in float, we need to parse accordingly
|
||||
if v, err := strconv.ParseFloat(string(data[start:tagPos]), 32); err == nil {
|
||||
d.v = time.Duration(float64(time.Second) * v)
|
||||
}
|
||||
} else {
|
||||
if v, err := strconv.ParseInt(string(data[start:tagPos]), 10, 32); err == nil {
|
||||
if isTime {
|
||||
d.v = getTimeBaseDuration(d.typ) * time.Duration(v)
|
||||
} else {
|
||||
d.v = getDateBaseDuration(d.typ) * time.Duration(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.v < 0 {
|
||||
return nil, fmt.Errorf("the minus sign must appear first in the duration")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// loadUintVal receives the data and position at which to try to load the duration element
|
||||
// isTime is used for distinguishing between P1M - 1 month, and PT1M - 1 minute
|
||||
// it returns a duration pair if it can read one at the start of data,
|
||||
// and the number of bytes read from data
|
||||
func loadUintVal(data []byte, start int, isTime bool) (*durationPair, int, error) {
|
||||
for i := start; i < len(data); i++ {
|
||||
chr := data[i]
|
||||
if validTag(data[i]) {
|
||||
d, err := parseTagWithValue(data, start, i, isTime)
|
||||
return d, i, err
|
||||
}
|
||||
if validByteForFloats(chr) {
|
||||
continue
|
||||
}
|
||||
return nil, i, fmt.Errorf("invalid character %c at pos %d", chr, i)
|
||||
}
|
||||
return nil, len(data), fmt.Errorf("unable to recognize any duration value")
|
||||
}
|
||||
|
||||
const (
|
||||
tagDuration = 'P'
|
||||
tagTime = 'T'
|
||||
tagYear = 'Y'
|
||||
tagMonth = 'M'
|
||||
tagDay = 'D'
|
||||
tagHour = 'H'
|
||||
tagMinute = 'M'
|
||||
tagSecond = 'S'
|
||||
)
|
||||
|
||||
func validTag(b byte) bool {
|
||||
return b == tagYear || b == tagMonth || b == tagDay || b == tagHour || b == tagMinute || b == tagSecond
|
||||
}
|
||||
|
||||
// Unmarshal takes a byte array and unmarshals it to a time.Duration value
|
||||
// It is used to parse values in the following format: -PuYuMuDTuHuMufS, where:
|
||||
// * - shows if the duration is negative
|
||||
// * P is the duration tag
|
||||
// * T is the time tag separator
|
||||
// * Y,M,D,H,M,S are tags for year, month, day, hour, minute, second values
|
||||
// * u is an unsigned integer value
|
||||
// * uf is an unsigned float value (just for seconds)
|
||||
func Unmarshal(data []byte, d *time.Duration) error {
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("invalid xsd:duration: empty value")
|
||||
}
|
||||
pos := 0
|
||||
// loading if the value is negative
|
||||
negative := data[pos] == '-'
|
||||
if negative {
|
||||
// skipping over the minus
|
||||
pos++
|
||||
}
|
||||
if data[pos] != tagDuration {
|
||||
return fmt.Errorf("invalid xsd:duration: first character must be %q", 'P')
|
||||
}
|
||||
// skipping over the "P"
|
||||
pos++
|
||||
|
||||
onePastEnd := len(data)
|
||||
if pos >= onePastEnd {
|
||||
return fmt.Errorf("invalid xsd:duration: at least one number and designator are required")
|
||||
}
|
||||
isTime := false
|
||||
duration := time.Duration(0)
|
||||
for {
|
||||
if data[pos] == tagTime {
|
||||
pos++
|
||||
isTime = true
|
||||
}
|
||||
p, cnt, err := loadUintVal(data, pos, isTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid xsd:duration: %w", err)
|
||||
}
|
||||
duration += p.v
|
||||
pos = cnt + 1
|
||||
if pos+1 >= onePastEnd {
|
||||
break
|
||||
}
|
||||
}
|
||||
if negative {
|
||||
duration *= -1
|
||||
}
|
||||
if onePastEnd > pos+1 {
|
||||
return fmt.Errorf("data contains more bytes than we are able to parse")
|
||||
}
|
||||
if d == nil {
|
||||
return fmt.Errorf("unable to store time.Duration to nil pointer")
|
||||
}
|
||||
*d = duration
|
||||
return nil
|
||||
}
|
||||
|
||||
func Days(d time.Duration) float64 {
|
||||
dd := d / Day
|
||||
h := d % Day
|
||||
return float64(dd) + float64(h)/(24*60*60*1e9)
|
||||
}
|
||||
|
||||
func Months(d time.Duration) float64 {
|
||||
m := d / Monthish
|
||||
w := d % Monthish
|
||||
return float64(m) + float64(w)/(4*7*24*60*60*1e9)
|
||||
}
|
||||
|
||||
func Years(d time.Duration) float64 {
|
||||
y := d / Yearish
|
||||
m := d % Yearish
|
||||
return float64(y) + float64(m)/(12*4*7*24*60*60*1e9)
|
||||
}
|
||||
|
||||
func Marshal(d time.Duration) ([]byte, error) {
|
||||
if d == 0 {
|
||||
return []byte{tagDuration, tagTime, '0', tagSecond}, nil
|
||||
}
|
||||
|
||||
neg := d < 0
|
||||
if neg {
|
||||
d = -d
|
||||
}
|
||||
y := Years(d)
|
||||
d -= time.Duration(y) * Yearish
|
||||
m := Months(d)
|
||||
d -= time.Duration(m) * Monthish
|
||||
dd := Days(d)
|
||||
d -= time.Duration(dd) * Day
|
||||
H := d.Hours()
|
||||
d -= time.Duration(H) * time.Hour
|
||||
M := d.Minutes()
|
||||
d -= time.Duration(M) * time.Minute
|
||||
s := d.Seconds()
|
||||
d -= time.Duration(s) * time.Second
|
||||
b := bytes.Buffer{}
|
||||
if neg {
|
||||
b.Write([]byte{'-'})
|
||||
}
|
||||
b.Write([]byte{'P'})
|
||||
if int(y) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int64(y), tagYear))
|
||||
}
|
||||
if int(m) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int64(m), tagMonth))
|
||||
}
|
||||
if int(dd) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int64(dd), tagDay))
|
||||
}
|
||||
|
||||
if H+M+s > 0 {
|
||||
b.Write([]byte{tagTime})
|
||||
if int(H) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int64(H), tagHour))
|
||||
}
|
||||
if int(M) > 0 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int64(M), tagMinute))
|
||||
}
|
||||
if int(s) > 0 {
|
||||
if s-float64(int(s)) < 0.01 {
|
||||
b.WriteString(fmt.Sprintf("%d%c", int(s), tagSecond))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("%.1f%c", s, tagSecond))
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
224
xsd_duration_test.go
Normal file
224
xsd_duration_test.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
package xsd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
P2Y6M5DT12H35M30S = 2*Yearish + 6*Monthish + 5*Day + 12*time.Hour + 35*time.Minute + 30*time.Second
|
||||
P1DT2H = Day + 2*time.Hour
|
||||
P20M = 20 * Monthish
|
||||
PT20M = 20 * time.Minute
|
||||
P0Y = time.Duration(0)
|
||||
NegP60D = -1 * (60 * Day)
|
||||
PT1M30_5S = time.Minute + time.Duration(30.5*float64(time.Second))
|
||||
)
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
type args struct {
|
||||
d time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "2 years, 6 months, 5 days, 12 hours, 35 minutes, 30 seconds",
|
||||
args: args{P2Y6M5DT12H35M30S},
|
||||
wantErr: false,
|
||||
want: []byte("P2Y6M5DT12H35M30S"),
|
||||
},
|
||||
{
|
||||
name: "1 day, 2 hours",
|
||||
args: args{P1DT2H},
|
||||
wantErr: false,
|
||||
want: []byte("P1DT2H"),
|
||||
},
|
||||
{
|
||||
name: "20 months (the number of months can be more than 12)",
|
||||
args: args{P20M},
|
||||
want: []byte("P1Y8M4D"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "20 minutes",
|
||||
args: args{PT20M},
|
||||
want: []byte("PT20M"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "0 years",
|
||||
args: args{P0Y},
|
||||
want: []byte("PT0S"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "minus 60 days",
|
||||
args: args{NegP60D},
|
||||
want: []byte("-P2M"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "1 minute, 30.5 seconds",
|
||||
args: args{PT1M30_5S},
|
||||
want: []byte("PT1M30.5S"),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Marshal(tt.args.d)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Marshal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("Marshal() got = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
d *time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "an empty value should not be valid",
|
||||
args: args{[]byte{}, nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "at least one number and designator are required",
|
||||
args: args{[]byte("P"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing type",
|
||||
args: args{[]byte("PT"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minus sign must appear first",
|
||||
args: args{[]byte("P-20M"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no time items are present, so \"T\" must not be present",
|
||||
args: args{[]byte("P20MT"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no value is specified for months, so \"M\" must not be present",
|
||||
args: args{[]byte("P1YM5D"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "only the seconds can be expressed as a decimal",
|
||||
args: args{[]byte("P15.5Y"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "\"T\" must be present to separate days and hours",
|
||||
args: args{[]byte("P1D2H"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "\"P\" must always be present",
|
||||
args: args{[]byte("1Y2M"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "years must appear before months",
|
||||
args: args{[]byte("P2M1Y"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "at least one digit must follow the decimal point if it appears",
|
||||
args: args{[]byte("PT15.S"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid data at the end",
|
||||
args: args{[]byte("P2Y6M5DT12H35M30Stest"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid data at the start",
|
||||
args: args{[]byte("testP2Y6M5DT12H35M30S"), nil},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "2 years, 6 months, 5 days, 12 hours, 35 minutes, 30 seconds",
|
||||
args: args{[]byte("P2Y6M5DT12H35M30S"), new(time.Duration)},
|
||||
want: P2Y6M5DT12H35M30S,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "1 day, 2 hours",
|
||||
args: args{[]byte("P1DT2H"), new(time.Duration)},
|
||||
want: P1DT2H,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "20 months (the number of months can be more than 12)",
|
||||
args: args{[]byte("P20M"), new(time.Duration)},
|
||||
want: P20M,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "20 minutes",
|
||||
args: args{[]byte("PT20M"), new(time.Duration)},
|
||||
want: PT20M,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "20 months (0 is permitted as a number, but is not required)",
|
||||
args: args{[]byte("P0Y20M0D"), new(time.Duration)},
|
||||
want: P20M,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "0 years",
|
||||
args: args{[]byte("P0Y"), new(time.Duration)},
|
||||
want: P0Y,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "minus 60 days",
|
||||
args: args{[]byte("-P60D"), new(time.Duration)},
|
||||
want: NegP60D,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "1 minute, 30.5 seconds",
|
||||
args: args{[]byte("PT1M30.5S"), new(time.Duration)},
|
||||
want: PT1M30_5S,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Unmarshal(tt.args.data, tt.args.d); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.want == 0 {
|
||||
return
|
||||
}
|
||||
if *tt.args.d != tt.want {
|
||||
t.Errorf("Marshal() got = %s, want %s", tt.args.d, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue