Skip to main content

ifc_schedule/schedule/
work_control.rs

1//! `IfcWorkPlan` and `IfcWorkSchedule`: the documents that hold tasks.
2//!
3//! # Read by name in the declared release (#212)
4//!
5//! Both are `IfcWorkControl` subtypes, which is `IfcControl` -> `IfcObject`
6//! -> `IfcRoot`. IFC4 ADD2 TC1 and IFC4X3 ADD2 lay them out as below.
7//! IFC2X3 TC1 names slot 5 `Identifier`, types the dates as
8//! `IfcDateTimeSelect` records and `Duration` and `TotalFloat` as
9//! `IfcTimeMeasure`, and ends with `WorkControlType`
10//! (`IfcWorkControlTypeEnum`) and `UserDefinedControlType` instead of
11//! `PredefinedType`. Every accessor looks its attribute up by name in the
12//! model's declared release; a date or duration comes back in the form the
13//! release declares it ([`AuthoredDateTime`], [`AuthoredDuration`]):
14//!
15//! ```text
16//! 0 GlobalId        1 OwnerHistory    2 Name
17//! 3 Description     4 ObjectType      5 Identification   (IfcControl)
18//! -- IfcWorkControl --
19//! 6 CreationDate    7 Creators        8 Purpose
20//! 9 Duration       10 TotalFloat     11 StartTime
21//! 12 FinishTime
22//! -- subtype --
23//! 13 PredefinedType
24//! ```
25//!
26//! `StartTime` is slot 11 and required by the schema; `FinishTime` is 12 and
27//! optional. A reader that assumes the subtype's `PredefinedType` sits right
28//! after `Identification` -- as it does on most `IfcControl` subtypes -- lands
29//! on `CreationDate` instead and reports a date as a type token.
30//!
31//! # Dates are returned as authored
32//!
33//! `IfcDateTime` is an ISO 8601 string, IFC2X3's `IfcDateTimeSelect` a
34//! record. This crate does not parse either into a calendar type: doing so would force a date library into a crate whose only
35//! dependency is `ifc-model`, and would have to decide what to do with the
36//! offsets and partial dates real files carry. The string is returned intact
37//! and a caller that needs arithmetic parses it with the library it already
38//! uses.
39
40use ifc_model::{Entity, EntityId, Model, Value};
41
42use crate::error::ScheduleReadError;
43use crate::release::ReadRelease;
44use crate::SchemaVersion;
45
46/// `IfcWorkControl` slots, shared by plans and schedules, in IFC4 and
47/// IFC4X3. IFC2X3 differs from slot 13 on and in the types of 6 and 9..=12;
48/// the accessors read by name, never through these.
49pub mod slot {
50    /// `GlobalId` (from `IfcRoot`).
51    pub const GLOBAL_ID: usize = 0;
52    /// `Name` (from `IfcRoot`).
53    pub const NAME: usize = 2;
54    /// `Description` (from `IfcRoot`).
55    pub const DESCRIPTION: usize = 3;
56    /// `Identification` (from `IfcControl`).
57    pub const IDENTIFICATION: usize = 5;
58    /// `CreationDate`.
59    pub const CREATION_DATE: usize = 6;
60    /// `Purpose`.
61    pub const PURPOSE: usize = 8;
62    /// `Duration`, an ISO 8601 duration.
63    pub const DURATION: usize = 9;
64    /// `TotalFloat`, an ISO 8601 duration.
65    pub const TOTAL_FLOAT: usize = 10;
66    /// `StartTime`, required by the schema.
67    pub const START_TIME: usize = 11;
68    /// `FinishTime`.
69    pub const FINISH_TIME: usize = 12;
70    /// `PredefinedType`, contributed by the subtype.
71    pub const PREDEFINED_TYPE: usize = 13;
72}
73
74/// Whether a work control is a plan or a schedule.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum WorkControlKind {
78    /// `IfcWorkPlan`: a container for schedules.
79    Plan,
80    /// `IfcWorkSchedule`: a container for tasks.
81    Schedule,
82}
83
84impl WorkControlKind {
85    fn from_type(name: &str) -> Option<Self> {
86        if name.eq_ignore_ascii_case("IFCWORKPLAN") {
87            Some(Self::Plan)
88        } else if name.eq_ignore_ascii_case("IFCWORKSCHEDULE") {
89            Some(Self::Schedule)
90        } else {
91            None
92        }
93    }
94}
95
96/// A date and time as the model's release types it.
97///
98/// IFC4 and IFC4X3 declare `IfcDateTime`, ISO 8601 text; IFC2X3 declares
99/// `IfcDateTimeSelect`, a reference to an `IfcCalendarDate`, `IfcLocalTime`
100/// or `IfcDateAndTime` record. Both are returned as authored.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum AuthoredDateTime<'m> {
104    /// IFC4/IFC4X3 `IfcDateTime` text.
105    Text(&'m str),
106    /// IFC2X3 `IfcDateTimeSelect` record.
107    Record(EntityId),
108}
109
110impl<'m> AuthoredDateTime<'m> {
111    /// The text form, or `None` for a record.
112    #[must_use]
113    pub const fn text(self) -> Option<&'m str> {
114        match self {
115            Self::Text(text) => Some(text),
116            Self::Record(_) => None,
117        }
118    }
119
120    /// The record form, or `None` for text.
121    #[must_use]
122    pub const fn record(self) -> Option<EntityId> {
123        match self {
124            Self::Record(id) => Some(id),
125            Self::Text(_) => None,
126        }
127    }
128}
129
130/// A duration as the model's release types it.
131///
132/// IFC4 and IFC4X3 declare `IfcDuration`, ISO 8601 duration text; IFC2X3
133/// declares `IfcTimeMeasure`, a number in the project's time unit.
134#[derive(Debug, Clone, Copy, PartialEq)]
135#[non_exhaustive]
136pub enum AuthoredDuration<'m> {
137    /// IFC4/IFC4X3 `IfcDuration` text.
138    Text(&'m str),
139    /// IFC2X3 `IfcTimeMeasure`, in the project's time unit.
140    TimeMeasure(f64),
141}
142
143impl<'m> AuthoredDuration<'m> {
144    /// The text form, or `None` for a time measure.
145    #[must_use]
146    pub const fn text(self) -> Option<&'m str> {
147        match self {
148            Self::Text(text) => Some(text),
149            Self::TimeMeasure(_) => None,
150        }
151    }
152}
153
154/// A borrowed view of an `IfcWorkPlan` or `IfcWorkSchedule`, read against
155/// one release.
156#[derive(Debug, Clone, Copy)]
157pub struct WorkControl<'m> {
158    id: EntityId,
159    entity: &'m Entity,
160    kind: WorkControlKind,
161    release: ReadRelease,
162}
163
164impl<'m> WorkControl<'m> {
165    /// Wrap an entity if it is a work plan or work schedule, read against
166    /// `release`; `Ok(None)` for another entity.
167    ///
168    /// [`work_plans`] and [`work_schedules`] bind the model's declared
169    /// release; use this when the release is known some other way.
170    ///
171    /// # Errors
172    ///
173    /// [`ScheduleReadError::UnsupportedSchema`] for a release the readers
174    /// are not verified against (IFC4X1, IFC4X2).
175    pub fn new(
176        id: EntityId,
177        entity: &'m Entity,
178        release: SchemaVersion,
179    ) -> Result<Option<Self>, ScheduleReadError> {
180        let release = ReadRelease::of_version(release)?;
181        Ok(Self::bound(id, entity, release))
182    }
183
184    fn bound(id: EntityId, entity: &'m Entity, release: ReadRelease) -> Option<Self> {
185        let kind = WorkControlKind::from_type(&entity.type_name)?;
186        Some(Self {
187            id,
188            entity,
189            kind,
190            release,
191        })
192    }
193
194    /// The release this record is read against.
195    #[must_use]
196    pub fn release(&self) -> SchemaVersion {
197        self.release.version()
198    }
199
200    fn entity_type(&self) -> &'static str {
201        match self.kind {
202            WorkControlKind::Plan => "IFCWORKPLAN",
203            WorkControlKind::Schedule => "IFCWORKSCHEDULE",
204        }
205    }
206
207    fn text(&self, attribute: &'static str) -> Option<&'m str> {
208        self.release
209            .text(self.entity_type(), self.entity, attribute)
210    }
211
212    fn date_time(&self, attribute: &'static str) -> Option<AuthoredDateTime<'m>> {
213        match self
214            .release
215            .value(self.entity_type(), self.entity, attribute)?
216        {
217            Value::Ref(record) => Some(AuthoredDateTime::Record(*record)),
218            value => value.unwrap_typed().as_text().map(AuthoredDateTime::Text),
219        }
220    }
221
222    fn duration_value(&self, attribute: &'static str) -> Option<AuthoredDuration<'m>> {
223        let value = self
224            .release
225            .value(self.entity_type(), self.entity, attribute)?
226            .unwrap_typed();
227        match value.as_text() {
228            Some(text) => Some(AuthoredDuration::Text(text)),
229            None => value.as_f64().map(AuthoredDuration::TimeMeasure),
230        }
231    }
232
233    /// The entity id in the file.
234    #[must_use]
235    pub fn id(&self) -> EntityId {
236        self.id
237    }
238
239    /// Whether this is a plan or a schedule.
240    #[must_use]
241    pub fn kind(&self) -> WorkControlKind {
242        self.kind
243    }
244
245    /// The `GlobalId` string.
246    #[must_use]
247    pub fn global_id(&self) -> Option<&'m str> {
248        self.text("GlobalId")
249    }
250
251    /// The name.
252    #[must_use]
253    pub fn name(&self) -> Option<&'m str> {
254        self.text("Name")
255    }
256
257    /// The description.
258    #[must_use]
259    pub fn description(&self) -> Option<&'m str> {
260        self.text("Description")
261    }
262
263    /// The user-facing identification code.
264    #[must_use]
265    pub fn identification(&self) -> Option<&'m str> {
266        self.text("Identification")
267    }
268
269    /// Why the plan or schedule exists, as authored.
270    #[must_use]
271    pub fn purpose(&self) -> Option<&'m str> {
272        self.text("Purpose")
273    }
274
275    /// When it was created, as authored: ISO 8601 text or an IFC2X3 date
276    /// record.
277    #[must_use]
278    pub fn creation_date(&self) -> Option<AuthoredDateTime<'m>> {
279        self.date_time("CreationDate")
280    }
281
282    /// The planned start, as authored. Required by the schema.
283    #[must_use]
284    pub fn start_time(&self) -> Option<AuthoredDateTime<'m>> {
285        self.date_time("StartTime")
286    }
287
288    /// The planned finish, as authored.
289    #[must_use]
290    pub fn finish_time(&self) -> Option<AuthoredDateTime<'m>> {
291        self.date_time("FinishTime")
292    }
293
294    /// The overall duration, as authored: an ISO 8601 duration or an IFC2X3
295    /// time measure.
296    #[must_use]
297    pub fn duration(&self) -> Option<AuthoredDuration<'m>> {
298        self.duration_value("Duration")
299    }
300
301    /// The total float, as authored: an ISO 8601 duration or an IFC2X3 time
302    /// measure.
303    #[must_use]
304    pub fn total_float(&self) -> Option<AuthoredDuration<'m>> {
305        self.duration_value("TotalFloat")
306    }
307
308    /// The predefined type token, without its dots. `None` in IFC2X3, which
309    /// declares `WorkControlType` instead (see [`Self::work_control_type`]).
310    #[must_use]
311    pub fn predefined_type(&self) -> Option<&'m str> {
312        self.release
313            .token(self.entity_type(), self.entity, "PredefinedType")
314    }
315
316    /// IFC2X3's `WorkControlType` token (`IfcWorkControlTypeEnum`), without
317    /// its dots. `None` in IFC4 and IFC4X3, which declare `PredefinedType`
318    /// with their own enumerations instead; the two are not aliased.
319    #[must_use]
320    pub fn work_control_type(&self) -> Option<&'m str> {
321        self.release
322            .token(self.entity_type(), self.entity, "WorkControlType")
323    }
324}
325
326/// Every work plan in the model, in file order, read against the model's
327/// declared release.
328///
329/// # Errors
330///
331/// [`ScheduleReadError::UnsupportedSchema`] or
332/// [`ScheduleReadError::MultipleSchemas`] for a header the readers cannot
333/// bind; a header with no schema reads as IFC4.
334pub fn work_plans(model: &Model) -> Result<Vec<WorkControl<'_>>, ScheduleReadError> {
335    of_kind(model, "IFCWORKPLAN")
336}
337
338/// Every work schedule in the model, in file order, read against the
339/// model's declared release.
340///
341/// # Errors
342///
343/// As [`work_plans`].
344pub fn work_schedules(model: &Model) -> Result<Vec<WorkControl<'_>>, ScheduleReadError> {
345    of_kind(model, "IFCWORKSCHEDULE")
346}
347
348fn of_kind<'m>(
349    model: &'m Model,
350    type_name: &str,
351) -> Result<Vec<WorkControl<'m>>, ScheduleReadError> {
352    let release = ReadRelease::of(model)?;
353    Ok(model
354        .of_type(type_name)
355        .filter_map(|(id, entity)| WorkControl::bound(id, entity, release))
356        .collect())
357}