Skip to content

Commit

Permalink
base time
Browse files Browse the repository at this point in the history
  • Loading branch information
RangelReale committed Aug 1, 2024
1 parent 2cd0053 commit 43698f9
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 2 deletions.
49 changes: 49 additions & 0 deletions value/base_time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package value

import (
"fmt"
"time"
)

// ValueBaseTime is a [debefix.ValueCalculator] calculate times based on a base time and a duration.
// In addition to [time.Duration], 3 more suffixes are supported: d (days), w (weeks), y (years).
type ValueBaseTime struct {
baseTime time.Time
typeName string
}

func NewValueBaseTime(baseTime time.Time, options ...ValueTimeOption) *ValueBaseTime {
ret := &ValueBaseTime{
baseTime: baseTime,
typeName: "basetime",
}
for _, opt := range options {
opt(ret)
}
return ret
}

func (v ValueBaseTime) CalculateValue(typ string, parameter string) (bool, any, error) {
if typ != v.typeName {
return false, nil, nil
}

if parameter == "" {
return true, v.baseTime, nil
}

duration, err := parseDuration(parameter)
if err != nil {
return false, nil, fmt.Errorf("error parsing duration '%s': %w", parameter, err)
}

return true, v.baseTime.Add(time.Duration(duration)), nil
}

type ValueTimeOption func(*ValueBaseTime)

func WithValueTimeTypeName(typeName string) ValueTimeOption {
return func(v *ValueBaseTime) {
v.typeName = typeName
}
}
85 changes: 85 additions & 0 deletions value/basetime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package value

import (
"testing"
"testing/fstest"
"time"

"github.com/rrgmc/debefix"
"gotest.tools/v3/assert"
)

func TestBaseTimeValueCalculated(t *testing.T) {
provider := debefix.NewFSFileProvider(fstest.MapFS{
"users.dbf.yaml": &fstest.MapFile{
Data: []byte(`tables:
tags:
rows:
- tag_id: 12
tag_name: "All"
created_at: !expr "calculated:basetime:1d5h"
updated_at: !expr "calculated:basetime:1d5h10m"
`),
},
})

data, err := debefix.Load(provider)
assert.NilError(t, err)

rowCount := map[string]int{}

baseTime := time.Date(2014, 3, 4, 10, 10, 12, 0, time.UTC)

resolved, err := debefix.Resolve(data, func(ctx debefix.ResolveContext, fields map[string]any) error {
rowCount[ctx.TableID()]++
return nil
}, debefix.WithResolvedValueCalculator(NewValueBaseTime(baseTime)))
assert.NilError(t, err)

assert.DeepEqual(t, map[string]int{
"tags": 1,
}, rowCount)

ctime, isTime := resolved.Tables["tags"].Rows[0].Fields["created_at"].(time.Time)
assert.Assert(t, isTime, "field value should be of time.Time value")

assert.Equal(t, 5, ctime.Day())
assert.Equal(t, 15, ctime.Hour())
}

func TestBaseTimeValueCalculatedEmptyParameter(t *testing.T) {
provider := debefix.NewFSFileProvider(fstest.MapFS{
"users.dbf.yaml": &fstest.MapFile{
Data: []byte(`tables:
tags:
rows:
- tag_id: 12
tag_name: "All"
created_at: !expr "calculated:basetime"
`),
},
})

data, err := debefix.Load(provider)
assert.NilError(t, err)

rowCount := map[string]int{}

baseTime := time.Date(2014, 3, 4, 10, 10, 12, 0, time.UTC)

resolved, err := debefix.Resolve(data, func(ctx debefix.ResolveContext, fields map[string]any) error {
rowCount[ctx.TableID()]++
return nil
}, debefix.WithResolvedValueCalculator(NewValueBaseTime(baseTime)))
assert.NilError(t, err)

assert.DeepEqual(t, map[string]int{
"tags": 1,
}, rowCount)

ctime, isTime := resolved.Tables["tags"].Rows[0].Fields["created_at"].(time.Time)
assert.Assert(t, isTime, "field value should be of time.Time value")

assert.Equal(t, 4, ctime.Day())
assert.Equal(t, 10, ctime.Hour())
}
213 changes: 213 additions & 0 deletions value/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package value

// source: https://github.com/wangyumu/duration/blob/main/duration.go

import (
"errors"
"time"
)

// duration is a standard unit of time.
type duration time.Duration

// Nanoseconds returns the duration as an integer nanosecond count.
func (d duration) Nanoseconds() int64 { return int64(d) }

// Seconds returns the duration as a floating point number of seconds.
func (d duration) Seconds() float64 {
sec := d / Second
nsec := d % Second
return float64(sec) + float64(nsec)*1e-9
}

// Hours returns the duration as a floating point number of hours.
func (d duration) Hours() float64 {
hour := d / Hour
nsec := d % Hour
return float64(hour) + float64(nsec)*(1e-9/60/60)
}

