///|
priv enum WrapperDateValue {
Omitted
NullDate
DateValue(RRuleDateTime)
}
///|
// YearLocator passes raw month/day values to datetime.replace. Preserve the
// keyword's shape even though the recurrence core consumes a selector array.
priv enum WrapperCalendarForm {
OmittedCalendar
NullCalendar
ScalarCalendar(Int32)
ArrayCalendar
}
///|
fn WrapperCalendarForm::date_field(
self : WrapperCalendarForm,
) -> Int32 raise DateError {
match self {
OmittedCalendar => 1
ScalarCalendar(value) => value
NullCalendar | ArrayCalendar =>
raise DateError::InvalidDate(
message="YearLocator calendar keyword must be a scalar integer",
)
}
}
///|
priv struct WrapperOptions {
frequency : RRuleFrequency
interval : Int32
wkst : Int32
dtstart : WrapperDateValue
until : WrapperDateValue
count : Int32?
cache : Bool
bymonth_form : WrapperCalendarForm
bymonthday_form : WrapperCalendarForm
bymonth : Array[Int32]?
bymonthday : Array[Int32]?
byyearday : Array[Int32]?
byweekno : Array[Int32]?
byeaster : Array[Int32]?
byweekday : Array[Int32]?
byhour : Array[Int32]?
byminute : Array[Int32]?
bysecond : Array[Int32]?
bysetpos : Array[Int32]?
byweekdays : Array[RelativeWeekday]?
}
///|
fn wrapper_date_option(value : RRuleDateTime?) -> WrapperDateValue {
match value {
Some(value) => DateValue(value)
None => Omitted
}
}
///|
// None-valued date keywords are distinct from omitted ones in the original
// wrapper: accessing their tzinfo fails before constructing the underlying rule.
fn WrapperOptions::with_null(
self : WrapperOptions,
names : Array[String],
) -> WrapperOptions raise DateError {
let mut result = self
for name in names {
result = match name {
"dtstart" => { ..result, dtstart: NullDate, }
"until" => { ..result, until: NullDate, }
"count" => { ..result, count: None, }
"wkst" => { ..result, wkst: 0, }
"cache" => { ..result, cache: false, }
"bymonth" => { ..result, bymonth: None, bymonth_form: NullCalendar, }
"bymonthday" =>
{ ..result, bymonthday: None, bymonthday_form: NullCalendar, }
"byyearday" => { ..result, byyearday: None, }
"byweekno" => { ..result, byweekno: None, }
"byeaster" => { ..result, byeaster: None, }
"byweekday" | "byweekdays" =>
{ ..result, byweekday: None, byweekdays: None, }
"byhour" => { ..result, byhour: None, }
"byminute" => { ..result, byminute: None, }
"bysecond" => { ..result, bysecond: None, }
"bysetpos" => { ..result, bysetpos: None, }
_ =>
raise DateError::InvalidDate(
message="unsupported null rule keyword '\{name}'",
)
}
}
result
}
///|
fn WrapperOptions::snapshot(self : WrapperOptions) -> WrapperOptions {
{
..self,
bymonth: self.bymonth.map(values => values.copy()),
bymonthday: self.bymonthday.map(values => values.copy()),
byyearday: self.byyearday.map(values => values.copy()),
byweekno: self.byweekno.map(values => values.copy()),
byeaster: self.byeaster.map(values => values.copy()),
byweekday: self.byweekday.map(values => values.copy()),
byhour: self.byhour.map(values => values.copy()),
byminute: self.byminute.map(values => values.copy()),
bysecond: self.bysecond.map(values => values.copy()),
bysetpos: self.bysetpos.map(values => values.copy()),
byweekdays: self.byweekdays.map(values => values.copy()),
}
}
///|
fn WrapperOptions::normalized(
self : WrapperOptions,
base : DateTimeZone?,
) -> (WrapperOptions, DateTimeZone?) raise DateError {
let mut timezone = base
let mut result = self
match self.dtstart {
NullDate =>
raise DateError::InvalidDate(
message="rrulewrapper dtstart cannot be None",
)
DateValue(start) if start.timezone is Some(zone) => {
let wall = match timezone {
Some(base) => start.wall_in(base)
None => {
timezone = Some(zone)
rrule_wall_date(start.value)
}
}
result = { ..result, dtstart: DateValue(RRuleDateTime::naive(wall)), }
}
_ => ()
}
match self.until {
NullDate =>
raise DateError::InvalidDate(message="rrulewrapper until cannot be None")
DateValue(until) if until.timezone is Some(_) => {
guard timezone is Some(zone) else {
raise DateError::InvalidDate(
message="until cannot be aware when dtstart is naive and no base timezone exists",
)
}
result = {
..result,
until: DateValue(RRuleDateTime::naive(until.wall_in(zone))),
}
}
_ => ()
}
(result, timezone)
}
///|
fn WrapperOptions::build_rule(
self : WrapperOptions,
context : DateParserContext?,
) -> RRule raise DateError {
let start = match self.dtstart {
DateValue(start) => rrule_wall_date(start.value, truncate=true)
_ => {
let context = match context {
Some(value) => value
None => DateParserContext::system()
}
rrule_wall_date(context.local_now((context.clock)()), truncate=true)
}
}
let until = match self.until {
DateValue(value) => Some(rrule_wall_date(value.value))
_ => None
}
if self.byweekday is Some(_) && self.byweekdays is Some(_) {
raise DateError::InvalidDate(
message="use byweekday integers or byweekdays objects",
)
}
let mut plain = self.byweekday
let mut ordinal : Array[RelativeWeekday]? = None
if self.byweekdays is Some(values) {
if values.is_empty() {
raise DateError::InvalidDate(
message="weekday selector array must not be empty",
)
}
let weekdays : Array[Int32] = []
let ordered : Array[RelativeWeekday] = []
for value in values {
ignore(weekday_selector(value.weekday))
if (self.frequency is Yearly || self.frequency is Monthly) &&
value.nth.unwrap_or(0) != 0 {
if !ordered.contains(value) {
ordered.push(value)
}
} else if !weekdays.contains(value.weekday) {
weekdays.push(value.weekday)
}
}
if !weekdays.is_empty() {
plain = Some(weekdays)
}
if !ordered.is_empty() {
ordinal = Some(ordered)
}
}
let core = rrule(
self.frequency,
start,
interval=self.interval,
wkst=self.wkst,
bymonth?=self.bymonth,
bymonthday?=self.bymonthday,
byyearday?=self.byyearday,
byweekno?=self.byweekno,
byeaster?=self.byeaster,
byweekday?=plain,
byhour?=self.byhour,
byminute?=self.byminute,
bysecond?=self.bysecond,
bysetpos?=self.bysetpos,
until?,
cache=self.cache,
)
let rule = match self.frequency {
Hourly =>
{
..core.rule,
byhour: wrapper_clock_selection(
core.rule.byhour,
start.hour,
self.interval,
24,
),
}
Minutely =>
{
..core.rule,
byminute: wrapper_clock_selection(
core.rule.byminute,
start.minute,
self.interval,
60,
),
}
Secondly =>
{
..core.rule,
bysecond: wrapper_clock_selection(
core.rule.bysecond,
start.second,
self.interval,
60,
),
}
_ => core.rule
}
{
rule: { ..rule, count: self.count, bynweekday: ordinal, },
cache: core.cache,
}
}
///|
// dateutil's same-frequency BYHOUR/BYMINUTE/BYSECOND constructor keeps only
// residues reachable from DTSTART and raises before iteration if none remain.
// The raw construction arrays stay intact for the next wrapper.set call.
fn wrapper_clock_selection(
values : Array[Int32]?,
start : Int32,
interval : Int32,
base : Int32,
) -> Array[Int32]? raise DateError {
match values {
None => None
Some(values) => {
let divisor = locator_gcd(interval, base)
let selected = values.filter(value => (value - start) % divisor == 0)
if selected.is_empty() {
raise DateError::InvalidDate(
message="recurrence clock selector has no reachable value",
)
}
Some(selected)
}
}
}