Skip to main content

ifc_schedule/calendar/
definition.rs

1//! `IfcWorkCalendar` and the working/exception times it declares.
2//!
3//! # Slots, verified against IFC4 EXPRESS
4//!
5//! `IfcWorkCalendar` is an `IfcControl`, so the first six slots are inherited:
6//!
7//! ```text
8//! 0 GlobalId        1 OwnerHistory    2 Name
9//! 3 Description     4 ObjectType      5 Identification   (IfcControl)
10//! 6 WorkingTimes    7 ExceptionTimes  8 PredefinedType
11//!
12//! IfcWorkTime  (IfcSchedulingTime)
13//! 0 Name           1 DataOrigin      2 UserDefinedDataOrigin
14//! 3 RecurrencePattern    4 Start      5 Finish
15//!
16//! IfcRecurrencePattern
17//! 0 RecurrenceType  1 DayComponent    2 WeekdayComponent
18//! 3 MonthComponent  4 Position        5 Interval
19//! 6 Occurrences     7 TimePeriods
20//! ```
21//!
22//! # Working times and exception times are both `IfcWorkTime`
23//!
24//! They differ only by which slot holds them: slot 6 declares when work
25//! happens, slot 7 declares when it does not. Same entity type, opposite
26//! meaning -- so a reader that collects "all the IfcWorkTimes" and treats them
27//! uniformly turns holidays into working days.
28
29use ifc_model::{Entity, EntityId, Model, Value};
30
31/// `IfcWorkCalendar` slots.
32pub mod slot {
33    /// `GlobalId` (from `IfcRoot`).
34    pub const GLOBAL_ID: usize = 0;
35    /// `Name` (from `IfcRoot`).
36    pub const NAME: usize = 2;
37    /// `Identification` (from `IfcControl`).
38    pub const IDENTIFICATION: usize = 5;
39    /// `WorkingTimes`: when work happens.
40    pub const WORKING_TIMES: usize = 6;
41    /// `ExceptionTimes`: when it does not.
42    pub const EXCEPTION_TIMES: usize = 7;
43    /// `PredefinedType`.
44    pub const PREDEFINED_TYPE: usize = 8;
45}
46
47/// `IfcWorkTime` slots.
48pub mod work_time_slot {
49    /// `Name`.
50    pub const NAME: usize = 0;
51    /// `RecurrencePattern`.
52    pub const RECURRENCE_PATTERN: usize = 3;
53    /// `Start`.
54    pub const START: usize = 4;
55    /// `Finish`.
56    pub const FINISH: usize = 5;
57}
58
59/// `IfcRecurrencePattern` slots.
60pub mod recurrence_slot {
61    /// `RecurrenceType`. Required by the schema.
62    pub const RECURRENCE_TYPE: usize = 0;
63    /// `DayComponent`.
64    pub const DAY_COMPONENT: usize = 1;
65    /// `WeekdayComponent`.
66    pub const WEEKDAY_COMPONENT: usize = 2;
67    /// `MonthComponent`.
68    pub const MONTH_COMPONENT: usize = 3;
69    /// `Position`, for `MONTHLY_BY_POSITION` and friends.
70    pub const POSITION: usize = 4;
71    /// `Interval`.
72    pub const INTERVAL: usize = 5;
73    /// `Occurrences`.
74    pub const OCCURRENCES: usize = 6;
75}
76
77/// Whether a period declares work or an exception to it.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum WorkTimeRole {
80    /// From `WorkingTimes`: work happens in this period.
81    Working,
82    /// From `ExceptionTimes`: work does not happen in this period.
83    Exception,
84}
85
86/// How a work period repeats.
87///
88/// `IfcRecurrenceTypeEnum`, verified against IFC4 EXPRESS.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RecurrenceType {
91    /// `.DAILY.`
92    Daily,
93    /// `.WEEKLY.`
94    Weekly,
95    /// `.MONTHLY_BY_DAY_OF_MONTH.`
96    MonthlyByDayOfMonth,
97    /// `.MONTHLY_BY_POSITION.`
98    MonthlyByPosition,
99    /// `.BY_DAY_COUNT.`
100    ByDayCount,
101    /// `.BY_WEEKDAY_COUNT.`
102    ByWeekdayCount,
103    /// `.YEARLY_BY_DAY_OF_MONTH.`
104    YearlyByDayOfMonth,
105    /// `.YEARLY_BY_POSITION.`
106    YearlyByPosition,
107}
108
109impl RecurrenceType {
110    fn parse(token: &str) -> Option<Self> {
111        Some(match token {
112            "DAILY" => Self::Daily,
113            "WEEKLY" => Self::Weekly,
114            "MONTHLY_BY_DAY_OF_MONTH" => Self::MonthlyByDayOfMonth,
115            "MONTHLY_BY_POSITION" => Self::MonthlyByPosition,
116            "BY_DAY_COUNT" => Self::ByDayCount,
117            "BY_WEEKDAY_COUNT" => Self::ByWeekdayCount,
118            "YEARLY_BY_DAY_OF_MONTH" => Self::YearlyByDayOfMonth,
119            "YEARLY_BY_POSITION" => Self::YearlyByPosition,
120            _ => return None,
121        })
122    }
123}
124
125/// A recurrence pattern as authored.
126///
127/// Not expanded into concrete dates: expansion needs a calendar library and a
128/// bounded window, and an unbounded pattern (`Occurrences` absent) has no
129/// finite expansion at all. The stated shape is returned and expansion is left
130/// to a caller who can supply both.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Recurrence {
133    /// The entity.
134    pub id: EntityId,
135    /// How it repeats.
136    pub recurrence_type: Option<RecurrenceType>,
137    /// Weekdays it applies to, 1 = Monday through 7 = Sunday.
138    pub weekdays: Vec<i64>,
139    /// The ordinal position within the period, for positional patterns.
140    ///
141    /// `MONTHLY_BY_POSITION` uses it as "the 2nd Tuesday"; a negative value
142    /// counts from the end of the period.
143    pub position: Option<i64>,
144    /// The interval between occurrences.
145    pub interval: Option<i64>,
146    /// How many times it repeats, if bounded.
147    pub occurrences: Option<i64>,
148}
149
150impl Recurrence {
151    /// Whether the pattern states a finite number of occurrences.
152    ///
153    /// An unbounded pattern is legal and common ("every Monday, forever"), so
154    /// a caller expanding one must impose its own window.
155    #[must_use]
156    pub fn is_bounded(&self) -> bool {
157        self.occurrences.is_some()
158    }
159}
160
161/// One working or exception period.
162#[derive(Debug, Clone, PartialEq)]
163pub struct WorkTime {
164    /// The `IfcWorkTime` entity.
165    pub id: EntityId,
166    /// Whether this declares work or an exception.
167    pub role: WorkTimeRole,
168    /// The period name, as authored.
169    pub name: Option<String>,
170    /// Start date, as authored.
171    pub start: Option<String>,
172    /// Finish date, as authored.
173    pub finish: Option<String>,
174    /// The recurrence pattern, if the period repeats.
175    pub recurrence: Option<Recurrence>,
176}
177
178/// A borrowed view of an `IfcWorkCalendar`.
179#[derive(Debug, Clone, Copy)]
180pub struct WorkCalendar<'m> {
181    id: EntityId,
182    entity: &'m Entity,
183}
184
185impl<'m> WorkCalendar<'m> {
186    /// The entity id in the file.
187    #[must_use]
188    pub fn id(&self) -> EntityId {
189        self.id
190    }
191
192    /// The calendar name.
193    #[must_use]
194    pub fn name(&self) -> Option<&'m str> {
195        self.entity.text(slot::NAME)
196    }
197
198    /// The user-facing identification code.
199    #[must_use]
200    pub fn identification(&self) -> Option<&'m str> {
201        self.entity.text(slot::IDENTIFICATION)
202    }
203
204    /// The predefined type token, without its dots.
205    #[must_use]
206    pub fn predefined_type(&self) -> Option<&'m str> {
207        match self.entity.attribute(slot::PREDEFINED_TYPE)? {
208            Value::Enum(token) => Some(token),
209            _ => None,
210        }
211    }
212
213    /// Periods when work happens.
214    #[must_use]
215    pub fn working_times(&self, model: &Model) -> Vec<WorkTime> {
216        self.times(model, slot::WORKING_TIMES, WorkTimeRole::Working)
217    }
218
219    /// Periods when work does not happen, such as holidays.
220    #[must_use]
221    pub fn exception_times(&self, model: &Model) -> Vec<WorkTime> {
222        self.times(model, slot::EXCEPTION_TIMES, WorkTimeRole::Exception)
223    }
224
225    fn times(&self, model: &Model, slot: usize, role: WorkTimeRole) -> Vec<WorkTime> {
226        let mut refs = Vec::new();
227        if let Some(v) = self.entity.attribute(slot) {
228            v.for_each_ref(&mut |id| refs.push(id));
229        }
230        refs.into_iter()
231            .filter_map(|id| read_work_time(model, id, role))
232            .collect()
233    }
234}
235
236fn read_work_time(model: &Model, id: EntityId, role: WorkTimeRole) -> Option<WorkTime> {
237    let entity = model.get(id)?;
238    if !entity.type_name.eq_ignore_ascii_case("IFCWORKTIME") {
239        return None;
240    }
241    let recurrence = match entity.attribute(work_time_slot::RECURRENCE_PATTERN) {
242        Some(Value::Ref(pattern)) => read_recurrence(model, *pattern),
243        _ => None,
244    };
245    Some(WorkTime {
246        id,
247        role,
248        name: entity.text(work_time_slot::NAME).map(str::to_string),
249        start: entity.text(work_time_slot::START).map(str::to_string),
250        finish: entity.text(work_time_slot::FINISH).map(str::to_string),
251        recurrence,
252    })
253}
254
255fn read_recurrence(model: &Model, id: EntityId) -> Option<Recurrence> {
256    let entity = model.get(id)?;
257    if !entity
258        .type_name
259        .eq_ignore_ascii_case("IFCRECURRENCEPATTERN")
260    {
261        return None;
262    }
263    let recurrence_type = match entity.attribute(recurrence_slot::RECURRENCE_TYPE) {
264        Some(Value::Enum(token)) => RecurrenceType::parse(token),
265        _ => None,
266    };
267    let mut weekdays = Vec::new();
268    if let Some(Value::List(items)) = entity.attribute(recurrence_slot::WEEKDAY_COMPONENT) {
269        for item in items {
270            if let Some(day) = item.unwrap_typed().as_i64() {
271                weekdays.push(day);
272            }
273        }
274    }
275    Some(Recurrence {
276        id,
277        recurrence_type,
278        weekdays,
279        position: entity
280            .attribute(recurrence_slot::POSITION)
281            .and_then(|v| v.unwrap_typed().as_i64()),
282        interval: entity
283            .attribute(recurrence_slot::INTERVAL)
284            .and_then(|v| v.unwrap_typed().as_i64()),
285        occurrences: entity
286            .attribute(recurrence_slot::OCCURRENCES)
287            .and_then(|v| v.unwrap_typed().as_i64()),
288    })
289}
290
291/// Every work calendar in the model, in file order.
292#[must_use]
293pub fn work_calendars(model: &Model) -> Vec<WorkCalendar<'_>> {
294    model
295        .of_type("IFCWORKCALENDAR")
296        .map(|(id, entity)| WorkCalendar { id, entity })
297        .collect()
298}