Skip to main content

ifc_schedule/authoring/
recurrence.rs

1//! Authoring `IfcRecurrencePattern` (#233).
2//!
3//! IFC4 ADD2 TC1 and IFC4X3 ADD2 both declare eight attributes:
4//!
5//! ```text
6//! 0 RecurrenceType  1 DayComponent    2 WeekdayComponent
7//! 3 MonthComponent  4 Position        5 Interval
8//! 6 Occurrences     7 TimePeriods (OPTIONAL LIST [1:?] OF IfcTimePeriod)
9//! ```
10//!
11//! Every record is written with all eight, `TimePeriods` included, so an
12//! authored pattern is never a short record. IFC2X3 declares neither
13//! `IfcRecurrencePattern` nor `IfcTimePeriod`; [`create_recurrence_pattern_in`]
14//! refuses it with `EntityNotInSchema`.
15
16use ifc_model::{Entity, EntityId, Model, Transaction, Value};
17
18use super::ScheduleAuthoringResult;
19use crate::calendar::recurrence_slot;
20use crate::error::ScheduleAuthoringError;
21
22const ENTITY: &str = "IFCRECURRENCEPATTERN";
23const TIME_PERIOD: &str = "IFCTIMEPERIOD";
24
25/// Authored fields for `IfcRecurrencePattern`.
26///
27/// Weekday and month components are 1-based in the schema: 1 = Monday
28/// through 7 = Sunday, and 1 = January through 12 = December. Out-of-range
29/// values are refused rather than written, because a reader has no way to
30/// tell a 0-based authoring mistake from a deliberate value.
31#[derive(Debug, Clone, Default)]
32#[non_exhaustive]
33pub struct RecurrenceDraft<'a> {
34    /// `RecurrenceType`. Required by the schema.
35    pub recurrence_type: &'a str,
36    /// `DayComponent`: days of the month, 1..=31.
37    pub days: Vec<i64>,
38    /// `WeekdayComponent`: 1 = Monday through 7 = Sunday.
39    pub weekdays: Vec<i64>,
40    /// `MonthComponent`: 1 = January through 12 = December.
41    pub months: Vec<i64>,
42    /// `Position`, for positional patterns. Negative counts from the end.
43    pub position: Option<i64>,
44    /// `Interval`: repeat every n periods. Must be positive.
45    pub interval: Option<i64>,
46    /// `Occurrences`: how many times it repeats. Must be positive.
47    pub occurrences: Option<i64>,
48    /// `TimePeriods`: the `IfcTimePeriod` records (from
49    /// [`create_time_period`](super::create_time_period)) each occurrence
50    /// spans, in authored order. Empty writes `$` (#233).
51    pub time_periods: Vec<EntityId>,
52}
53
54/// Stage an `IfcRecurrencePattern` with all eight IFC4/IFC4X3 attributes.
55///
56/// Refuses component values outside the schema's 1-based ranges, and a
57/// non-positive interval or occurrence count: "every 0 weeks" and "repeats
58/// -1 times" both parse and both describe nothing.
59///
60/// Takes no model, so it cannot see the model's release or resolve the
61/// `time_periods` references: it is for IFC4 and IFC4X3 only and writes
62/// the references as given. [`create_recurrence_pattern_in`] checks both.
63///
64/// # Errors
65///
66/// `InvalidValue` for a blank type, an out-of-range component or a
67/// non-positive count. Nothing is staged on an error.
68pub fn create_recurrence_pattern(
69    tx: &mut Transaction,
70    draft: &RecurrenceDraft<'_>,
71) -> ScheduleAuthoringResult<EntityId> {
72    check_draft(draft)?;
73    let mut attributes = vec![Value::Null; recurrence_slot::TIME_PERIODS + 1];
74    attributes[recurrence_slot::RECURRENCE_TYPE] = Value::Enum(draft.recurrence_type.into());
75    attributes[recurrence_slot::DAY_COMPONENT] = integer_list(&draft.days);
76    attributes[recurrence_slot::WEEKDAY_COMPONENT] = integer_list(&draft.weekdays);
77    attributes[recurrence_slot::MONTH_COMPONENT] = integer_list(&draft.months);
78    attributes[recurrence_slot::POSITION] = draft.position.map_or(Value::Null, Value::Integer);
79    attributes[recurrence_slot::INTERVAL] = draft.interval.map_or(Value::Null, Value::Integer);
80    attributes[recurrence_slot::OCCURRENCES] =
81        draft.occurrences.map_or(Value::Null, Value::Integer);
82    attributes[recurrence_slot::TIME_PERIODS] = if draft.time_periods.is_empty() {
83        Value::Null
84    } else {
85        Value::List(draft.time_periods.iter().copied().map(Value::Ref).collect())
86    };
87    Ok(tx.create(Entity::new(ENTITY, attributes)))
88}
89
90/// [`create_recurrence_pattern`] bound to `model`'s declared release (#233).
91///
92/// IFC4 and IFC4X3 stage exactly what [`create_recurrence_pattern`] stages,
93/// once every `time_periods` reference resolves, in the model or staged on
94/// `tx`, to an `IfcTimePeriod`. IFC2X3 declares no `IfcRecurrencePattern`
95/// and is refused.
96///
97/// # Errors
98///
99/// `EntityNotInSchema` for a release without `IfcRecurrencePattern`,
100/// `MultipleSchemas` or `UnsupportedSchema` when the header binds no single
101/// known release, `MissingReference` or `WrongReferenceType` for a time
102/// period that is absent or not an `IfcTimePeriod`, and those of
103/// [`create_recurrence_pattern`]. Nothing is staged on an error.
104pub fn create_recurrence_pattern_in(
105    tx: &mut Transaction,
106    model: &Model,
107    draft: &RecurrenceDraft<'_>,
108) -> ScheduleAuthoringResult<EntityId> {
109    crate::release::bind(model)?.require_entity(ENTITY)?;
110    check_draft(draft)?;
111    for &target in &draft.time_periods {
112        let actual = crate::release::projected_type(tx, model, target).ok_or(
113            ScheduleAuthoringError::MissingReference {
114                entity: ENTITY,
115                attribute: "TimePeriods",
116                target,
117            },
118        )?;
119        if !actual.eq_ignore_ascii_case(TIME_PERIOD) {
120            return Err(ScheduleAuthoringError::WrongReferenceType {
121                entity: ENTITY,
122                attribute: "TimePeriods",
123                target,
124                actual,
125                expected: TIME_PERIOD,
126            });
127        }
128    }
129    create_recurrence_pattern(tx, draft)
130}
131
132fn check_draft(draft: &RecurrenceDraft<'_>) -> ScheduleAuthoringResult<()> {
133    if draft.recurrence_type.trim().is_empty() {
134        return Err(ScheduleAuthoringError::InvalidValue {
135            entity: ENTITY,
136            attribute: "RecurrenceType",
137            expected: "a non-empty IfcRecurrenceTypeEnum value",
138        });
139    }
140    check_range(&draft.days, 1, 31, "DayComponent")?;
141    check_range(&draft.weekdays, 1, 7, "WeekdayComponent")?;
142    check_range(&draft.months, 1, 12, "MonthComponent")?;
143    check_positive(draft.interval, "Interval")?;
144    check_positive(draft.occurrences, "Occurrences")
145}
146
147/// Refuse component values outside the schema's inclusive range.
148fn check_range(
149    values: &[i64],
150    low: i64,
151    high: i64,
152    attribute: &'static str,
153) -> ScheduleAuthoringResult<()> {
154    if values.iter().any(|v| !(low..=high).contains(v)) {
155        return Err(ScheduleAuthoringError::InvalidValue {
156            entity: ENTITY,
157            attribute,
158            expected: "components within the schema's 1-based range",
159        });
160    }
161    Ok(())
162}
163
164/// Refuse a stated count that is zero or negative.
165fn check_positive(value: Option<i64>, attribute: &'static str) -> ScheduleAuthoringResult<()> {
166    if value.is_some_and(|v| v <= 0) {
167        return Err(ScheduleAuthoringError::InvalidValue {
168            entity: ENTITY,
169            attribute,
170            expected: "a positive count",
171        });
172    }
173    Ok(())
174}
175
176/// An omitted list stays `Null` rather than becoming an empty aggregate.
177fn integer_list(values: &[i64]) -> Value {
178    if values.is_empty() {
179        return Value::Null;
180    }
181    Value::List(values.iter().copied().map(Value::Integer).collect())
182}