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}