// IntervalRecurrence: "every Period after the start instant, in this
// IANA zone." Period carries intraday units (T7), so "every 90 minutes"
// works. Paired with IntervalIter, which yields successive fire
// instants by adding `every()` to the previous one.
class val IntervalRecurrence
"""
"Every Period after the start instant, in this IANA zone."
Period carries intraday units (T7), so "every 90 minutes" works.
"""
let _every: Period val
let _zone_name: String val
let _policy: OverflowPolicy
new val create(every': Period val, zone_name': String val, policy': OverflowPolicy) =>
_every = every'
_zone_name = zone_name'
_policy = policy'
fun val every(): Period val => _every
fun val zone_name(): String val => _zone_name
fun val policy(): OverflowPolicy => _policy
fun val iter_after(after: ZonedDateTime box): IntervalIter =>
"""
Build an iterator that yields successive fire instants. Each call
advances by `every()`. Pure-intraday Periods (months == 0 &&
days == 0) work today; calendar-mixed Periods produce a sticky
`NextFireError` until local-time arithmetic lands.
"""
IntervalIter(this, after)
class ref IntervalIter is Iterator[(ZonedDateTime iso^ | NextFireError)]
"""
Iterator over the fire instants of an `IntervalRecurrence`. Each
call to `next()` yields either a fresh `ZonedDateTime iso^` or a
`NextFireError`. The error is emitted exactly once, after which
`has_next()` returns `false`.
"""
let _period: Period val
let _zone_name: String val
var _state: _IntervalIterState
new ref create(r: IntervalRecurrence, after: ZonedDateTime box) =>
_period = r.every()
_zone_name = r.zone_name()
_state = _IntervalIterStart(after.to_posix())
fun ref has_next(): Bool =>
match _state
| _IntervalIterDone => false
else true
end
fun ref next(): (ZonedDateTime iso^ | NextFireError) =>
match _state
| let s: _IntervalIterStart => _emit(_begin(s.after_posix))
| let c: _IntervalIterCursor => _emit(_step(c.last_fire))
| let stuck: _IntervalIterStuck =>
let e = stuck.err
_state = _IntervalIterDone
e
| _IntervalIterDone => NextFireError
end
fun ref _emit(posix: ((I64, I64) | NextFireError))
: (ZonedDateTime iso^ | NextFireError)
=>
match posix
| (let sec: I64, let nsec: I64) =>
try
recover iso ZonedDateTime.from_posix_in_zone((sec, nsec), _zone_name)? end
else
_state = _IntervalIterDone
NextFireError
end
| let err: NextFireError =>
_state = _IntervalIterDone
err
end
fun ref _begin(after_posix: (I64, I64)): ((I64, I64) | NextFireError) =>
// Validate zone (even though pure-intraday math doesn't actually
// use it — consistency with the other iterators).
try
ZonedDateTime.from_posix_in_zone(after_posix, _zone_name)?
else
_state = _IntervalIterStuck(NextFireError)
return NextFireError
end
if (_period.months() != 0) or (_period.days() != 0) then
// TODO: calendar-mixed intervals need local-time arithmetic.
_state = _IntervalIterStuck(NextFireError)
return NextFireError
end
if _period.nanos() <= 0 then
_state = _IntervalIterStuck(NextFireError)
return NextFireError
end
_step(after_posix)
fun ref _step(from: (I64, I64)): ((I64, I64) | NextFireError) =>
"""
Add `_period.nanos()` to `from`. Validation in `_begin` already
confirmed the period is pure-intraday and positive.
"""
let total_nanos = _period.nanos()
let new_sec_raw = from._1 + (total_nanos / 1_000_000_000)
let new_nsec_raw = from._2 + (total_nanos % 1_000_000_000)
let carry = new_nsec_raw / 1_000_000_000
let new_sec = new_sec_raw + carry
let new_nsec = new_nsec_raw % 1_000_000_000
let fire = (new_sec, new_nsec)
_state = _IntervalIterCursor(fire)
fire
class val _IntervalIterStart
let after_posix: (I64, I64)
new val create(p: (I64, I64)) => after_posix = p
class val _IntervalIterCursor
let last_fire: (I64, I64)
new val create(p: (I64, I64)) => last_fire = p
class val _IntervalIterStuck
"""Internal: error queued, to be emitted on the next `next()` call."""
let err: NextFireError
new val create(e: NextFireError) => err = e
primitive _IntervalIterDone
"""Internal: error has been emitted; the iterator is exhausted."""
type _IntervalIterState is
( _IntervalIterStart
| _IntervalIterCursor
| _IntervalIterStuck
| _IntervalIterDone )