Skip to main content

ifc_schedule/authoring/
timing.rs

1//! Authoring task times, recurring task times, and time periods.
2//!
3//! Split from the module root: these four writers share the
4//! `IfcTaskTime` slot table, and the root was over the monolith limit.
5
6use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
7
8use super::{optional_text, ScheduleAuthoringResult};
9use crate::error::ScheduleAuthoringError;
10use crate::task::definition::time_slot;
11
12/// Authored fields for `IfcTaskTime`.
13///
14/// Durations are ISO 8601 duration strings and timestamps ISO 8601
15/// datetimes, written exactly as given. Parsing them here would mean this
16/// crate owning calendar arithmetic it deliberately does not.
17#[derive(Debug, Clone, Copy, Default)]
18pub struct TaskTimeDraft<'a> {
19    /// `IfcPhysicalQuantity`-style Name, slot 0.
20    pub name: Option<&'a str>,
21    /// `IfcTaskTime.DurationType`, `WORKTIME` or `ELAPSEDTIME`.
22    pub duration_type: Option<&'a str>,
23    /// `IfcTaskTime.ScheduleDuration`, an ISO 8601 duration.
24    pub schedule_duration: Option<&'a str>,
25    /// `IfcTaskTime.ScheduleStart`.
26    pub schedule_start: Option<&'a str>,
27    /// `IfcTaskTime.ScheduleFinish`.
28    pub schedule_finish: Option<&'a str>,
29    /// `IfcTaskTime.ActualStart`.
30    pub actual_start: Option<&'a str>,
31    /// `IfcTaskTime.ActualFinish`.
32    pub actual_finish: Option<&'a str>,
33    /// `IfcTaskTime.IsCritical`.
34    pub is_critical: Option<bool>,
35    /// `IfcTaskTime.Completion`, a percentage in `0..=100`.
36    pub completion: Option<f64>,
37}
38
39/// Build and validate the shared `IfcTaskTime` attribute vector.
40///
41/// `IfcTaskTimeRecurring` inherits every one of these slots and adds
42/// `Recurrence` at the end, so both writers share this body rather than
43/// duplicating nine slot assignments and two range checks.
44fn task_time_attributes(
45    entity: &'static str,
46    draft: TaskTimeDraft<'_>,
47) -> ScheduleAuthoringResult<Vec<Value>> {
48    if let Some(completion) = draft.completion {
49        if !completion.is_finite() || !(0.0..=100.0).contains(&completion) {
50            return Err(ScheduleAuthoringError::InvalidValue {
51                entity,
52                attribute: "Completion",
53                expected: "a percentage in 0..=100",
54            });
55        }
56    }
57    if let Some(kind) = draft.duration_type {
58        if !["WORKTIME", "ELAPSEDTIME", "NOTDEFINED"]
59            .iter()
60            .any(|known| known.eq_ignore_ascii_case(kind))
61        {
62            return Err(ScheduleAuthoringError::InvalidValue {
63                entity,
64                attribute: "DurationType",
65                expected: "WORKTIME, ELAPSEDTIME or NOTDEFINED",
66            });
67        }
68    }
69    let mut attributes = vec![Value::Null; time_slot::COMPLETION + 1];
70    attributes[0] = optional_text(draft.name);
71    attributes[time_slot::DURATION_TYPE] = draft
72        .duration_type
73        .map_or(Value::Null, |t| Value::Enum(t.into()));
74    attributes[time_slot::SCHEDULE_DURATION] = optional_text(draft.schedule_duration);
75    attributes[time_slot::SCHEDULE_START] = optional_text(draft.schedule_start);
76    attributes[time_slot::SCHEDULE_FINISH] = optional_text(draft.schedule_finish);
77    attributes[time_slot::ACTUAL_START] = optional_text(draft.actual_start);
78    attributes[time_slot::ACTUAL_FINISH] = optional_text(draft.actual_finish);
79    attributes[time_slot::IS_CRITICAL] = draft.is_critical.map_or(Value::Null, Value::Bool);
80    attributes[time_slot::COMPLETION] = draft.completion.map_or(Value::Null, Value::Real);
81    Ok(attributes)
82}
83
84/// Stage an `IfcTaskTime`.
85///
86/// # Errors
87///
88/// Refuses a completion outside `0..=100` and an unknown duration type.
89pub fn create_task_time(
90    tx: &mut Transaction,
91    draft: TaskTimeDraft<'_>,
92) -> ScheduleAuthoringResult<EntityId> {
93    let attributes = task_time_attributes("IFCTASKTIME", draft)?;
94    Ok(tx.create(Entity::new("IFCTASKTIME", attributes)))
95}
96
97/// Stage an `IfcTaskTimeRecurring`.
98///
99/// The recurring form is an `IfcTaskTime` plus a required
100/// `Recurrence` at slot 20. The pattern is what makes the task repeat,
101/// so it is a required argument rather than an optional draft field:
102/// a recurring time with no pattern recurs never.
103///
104/// # Errors
105///
106/// As for [`create_task_time`], and refuses a `recurrence` that is not
107/// an `IfcRecurrencePattern`.
108pub fn create_task_time_recurring(
109    tx: &mut Transaction,
110    model: &Model,
111    draft: TaskTimeDraft<'_>,
112    recurrence: EntityId,
113) -> ScheduleAuthoringResult<EntityId> {
114    const ENTITY: &str = "IFCTASKTIMERECURRING";
115    const RECURRENCE_SLOT: usize = 20;
116    let pattern_is_valid = tx
117        .edits()
118        .iter()
119        .rev()
120        .find_map(|edit| match edit {
121            Edit::Create { id, entity } if *id == recurrence => {
122                Some(entity.type_name.as_ref().to_owned())
123            }
124            _ => None,
125        })
126        .or_else(|| {
127            model
128                .get(recurrence)
129                .map(|entity| entity.type_name.as_ref().to_owned())
130        })
131        .is_some_and(|name| name.eq_ignore_ascii_case("IFCRECURRENCEPATTERN"));
132    if !pattern_is_valid {
133        return Err(ScheduleAuthoringError::InvalidValue {
134            entity: ENTITY,
135            attribute: "Recurrence",
136            expected: "an IfcRecurrencePattern",
137        });
138    }
139    let mut attributes = task_time_attributes(ENTITY, draft)?;
140    attributes.resize(RECURRENCE_SLOT + 1, Value::Null);
141    attributes[RECURRENCE_SLOT] = Value::Ref(recurrence);
142    Ok(tx.create(Entity::new(ENTITY, attributes)))
143}
144
145/// Stage an `IfcTimePeriod`: a start and end time of day.
146///
147/// Both slots are `IfcTime`, a time of day rather than a timestamp,
148/// and both are required. The values are written as given for the
149/// same reason `IfcTaskTime` does not parse its durations: the codec
150/// round-trips text, and reinterpreting it here would lose whatever
151/// the authoring tool meant by it.
152///
153/// # Errors
154///
155/// Refuses a blank start or end.
156pub fn create_time_period(
157    tx: &mut Transaction,
158    start_time: &str,
159    end_time: &str,
160) -> ScheduleAuthoringResult<EntityId> {
161    const ENTITY: &str = "IFCTIMEPERIOD";
162    for (attribute, value) in [("StartTime", start_time), ("EndTime", end_time)] {
163        if value.trim().is_empty() {
164            return Err(ScheduleAuthoringError::InvalidValue {
165                entity: ENTITY,
166                attribute,
167                expected: "a non-empty ISO 8601 time",
168            });
169        }
170    }
171    Ok(tx.create(Entity::new(
172        ENTITY,
173        vec![Value::Text(start_time.into()), Value::Text(end_time.into())],
174    )))
175}