use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use crate::names::{capitalize, clock, day_name, human_list, month_name};
use crate::parse::Schedule;
use crate::segment::Segment;
use crate::{Options, TimeFormat};
pub(crate) fn describe(schedule: &Schedule, options: Options) -> String {
let fmt = options.format();
let mut clauses: Vec<String> = Vec::new();
clauses.push(time_clause(&schedule.minute, &schedule.hour, fmt));
let has_day_of_month = !is_all(&schedule.dom);
let has_weekday = !is_all(&schedule.dow);
if has_day_of_month && has_weekday {
clauses.push(format!(
"{} or {}",
dom_text(&schedule.dom),
dow_text(&schedule.dow, true)
));
} else if has_day_of_month {
clauses.push(dom_text(&schedule.dom));
}
if let Some(c) = month_clause(&schedule.month) {
clauses.push(c);
}
if has_weekday && !has_day_of_month {
clauses.push(dow_text(&schedule.dow, false));
}
clauses.join(", ")
}
fn is_all(field: &[Segment]) -> bool {
matches!(field, [Segment::All])
}
fn single(field: &[Segment]) -> Option<u32> {
match field {
[Segment::Value(v)] => Some(*v),
_ => None,
}
}
fn step(field: &[Segment]) -> Option<u32> {
match field {
[Segment::Step(n)] => Some(*n),
_ => None,
}
}
fn all_values(field: &[Segment]) -> Option<Vec<u32>> {
field
.iter()
.map(|s| match s {
Segment::Value(v) => Some(*v),
_ => None,
})
.collect()
}
fn plural(n: u32, unit: &str) -> String {
if n == 1 {
String::from(unit)
} else {
format!("{unit}s")
}
}
fn every_unit(n: u32, unit: &str) -> String {
if n == 1 {
format!("every {unit}")
} else {
format!("every {n} {unit}s")
}
}
fn time_clause(minute: &[Segment], hour: &[Segment], fmt: TimeFormat) -> String {
if let (Some(m), Some(h)) = (single(minute), single(hour)) {
return format!("At {}", clock(h, m, fmt));
}
if let (Some(m), Some(hours)) = (single(minute), all_values(hour)) {
let times: Vec<String> = hours.iter().map(|&h| clock(h, m, fmt)).collect();
return format!("At {}", human_list(×));
}
if is_all(minute) && is_all(hour) {
return String::from("Every minute");
}
let minute_fixed = single(minute).is_some();
if is_all(hour) {
if let Some(n) = step(minute) {
return format!("Every {n} minutes");
}
if let Some(m) = single(minute) {
return format!("At {m} {} past the hour", plural(m, "minute"));
}
if let Some(vals) = all_values(minute) {
return format!("At minutes {} past the hour", number_list(&vals));
}
return capitalize(&minute_phrase(minute));
}
if is_all(minute) {
return format!("Every minute, {}", hour_phrase(hour, fmt, false));
}
format!(
"{}, {}",
capitalize(&minute_phrase(minute)),
hour_phrase(hour, fmt, minute_fixed)
)
}
fn minute_phrase(minute: &[Segment]) -> String {
if is_all(minute) {
return String::from("every minute");
}
if let Some(n) = step(minute) {
return format!("every {n} minutes");
}
match minute {
[Segment::Value(m)] => format!("at minute {m}"),
[Segment::Range(a, b)] => format!("every minute from {a} through {b}"),
[Segment::RangeStep(a, b, n)] => {
format!("{} from {a} through {b}", every_unit(*n, "minute"))
}
_ => match all_values(minute) {
Some(vals) => format!("at minutes {}", number_list(&vals)),
None => String::from("at the listed minutes"),
},
}
}
fn hour_phrase(hour: &[Segment], fmt: TimeFormat, minute_fixed: bool) -> String {
let end_min = if minute_fixed { 0 } else { 59 };
match hour {
[Segment::Step(n)] => format!("every {n} hours"),
[Segment::Value(h)] => {
format!(
"between {} and {}",
clock(*h, 0, fmt),
clock(*h, end_min, fmt)
)
}
[Segment::Range(a, b)] => {
format!(
"between {} and {}",
clock(*a, 0, fmt),
clock(*b, end_min, fmt)
)
}
[Segment::RangeStep(a, b, n)] => format!(
"every {n} hours between {} and {}",
clock(*a, 0, fmt),
clock(*b, 0, fmt)
),
_ => match all_values(hour) {
Some(vals) => {
let times: Vec<String> = vals.iter().map(|&h| clock(h, 0, fmt)).collect();
format!("at {}", human_list(×))
}
None => String::from("during the listed hours"),
},
}
}
fn dom_text(dom: &[Segment]) -> String {
match dom {
[Segment::Value(d)] => format!("on day {d} of the month"),
[Segment::Range(a, b)] => format!("on days {a} through {b} of the month"),
[Segment::Step(n)] => format!("every {n} days of the month"),
[Segment::RangeStep(a, b, n)] => {
format!(
"{} from {a} through {b} of the month",
every_unit(*n, "day")
)
}
_ => match all_values(dom) {
Some(vals) => format!("on days {} of the month", number_list(&vals)),
None => String::from("on the listed days of the month"),
},
}
}
fn month_clause(month: &[Segment]) -> Option<String> {
if is_all(month) {
return None;
}
Some(match month {
[Segment::Value(m)] => format!("only in {}", month_name(*m)),
[Segment::Range(a, b)] => format!("{} through {}", month_name(*a), month_name(*b)),
[Segment::Step(n)] => format!("every {n} months"),
[Segment::RangeStep(a, b, n)] => format!(
"{} from {} through {}",
every_unit(*n, "month"),
month_name(*a),
month_name(*b)
),
_ => match all_values(month) {
Some(vals) => {
let names: Vec<String> = vals.iter().map(|&m| month_name(m)).collect();
format!("only in {}", human_list(&names))
}
None => String::from("in the listed months"),
},
})
}
fn dow_text(dow: &[Segment], or_mode: bool) -> String {
let only = if or_mode { "on" } else { "only on" };
match dow {
[Segment::Value(w)] => format!("{only} {}", day_name(*w)),
[Segment::Range(a, b)] => {
let body = format!("{} through {}", day_name(*a), day_name(*b));
if or_mode {
format!("on {body}")
} else {
body
}
}
[Segment::Step(n)] => format!("every {n} days of the week"),
[Segment::RangeStep(a, b, n)] => format!(
"{} from {} through {}",
every_unit(*n, "day"),
day_name(*a),
day_name(*b)
),
_ => match all_values(dow) {
Some(vals) => {
let names: Vec<String> = vals.iter().map(|&w| day_name(w)).collect();
format!("{only} {}", human_list(&names))
}
None => String::from("on the listed days of the week"),
},
}
}
fn number_list(vals: &[u32]) -> String {
let items: Vec<String> = vals.iter().map(|v| format!("{v}")).collect();
human_list(&items)
}