azkustodata/value/timespan.go (206 lines of code) (raw):
package value
import (
"fmt"
"github.com/Azure/azure-kusto-go/azkustodata/types"
"reflect"
"strconv"
"strings"
"time"
)
const tick = 100 * time.Nanosecond
const day = 24 * time.Hour
// Timespan represents a Kusto timespan type. Timespan implements Kusto.
type Timespan struct {
pointerValue[time.Duration]
}
func NewTimespan(v time.Duration) *Timespan {
return &Timespan{newPointerValue[time.Duration](&v)}
}
func NewNullTimespan() *Timespan {
return &Timespan{newPointerValue[time.Duration](nil)}
}
func TimespanFromString(s string) (*Timespan, error) {
t := &Timespan{}
err := t.Unmarshal(s)
if err != nil {
return nil, err
}
return t, nil
}
// Marshal marshals the Timespan into a Kusto compatible string. The string is the constant invariant (c)
// format. See https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings#the-constant-c-format-specifier .
func (t *Timespan) Marshal() string {
if t == nil || t.value == nil || *t.value/tick == 0 {
return "00:00:00"
}
// val is used to track the duration value as we move our parts of our time into our string format.
// For example, after we write to our string the number of days that value had, we remove those days
// from the duration. We continue doing this until val only holds values < 10 millionth of a second (tick)
// as that is the lowest precision in our string representation.
val := *t.value
var sb strings.Builder
// Add a - sign if we have a negative value. Convert our value to positive for easier processing.
if val < 0 {
sb.WriteString("-")
val = -val
}
// Only include the day if the duration is 1+ days.
days := val / day
if days > 0 {
sb.WriteString(fmt.Sprintf("%d.", days))
}
hours := (val % day) / time.Hour
minutes := (val % time.Hour) / time.Minute
seconds := (val % time.Minute) / time.Second
ticks := (val % time.Second) / tick
sb.WriteString(fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds))
if ticks > 0 {
sb.WriteString(fmt.Sprintf(".%07d", ticks))
}
return sb.String()
}
// Unmarshal unmarshals i into Timespan. i must be a string representing a Values timespan or nil.
func (t *Timespan) Unmarshal(i interface{}) error {
const (
hoursIndex = 0
minutesIndex = 1
secondsIndex = 2
)
if i == nil {
t.value = nil
return nil
}
v, ok := i.(string)
if !ok {
return convertError(t, i)
}
negative := false
if len(v) > 1 {
if string(v[0]) == "-" {
negative = true
v = v[1:]
}
}
sp := strings.Split(v, ":")
if len(sp) != 3 {
return parseError(v, sp, fmt.Errorf("value to unmarshal into Timespan does not seem to fit format '00:00:00', where values are decimal(%s)", v))
}
var sum time.Duration
d, err := t.unmarshalDaysHours(sp[hoursIndex])
if err != nil {
return parseError(v, sp, err)
}
sum += d
d, err = t.unmarshalMinutes(sp[minutesIndex])
if err != nil {
return parseError(v, sp, err)
}
sum += d
d, err = t.unmarshalSeconds(sp[secondsIndex])
if err != nil {
return parseError(v, sp, err)
}
sum += d
if negative {
sum = sum * time.Duration(-1)
}
t.value = &sum
return nil
}
func (t *Timespan) unmarshalDaysHours(s string) (time.Duration, error) {
sp := strings.Split(s, ".")
switch len(sp) {
case 1:
hours, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("timespan's hours/day field was incorrect, was %s: %s", s, err)
}
return time.Duration(hours) * time.Hour, nil
case 2:
days, err := strconv.Atoi(sp[0])
if err != nil {
return 0, fmt.Errorf("timespan's hours/day field was incorrect, was %s", s)
}
hours, err := strconv.Atoi(sp[1])
if err != nil {
return 0, fmt.Errorf("timespan's hours/day field was incorrect, was %s", s)
}
return time.Duration(days)*day + time.Duration(hours)*time.Hour, nil
}
return 0, fmt.Errorf("timespan's hours/days field did not have the requisite '.'s, was %s", s)
}
func (t *Timespan) unmarshalMinutes(s string) (time.Duration, error) {
s = strings.Split(s, ".")[0] // We can have 01 or 01.00 or 59, but nothing comes behind the .
minutes, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("timespan's minutes field was incorrect, was %s", s)
}
if minutes < 0 || minutes > 59 {
return 0, fmt.Errorf("timespan's minutes field was incorrect, was %s", s)
}
return time.Duration(minutes) * time.Minute, nil
}
// unmarshalSeconds deals with this crazy output format. Instead of having some multiplier, the number
// of precision characters behind the decimal indicates your multiplier. This can be between 0 and 7, but
// really only has 3, 4 and 7. There is something called a tick, which is 100 Nanoseconds and the precision
// at len 4 is 100 * Microsecond (don't know if that has a name).
func (t *Timespan) unmarshalSeconds(s string) (time.Duration, error) {
// "03" = 3 * time.Second
// "00.099" = 99 * time.Millisecond
// "03.0123" == 3 * time.Second + 12300 * time.Microsecond
sp := strings.Split(s, ".")
switch len(sp) {
case 1:
seconds, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("timespan's seconds field was incorrect, was %s", s)
}
return time.Duration(seconds) * time.Second, nil
case 2:
seconds, err := strconv.Atoi(sp[0])
if err != nil {
return 0, fmt.Errorf("timespan's seconds field was incorrect, was %s", s)
}
n, err := strconv.Atoi(sp[1])
if err != nil {
return 0, fmt.Errorf("timespan's seconds field was incorrect, was %s", s)
}
var prec time.Duration
switch len(sp[1]) {
case 1:
prec = time.Duration(n) * (100 * time.Millisecond)
case 2:
prec = time.Duration(n) * (10 * time.Millisecond)
case 3:
prec = time.Duration(n) * time.Millisecond
case 4:
prec = time.Duration(n) * 100 * time.Microsecond
case 5:
prec = time.Duration(n) * 10 * time.Microsecond
case 6:
prec = time.Duration(n) * time.Microsecond
case 7:
prec = time.Duration(n) * tick
case 8:
prec = time.Duration(n) * (10 * time.Nanosecond)
case 9:
prec = time.Duration(n) * time.Nanosecond
default:
return 0, fmt.Errorf("timespan's seconds field did not have 1-9 numbers after the decimal, had %v", s)
}
return time.Duration(seconds)*time.Second + prec, nil
}
return 0, fmt.Errorf("timespan's seconds field did not have the requisite '.'s, was %s", s)
}
// Convert Timespan into reflect value.
func (t *Timespan) Convert(v reflect.Value) error {
pt := v.Type()
switch {
case pt.AssignableTo(reflect.TypeOf(time.Duration(0))):
if t.value != nil {
v.Set(reflect.ValueOf(*t.value))
}
return nil
case pt.ConvertibleTo(reflect.TypeOf(new(time.Duration))):
if t.value != nil {
pt := t.value
v.Set(reflect.ValueOf(pt))
}
return nil
case pt.ConvertibleTo(reflect.TypeOf(Timespan{})):
v.Set(reflect.ValueOf(*t))
return nil
case pt.ConvertibleTo(reflect.TypeOf(&Timespan{})):
v.Set(reflect.ValueOf(t))
return nil
}
return convertError(t, v)
}
func TimespanString(d time.Duration) string {
return NewTimespan(d).Marshal()
}
// GetType returns the type of the value.
func (t *Timespan) GetType() types.Column {
return types.Timespan
}