///|
/// Display names indexed by cron weekday value; index 7 is Sunday again so
/// both spellings of Sunday render identically.
let weekday_display : Array[String] = [
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
]
///|
let month_display : Array[String] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September",
"October", "November", "December",
]
///|
fn weekday_name(value : Int) -> String {
if value >= 0 && value <= 7 {
weekday_display[value]
} else {
value.to_string()
}
}
///|
fn month_name(value : Int) -> String {
if value >= 1 && value <= 12 {
month_display[value - 1]
} else {
value.to_string()
}
}
///|
fn ordinal(value : Int) -> String {
let remainder = value % 100
let suffix = if remainder >= 11 && remainder <= 13 {
"th"
} else {
match value % 10 {
1 => "st"
2 => "nd"
3 => "rd"
_ => "th"
}
}
value.to_string() + suffix
}
///|
/// Join phrases as English prose: "a", "a and b", "a, b and c".
fn join_and(parts : Array[String]) -> String {
match parts.length() {
0 => ""
1 => parts[0]
_ => {
let writer = StringBuilder::new()
for index in 0..<(parts.length() - 1) {
if index > 0 {
writer.write_string(", ")
}
writer.write_string(parts[index])
}
writer.write_string(" and ")
writer.write_string(parts[parts.length() - 1])
writer.to_string()
}
}
}
///|
/// The values of a list whose elements are all exact, or `None`.
fn exact_values(items : Array[Field]) -> Array[Int]? {
let values : Array[Int] = []
for item in items {
match item {
Exact(value) => values.push(value)
_ => return None
}
}
Some(values)
}
///|
fn minute_item(field : Field) -> String {
match field {
Any => "every minute"
Exact(value) => "at minute " + value.to_string()
Every(1) => "every minute"
Every(step) => "every " + step.to_string() + " minutes"
Range(start, end) =>
"every minute from " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, 1) =>
"every minute from " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, step) =>
"every " +
step.to_string() +
" minutes from " +
start.to_string() +
" through " +
end.to_string()
List(items) =>
match exact_values(items) {
Some(values) =>
if values.length() == 1 {
"at minute " + values[0].to_string()
} else {
"at minutes " + join_and(values.map(value => value.to_string()))
}
None => join_and(items.map(item => minute_item(item)))
}
}
}
///|
fn hour_item(field : Field) -> String {
match field {
Any => "every hour"
Exact(value) => "hour " + value.to_string()
Every(1) => "every hour"
Every(step) => "every " + ordinal(step) + " hour"
Range(start, end) =>
"hours " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, 1) =>
"hours " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, step) =>
"every " +
ordinal(step) +
" hour from " +
start.to_string() +
" through " +
end.to_string()
List(items) =>
match exact_values(items) {
Some(values) =>
if values.length() == 1 {
"hour " + values[0].to_string()
} else {
"hours " + join_and(values.map(value => value.to_string()))
}
None => join_and(items.map(item => hour_item(item)))
}
}
}
///|
fn day_of_month_item(field : Field) -> String {
match field {
Any => "every day"
Exact(value) => "day " + value.to_string()
Every(1) => "every day"
Every(step) => "every " + ordinal(step) + " day"
Range(start, end) =>
"days " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, 1) =>
"days " + start.to_string() + " through " + end.to_string()
RangeEvery(start, end, step) =>
"every " +
ordinal(step) +
" day from " +
start.to_string() +
" through " +
end.to_string()
List(items) =>
match exact_values(items) {
Some(values) =>
if values.length() == 1 {
"day " + values[0].to_string()
} else {
"days " + join_and(values.map(value => value.to_string()))
}
None => join_and(items.map(item => day_of_month_item(item)))
}
}
}
///|
fn weekday_item(field : Field) -> String {
match field {
Any => "every day of the week"
Exact(value) => weekday_name(value)
Every(1) => "every day of the week"
Every(step) => "every " + ordinal(step) + " day of the week"
Range(start, end) => weekday_name(start) + " through " + weekday_name(end)
RangeEvery(start, end, 1) =>
weekday_name(start) + " through " + weekday_name(end)
RangeEvery(start, end, step) =>
"every " +
ordinal(step) +
" day of the week from " +
weekday_name(start) +
" through " +
weekday_name(end)
List(items) =>
match exact_values(items) {
Some(values) => join_and(values.map(value => weekday_name(value)))
None => join_and(items.map(item => weekday_item(item)))
}
}
}
///|
fn month_item(field : Field) -> String {
match field {
Any => "every month"
Exact(value) => month_name(value)
Every(1) => "every month"
Every(step) => "every " + ordinal(step) + " month"
Range(start, end) => month_name(start) + " through " + month_name(end)
RangeEvery(start, end, 1) =>
month_name(start) + " through " + month_name(end)
RangeEvery(start, end, step) =>
"every " +
ordinal(step) +
" month from " +
month_name(start) +
" through " +
month_name(end)
List(items) =>
match exact_values(items) {
Some(values) => join_and(values.map(value => month_name(value)))
None => join_and(items.map(item => month_item(item)))
}
}
}
///|
/// Render the schedule as an English sentence fragment, for example
/// "every 15 minutes past hours 9 through 17 on Monday through Friday".
/// When both day fields are restricted the wording mirrors cron's OR rule.
pub fn Cron::describe(self : Cron) -> String {
let writer = StringBuilder::new()
writer.write_string(minute_item(self.minute))
if !(self.minute is Any && self.hour is Any) {
writer.write_string(" past ")
writer.write_string(hour_item(self.hour))
}
match (self.day_of_month, self.weekday) {
(Any, Any) => ()
(Any, _) => {
writer.write_string(" on ")
writer.write_string(weekday_item(self.weekday))
}
(_, Any) => {
writer.write_string(" on ")
writer.write_string(day_of_month_item(self.day_of_month))
writer.write_string(" of the month")
}
_ => {
writer.write_string(" on ")
writer.write_string(day_of_month_item(self.day_of_month))
writer.write_string(" of the month or on ")
writer.write_string(weekday_item(self.weekday))
}
}
if !(self.month is Any) {
writer.write_string(" in ")
writer.write_string(month_item(self.month))
}
writer.to_string()
}