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
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue