| package timeutil |
|
|
| import ( |
| "errors" |
| "fmt" |
| "time" |
|
|
| "github.com/openmeterio/openmeter/pkg/datetime" |
| ) |
|
|
| const MAX_SAFE_ITERATIONS = 1_000_000 |
|
|
| type Recurrence struct { |
| Interval RecurrenceInterval `json:"interval"` |
| |
| |
| Anchor time.Time `json:"anchor"` |
| } |
|
|
| func (r Recurrence) Validate() error { |
| var errs []error |
|
|
| if r.Interval.ISODuration.Sign() != 1 { |
| errs = append(errs, fmt.Errorf("recurrence interval must be positive")) |
| } |
|
|
| if r.Anchor.IsZero() { |
| errs = append(errs, fmt.Errorf("recurrence anchor must be set")) |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| |
| func (r Recurrence) GetPeriodAt(t time.Time) (ClosedPeriod, error) { |
| var def ClosedPeriod |
|
|
| next, err := r.IterateFromNextAfter(t, Exclusive) |
| if err != nil { |
| return def, err |
| } |
|
|
| |
| if next.At.Equal(t) { |
| start := next |
| end, err := start.Next() |
| if err != nil { |
| return def, err |
| } |
|
|
| return ClosedPeriod{start.At, end.At}, nil |
| } |
|
|
| |
| prev, err := r.IterateFromPrevBefore(t, Inclusive) |
| if err != nil { |
| return def, err |
| } |
|
|
| return ClosedPeriod{prev.At, next.At}, nil |
| } |
|
|
| |
| |
| |
| |
| func (r Recurrence) IterateFromNextAfter(t time.Time, boundaryBehavior Boundary) (RecurrenceIterator, error) { |
| if err := boundaryBehavior.Validate(); err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if t.IsZero() { |
| return RecurrenceIterator{}, fmt.Errorf("t cannot be zero") |
| } |
|
|
| inclusiveNextAfter, err := r.iterateFromNextAfterInclusive(t) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if boundaryBehavior == Exclusive && inclusiveNextAfter.At.Equal(t) { |
| return inclusiveNextAfter.Next() |
| } |
|
|
| return inclusiveNextAfter, nil |
| } |
|
|
| |
| |
| |
| |
| |
| func (r Recurrence) NextAfter(t time.Time, boundaryBehavior Boundary) (time.Time, error) { |
| iter, err := r.IterateFromNextAfter(t, boundaryBehavior) |
| if err != nil { |
| return time.Time{}, err |
| } |
|
|
| return iter.At, nil |
| } |
|
|
| func (r Recurrence) iterateFromNextAfterInclusive(t time.Time) (RecurrenceIterator, error) { |
| |
| if r.Anchor.After(t) { |
| res := r.Anchor |
| ic := 0 |
|
|
| |
| for res.After(t) { |
| |
| if ic <= -MAX_SAFE_ITERATIONS { |
| return RecurrenceIterator{}, fmt.Errorf("recurrence.NextAfter: too many iterations") |
| } |
| ic -= 1 |
|
|
| v, err := r.addIntervalNTimes(r.Anchor, ic) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if v.Before(t) { |
| break |
| } |
|
|
| res = v |
| } |
|
|
| return RecurrenceIterator{ |
| r: r, |
| iteration: ic, |
| At: res, |
| }, nil |
| } |
|
|
| |
| if r.Anchor.Before(t) { |
| res := r.Anchor |
| ic := 0 |
|
|
| |
| for res.Before(t) { |
| if ic >= MAX_SAFE_ITERATIONS { |
| return RecurrenceIterator{}, fmt.Errorf("recurrence.NextAfter: too many iterations") |
| } |
| ic += 1 |
|
|
| v, err := r.addIntervalNTimes(r.Anchor, ic) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| res = v |
| } |
|
|
| return RecurrenceIterator{ |
| r: r, |
| iteration: ic, |
| At: res, |
| }, nil |
| } |
|
|
| return r.Iterator(), nil |
| } |
|
|
| |
| |
| |
| |
| func (r Recurrence) IterateFromPrevBefore(t time.Time, boundaryBehavior Boundary) (RecurrenceIterator, error) { |
| if err := boundaryBehavior.Validate(); err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if t.IsZero() { |
| return RecurrenceIterator{}, fmt.Errorf("t cannot be zero") |
| } |
|
|
| inclusivePrevBefore, err := r.iterateFromPrevBeforeInclusive(t) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if boundaryBehavior == Exclusive && inclusivePrevBefore.At.Equal(t) { |
| return inclusivePrevBefore.Prev() |
| } |
|
|
| return inclusivePrevBefore, nil |
| } |
|
|
| |
| |
| |
| |
| |
| func (r Recurrence) PrevBefore(t time.Time, boundaryBehavior Boundary) (time.Time, error) { |
| iter, err := r.IterateFromPrevBefore(t, boundaryBehavior) |
| if err != nil { |
| return time.Time{}, err |
| } |
|
|
| return iter.At, nil |
| } |
|
|
| |
| |
| |
| func (r Recurrence) iterateFromPrevBeforeInclusive(t time.Time) (RecurrenceIterator, error) { |
| |
| if r.Anchor.After(t) { |
| res := r.Anchor |
| ic := 0 |
|
|
| for res.After(t) { |
| if ic <= -MAX_SAFE_ITERATIONS { |
| return RecurrenceIterator{}, fmt.Errorf("recurrence.PrevBefore: too many iterations") |
| } |
| ic -= 1 |
|
|
| v, err := r.addIntervalNTimes(r.Anchor, ic) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
| res = v |
| } |
|
|
| return RecurrenceIterator{ |
| r: r, |
| iteration: ic, |
| At: res, |
| }, nil |
| |
| } |
|
|
| if r.Anchor.Before(t) { |
| res := r.Anchor |
| ic := 0 |
|
|
| for res.Before(t) { |
| if ic >= MAX_SAFE_ITERATIONS { |
| return RecurrenceIterator{}, fmt.Errorf("recurrence.PrevBefore: too many iterations") |
| } |
| ic += 1 |
|
|
| v, err := r.addIntervalNTimes(r.Anchor, ic) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| if v.After(t) { |
| break |
| } |
|
|
| res = v |
| } |
|
|
| return RecurrenceIterator{ |
| r: r, |
| iteration: ic, |
| At: res, |
| }, nil |
| } |
|
|
| return r.Iterator(), nil |
| } |
|
|
| |
| func (r Recurrence) Iterator() RecurrenceIterator { |
| return RecurrenceIterator{ |
| r: r, |
| iteration: 0, |
| At: r.Anchor, |
| } |
| } |
|
|
| func (r Recurrence) addIntervalNTimes(t time.Time, nrIntervals int) (time.Time, error) { |
| interval, err := r.Interval.Mul(nrIntervals) |
| if err != nil { |
| return time.Time{}, err |
| } |
|
|
| n, ok := interval.AddTo(t) |
| if !ok { |
| return time.Time{}, fmt.Errorf("next recurrence calculation wasn't exact, likely a fractional duration: %v", r.Interval) |
| } |
| return n, nil |
| } |
|
|
| type RecurrenceIterator struct { |
| r Recurrence |
| iteration int |
| At time.Time |
| } |
|
|
| func (i RecurrenceIterator) Next() (RecurrenceIterator, error) { |
| return i.iteratorWithDelta(1) |
| } |
|
|
| func (i RecurrenceIterator) Prev() (RecurrenceIterator, error) { |
| return i.iteratorWithDelta(-1) |
| } |
|
|
| func (i RecurrenceIterator) iteratorWithDelta(delta int) (RecurrenceIterator, error) { |
| i.iteration += delta |
|
|
| res, err := i.r.addIntervalNTimes(i.r.Anchor, i.iteration) |
| if err != nil { |
| return RecurrenceIterator{}, err |
| } |
|
|
| i.At = res |
|
|
| return i, nil |
| } |
|
|
| type RecurrenceInterval struct { |
| datetime.ISODuration |
| } |
|
|
| var ( |
| RecurrencePeriodDaily RecurrenceInterval = RecurrenceInterval{datetime.DurationDay} |
| RecurrencePeriodWeek RecurrenceInterval = RecurrenceInterval{datetime.DurationWeek} |
| RecurrencePeriodMonth RecurrenceInterval = RecurrenceInterval{datetime.DurationMonth} |
| RecurrencePeriodYear RecurrenceInterval = RecurrenceInterval{datetime.DurationYear} |
| ) |
|
|
| func NewRecurrenceFromISODuration(p datetime.ISODuration, anchor time.Time) (Recurrence, error) { |
| return NewRecurrence(RecurrenceInterval{p}, anchor) |
| } |
|
|
| func NewRecurrence(p RecurrenceInterval, anchor time.Time) (Recurrence, error) { |
| rec := Recurrence{ |
| Interval: p, |
| Anchor: anchor, |
| } |
|
|
| if err := rec.Validate(); err != nil { |
| return Recurrence{}, err |
| } |
|
|
| return rec, nil |
| } |
|
|