ifc_schedule/authoring/
recurrence.rs1use 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#[derive(Debug, Clone, Default)]
32#[non_exhaustive]
33pub struct RecurrenceDraft<'a> {
34 pub recurrence_type: &'a str,
36 pub days: Vec<i64>,
38 pub weekdays: Vec<i64>,
40 pub months: Vec<i64>,
42 pub position: Option<i64>,
44 pub interval: Option<i64>,
46 pub occurrences: Option<i64>,
48 pub time_periods: Vec<EntityId>,
52}
53
54pub 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
90pub 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
147fn 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
164fn 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
176fn 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}