// WeekdayRecurrence: "every weekday-in-set at TimeOfDay, in this IANA
// zone". Paired with WeekdayIter, which yields successive fire instants.
class val WeekdayRecurrence
"""
"Every weekday-in-set at TimeOfDay, in this IANA zone."
"""
let _weekdays: Array[DayOfWeek] val
let _at: TimeOfDay val
let _zone_name: String val
new val create(
weekdays': Array[DayOfWeek] val,
at': TimeOfDay val,
zone_name': String val)
=>
_weekdays = weekdays'
_at = at'
_zone_name = zone_name'
fun val weekdays(): Array[DayOfWeek] val => _weekdays
fun val at(): TimeOfDay val => _at
fun val zone_name(): String val => _zone_name
fun val iter_after(after: ZonedDateTime box): WeekdayIter =>
"""
Build an iterator that yields successive fire instants strictly
after `after`. Each call to `next()` advances at least one day;
consecutive calls within the same week may advance only one day
(across weekdays in the set) or skip several (across excluded
weekend days).
"""
WeekdayIter(this, after)
class ref WeekdayIter is Iterator[(ZonedDateTime iso^ | NextFireError)]
"""
Iterator over the fire instants of a `WeekdayRecurrence`. Each call
to `next()` yields either a fresh `ZonedDateTime iso^` or a
`NextFireError` — the error is part of the value type, not a side
channel.
Termination: when the iterator can't progress, the next call emits
the relevant `NextFireError` exactly once, then `has_next()` returns
`false` forever after. A `for` loop sees the error in the body via
match, then terminates naturally on the following `has_next()` check.
"""
let _weekdays: Array[DayOfWeek] val
let _target_tod: TimeOfDay val
let _zone_name: String val
var _state: _WeekdayIterState
new ref create(r: WeekdayRecurrence, after: ZonedDateTime box) =>
_weekdays = r.weekdays()
_target_tod = r.at()
_zone_name = r.zone_name()
_state = _WeekdayIterStart(after.to_posix())
fun ref has_next(): Bool =>
match _state
| _WeekdayIterDone => false
else true
end
fun ref next(): (ZonedDateTime iso^ | NextFireError) =>
match _state
| let s: _WeekdayIterStart => _emit(_begin(s.after_posix))
| let c: _WeekdayIterCursor => _emit(_advance(c.cursor_date, c.lower))
| let stuck: _WeekdayIterStuck =>
let e = stuck.err
_state = _WeekdayIterDone
e
| _WeekdayIterDone =>
// Caller didn't honor the has_next() contract. Degrade
// gracefully rather than crash.
NextFireError
end
fun ref _emit(posix: ((I64, I64) | NextFireError))
: (ZonedDateTime iso^ | NextFireError)
=>
"""
Lift the internal POSIX-or-error result into the public ZDT-or-
error shape. Handles two state-transition cases:
- POSIX returned: build the ZDT. If construction fails (tzdata
coverage edge), transition to Done and emit NextFireError.
- Error returned: `_begin`/`_advance` already set `_state` to
`_Stuck`. We surface that error now and move to `_Done`.
"""
match posix
| (let sec: I64, let nsec: I64) =>
try
recover iso ZonedDateTime.from_posix_in_zone((sec, nsec), _zone_name)? end
else
_state = _WeekdayIterDone
NextFireError
end
| let err: NextFireError =>
_state = _WeekdayIterDone
err
end
fun ref _begin(after_posix: (I64, I64)): ((I64, I64) | NextFireError) =>
if _weekdays.size() == 0 then
_state = _WeekdayIterStuck(NextFireError)
return NextFireError
end
let zdt =
try
ZonedDateTime.from_posix_in_zone(after_posix, _zone_name)?
else
_state = _WeekdayIterStuck(NextFireError)
return NextFireError
end
_advance(zdt.local_date(), after_posix)
fun ref _advance(start_date: Date val, lower: (I64, I64))
: ((I64, I64) | NextFireError)
=>
var cur = start_date
var i: USize = 0
while i < 8 do
if _weekday_in_set(cur.day_of_week()) then
match _RecurrenceMath.local_to_utc_in_zone(
cur, _target_tod, _zone_name)
| (let s: I64, let n: I64) =>
let fire = (s, n)
if _RecurrenceMath.posix_gt(fire, lower) then
let next_cur =
match cur.add_days(1)
| let d: Date val => d
| let _: ArithmeticError =>
_state = _WeekdayIterStuck(NextFireError)
return fire // emit this fire; error queued for next call
end
_state = _WeekdayIterCursor(next_cur, fire)
return fire
end
| let e: NextFireError =>
_state = _WeekdayIterStuck(e)
return e
end
end
match cur.add_days(1)
| let d: Date val => cur = d
| let _: ArithmeticError =>
_state = _WeekdayIterStuck(NextFireError)
return NextFireError
end
i = i + 1
end
_state = _WeekdayIterStuck(NextFireError)
NextFireError
fun _weekday_in_set(d: DayOfWeek): Bool =>
"""
True if `d` is one of the weekdays this iterator fires on.
"""
for entry in _weekdays.values() do
if d is entry then return true end
end
false
class val _WeekdayIterStart
let after_posix: (I64, I64)
new val create(p: (I64, I64)) => after_posix = p
class val _WeekdayIterCursor
let cursor_date: Date val
let lower: (I64, I64)
new val create(d: Date val, l: (I64, I64)) =>
cursor_date = d
lower = l
class val _WeekdayIterStuck
"""Internal: error queued, to be emitted on the next `next()` call."""
let err: NextFireError
new val create(e: NextFireError) => err = e
primitive _WeekdayIterDone
"""Internal: error has been emitted; the iterator is exhausted."""
type _WeekdayIterState is
( _WeekdayIterStart
| _WeekdayIterCursor
| _WeekdayIterStuck
| _WeekdayIterDone )