mod definition;
pub(crate) mod export;
mod parse;
pub(crate) use parse::{SourceCalendar, parse_all};
use super::report::{CalendarOutcome, CalendarReport, Finding};
use super::source::SourceProject;
use crate::InterchangeError;
use dpm_model::{
ALWAYS, CalendarDefinition, Calendars, ClockTime, Plan, STANDARD, WeekPattern, WorkingPeriod,
};
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)]
pub(crate) struct Imported {
pub(crate) calendars: Calendars,
names: BTreeMap<i64, String>,
pub(crate) project: String,
}
impl Imported {
pub(crate) fn name(&self, uid: i64) -> Option<&str> {
self.names.get(&uid).map(String::as_str)
}
}
pub(crate) struct CalendarImport {
pub(crate) imported: Imported,
pub(crate) reports: Vec<CalendarReport>,
pub(crate) findings: Vec<Finding>,
}
pub(crate) fn import(
current: &Plan,
source: &SourceProject,
time_zone: &str,
) -> Result<CalendarImport, InterchangeError> {
let mut calendars = workspace(current, time_zone)?;
let standard = calendars
.definitions
.get(STANDARD)
.cloned()
.unwrap_or_else(dpm_model::standard_definition);
let used: BTreeSet<i64> = source
.calendar_uid
.into_iter()
.chain(source.tasks.iter().filter_map(|t| t.calendar_uid))
.collect();
let mut all = BTreeMap::new();
let mut reports = Vec::new();
for calendar in &source.calendars {
match all.entry(calendar.uid) {
Entry::Vacant(slot) => {
slot.insert(calendar);
}
Entry::Occupied(_) => {
reports.push(skipped(calendar, "an earlier calendar has the same UID"));
}
}
}
let mut names = BTreeMap::new();
let mut taken = BTreeSet::new();
for calendar in &source.calendars {
if all
.get(&calendar.uid)
.is_none_or(|c| !std::ptr::eq(*c, calendar))
{
continue;
}
if !calendar.is_base && !used.contains(&calendar.uid) {
reports.push(skipped(
calendar,
"resource calendar not imported; resources are not imported",
));
continue;
}
let combined = match definition::combine(&all, calendar.uid) {
Ok(combined) => combined,
Err(reason) => {
reports.push(skipped(calendar, &reason));
continue;
}
};
let (name, outcome) = place(
calendar,
combined.definition,
&standard,
&mut calendars,
&mut taken,
);
names.insert(calendar.uid, name.clone());
let affected_work = match outcome {
CalendarOutcome::Updated => following(current, &name),
_ => Vec::new(),
};
reports.push(CalendarReport {
uid: calendar.uid,
name: calendar.name.clone(),
calendar: Some(name),
outcome,
approximated: combined.approximated,
rejected: combined.rejected,
affected_work,
});
}
let mut findings = Vec::new();
let project = project_calendar(current, source, &names, &mut calendars, &mut findings);
Ok(CalendarImport {
imported: Imported {
calendars,
names,
project,
},
reports,
findings,
})
}
fn workspace(current: &Plan, time_zone: &str) -> Result<Calendars, InterchangeError> {
if Calendars::in_zone(time_zone).zone().is_err() {
return Err(InterchangeError::InvalidTimeZone {
time_zone: time_zone.into(),
});
}
match ¤t.calendars {
Some(existing) if existing.time_zone != time_zone => {
Err(InterchangeError::TimeZoneMismatch {
workspace: existing.time_zone.clone(),
requested: time_zone.into(),
})
}
Some(existing) => Ok(existing.clone()),
None => Ok(Calendars::in_zone(time_zone)),
}
}
fn project_calendar(
current: &Plan,
source: &SourceProject,
names: &BTreeMap<i64, String>,
calendars: &mut Calendars,
findings: &mut Vec<Finding>,
) -> String {
match source.calendar_uid.map(|uid| (uid, names.get(&uid))) {
Some((_, Some(name))) => {
if current.calendars.is_none() {
calendars.kinds.human = name.clone();
}
name.clone()
}
Some((uid, None)) => {
findings.push(Finding::new(
"calendars",
format!(
"project calendar UID {uid} not imported; work without its own calendar follows {:?}",
calendars.kinds.human
),
));
calendars.kinds.human.clone()
}
None => {
findings.push(Finding::new(
"calendars",
format!(
"the document names no project calendar; work without its own calendar follows {:?}",
calendars.kinds.human
),
));
calendars.kinds.human.clone()
}
}
}
fn skipped(calendar: &SourceCalendar, reason: &str) -> CalendarReport {
let mut rejected = vec![Finding::new("calendar", reason)];
rejected.extend(calendar.rejected.iter().cloned());
CalendarReport {
uid: calendar.uid,
name: calendar.name.clone(),
calendar: None,
outcome: CalendarOutcome::Skipped,
approximated: calendar.approximated.clone(),
rejected,
affected_work: Vec::new(),
}
}
fn unique_name(calendar: &SourceCalendar, taken: &mut BTreeSet<String>) -> String {
let base = if calendar.name.is_empty() {
format!("Calendar {}", calendar.uid)
} else {
calendar.name.clone()
};
let reserved = |name: &str| name == ALWAYS || name == STANDARD || taken.contains(name);
let mut name = base.clone();
let mut attempt: u32 = 1;
while reserved(&name) {
name = if attempt == 1 {
format!("{base} (UID {})", calendar.uid)
} else {
format!("{base} (UID {} #{attempt})", calendar.uid)
};
attempt = attempt.wrapping_add(1);
}
taken.insert(name.clone());
name
}
fn place(
calendar: &SourceCalendar,
definition: CalendarDefinition,
standard: &CalendarDefinition,
calendars: &mut Calendars,
taken: &mut BTreeSet<String>,
) -> (String, CalendarOutcome) {
if calendar.name.eq_ignore_ascii_case("Standard") && definition == *standard {
(STANDARD.to_owned(), CalendarOutcome::BuiltIn)
} else if calendar.name.eq_ignore_ascii_case("24 Hours") && definition == always_definition() {
(ALWAYS.to_owned(), CalendarOutcome::BuiltIn)
} else {
let name = unique_name(calendar, taken);
let outcome = store(calendars, &name, definition);
(name, outcome)
}
}
fn following(current: &Plan, name: &str) -> Vec<dpm_model::Key> {
let Some(calendars) = ¤t.calendars else {
return Vec::new();
};
current
.work_items
.values()
.filter(|w| w.is_executable() && calendars.resolve(w).calendar == name)
.map(|w| w.key.clone())
.collect()
}
fn store(calendars: &mut Calendars, name: &str, definition: CalendarDefinition) -> CalendarOutcome {
match calendars
.definitions
.insert(name.to_owned(), definition.clone())
{
None => CalendarOutcome::Created,
Some(previous) if previous == definition => CalendarOutcome::Unchanged,
Some(_) => CalendarOutcome::Updated,
}
}
pub(crate) fn always_definition() -> CalendarDefinition {
let day = vec![WorkingPeriod::new(
ClockTime::hours(0),
ClockTime::hours(24),
)];
CalendarDefinition {
week: WeekPattern {
mon: day.clone(),
tue: day.clone(),
wed: day.clone(),
thu: day.clone(),
fri: day.clone(),
sat: day.clone(),
sun: day,
},
exceptions: Vec::new(),
}
}
#[cfg(test)]
mod tests;