// Days returns the duration as a floating point number of days.
func (d duration) Days() float64 {
hour := d / Hour
nsec := d % Hour
return float64(hour) + float64(nsec)*(1e-9/60/60/24)
}

// Weeks returns the duration as a floating point number of days.
func (d duration) Weeks() float64 {
hour := d / Hour
nsec := d % Hour
return float64(hour) + float64(nsec)*(1e-9/60/60/24/7)
}

// Minutes returns the duration as a floating point number of minutes.
func (d duration) Minutes() float64 {
min := d / Minute
nsec := d % Minute
return float64(min) + float64(nsec)*(1e-9/60)
}

// Standard unit of time.
var (
Nanosecond = duration(time.Nanosecond)
Microsecond = duration(time.Microsecond)
Millisecond = duration(time.Millisecond)
Second = duration(time.Second)
Minute = duration(time.Minute)
Hour = duration(time.Hour)
Day = Hour * 24
Week = Day * 7
Fortnight = Week * 2
Month = Day * 30 // Approximation
Year = Day * 365 // Approximation
Decade = Year * 10 // Approximation
Century = Year * 100 // Approximation
Millennium = Year * 1000 // Approximation
)

var errLeadingInt = errors.New("duration: bad [0-9]*") // never printed

// leadingInt consumes the leading [0-9]* from s.
func leadingInt(s string) (x int64, rem string, err error) {
i := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if x > (1<<63-1)/10 {
// overflow
return 0, "", errLeadingInt
}
x = x*10 + int64(c) - '0'
if x < 0 {
// overflow
return 0, "", errLeadingInt
}
}
return x, s[i:], nil
}

var unitMap = map[string]int64{
"ns": int64(Nanosecond),
"us": int64(Microsecond),
"µs": int64(Microsecond), // U+00B5 = micro symbol
"μs": int64(Microsecond), // U+03BC = Greek letter mu
"ms": int64(Millisecond),
"s": int64(Second),
"m": int64(Minute),
"h": int64(Hour),
"d": int64(Day),
"w": int64(Week),
"y": int64(Year), // Approximation
}

// parseDuration parses a duration string.
// A duration string is a possibly signed sequence of
// decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w", "y".
func parseDuration(s string) (duration, error) {
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
orig := s
var d int64
neg := false

// Consume [-+]?
if s != "" {
c := s[0]
if c == '-' || c == '+' {
neg = c == '-'
s = s[1:]
}
}
// Special case: if all that is left is "0", this is zero.
if s == "0" {
return 0, nil
}
if s == "" {
return 0, errors.New("time: invalid duration " + orig)
}
for s != "" {
var (
v, f int64 // integers before, after decimal point
scale float64 = 1 // value = v + f/scale
)

var err error

// The next character must be [0-9.]
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
return 0, errors.New("time: invalid duration " + orig)
}
// Consume [0-9]*
pl := len(s)
v, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + orig)
}
pre := pl != len(s) // whether we consumed anything before a period

// Consume (\.[0-9]*)?
post := false
if s != "" && s[0] == '.' {
s = s[1:]
pl := len(s)
f, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + orig)
}
for n := pl - len(s); n > 0; n-- {
scale *= 10
}
post = pl != len(s)
}
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + orig)
}

// Consume unit.
i := 0
for ; i < len(s); i++ {
c := s[i]
if c == '.' || '0' <= c && c <= '9' {
break
}
}
if i == 0 {
return 0, errors.New("time: missing unit in duration " + orig)
}
u := s[:i]
s = s[i:]
unit, ok := unitMap[u]
if !ok {
return 0, errors.New("time: unknown unit " + u + " in duration " + orig)
}
if v > (1<<63-1)/unit {
// overflow
return 0, errors.New("time: invalid duration " + orig)
}
v *= unit
if f > 0 {
// float64 is needed to be nanosecond accurate for fractions of hours.
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
v += int64(float64(f) * (float64(unit) / scale))
if v < 0 {
// overflow
return 0, errors.New("time: invalid duration " + orig)
}
}
d += v
if d < 0 {
// overflow
return 0, errors.New("time: invalid duration " + orig)
}
}

if neg {
d = -d
}
return duration(d), nil
}
4 changes: 2 additions & 2 deletions value/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"github.com/google/uuid"
)

// ValueUUID is a [debefix.ValueParser] to parse "!uuid" tags to [uuid.UUID], and a [debefix.ResolvedValueParser] to
// to parse "uuid" type to [uuid.UUID].
// ValueUUID is a [debefix.ValueParser] to parse "!uuid" tags to [uuid.UUID], a [debefix.ResolvedValueParser] to
// to parse "uuid" type to [uuid.UUID], and [debefix.ValueCalculator] to generate new UUID values.
type ValueUUID struct{}

func (v ValueUUID) ParseValue(tag *ast.TagNode) (bool, any, error) {
Expand Down

0 comments on commit 43698f9

Please sign in to comment.