1
0
Fork 0

Adding upstream version 0.0~git20220703.02e7343.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 22:43:24 +02:00
parent 5031d60c3f
commit e50fa89f15
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4 changed files with 492 additions and 0 deletions

21
LICENSE Normal file
View 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
View file

@ -0,0 +1,3 @@
module git.sr.ht/~mariusor/go-xsd-duration
go 1.14

244
xsd_duration.go Normal file
View 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
View 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)
}
})
}
}