home/vendor/git.sr.ht/~mariusor/go-xsd-duration/xsd_duration.go

245 lines
5.8 KiB
Go

// 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
}