use ifc_model::{Entity, EntityId, Transaction, Value};
use super::CostAuthoringError;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum DateTimeValue<'a> {
Text(&'a str),
Date(CalendarDate),
Time(LocalTime),
DateAndTime(CalendarDate, LocalTime),
}
impl Default for DateTimeValue<'_> {
fn default() -> Self {
Self::Text("")
}
}
impl<'a> From<&'a str> for DateTimeValue<'a> {
fn from(text: &'a str) -> Self {
Self::Text(text)
}
}
impl From<CalendarDate> for DateTimeValue<'_> {
fn from(date: CalendarDate) -> Self {
Self::Date(date)
}
}
impl From<LocalTime> for DateTimeValue<'_> {
fn from(time: LocalTime) -> Self {
Self::Time(time)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CalendarDate {
pub year: i64,
pub month: i64,
pub day: i64,
}
impl CalendarDate {
#[must_use]
pub const fn new(year: i64, month: i64, day: i64) -> Self {
Self { year, month, day }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct LocalTime {
pub hour: i64,
pub minute: Option<i64>,
pub second: Option<f64>,
}
impl LocalTime {
#[must_use]
pub const fn new(hour: i64) -> Self {
Self {
hour,
minute: None,
second: None,
}
}
#[must_use]
pub const fn minute(mut self, value: i64) -> Self {
self.minute = Some(value);
self
}
#[must_use]
pub const fn second(mut self, value: f64) -> Self {
self.second = Some(value);
self
}
}
fn invalid(
entity: &'static str,
attribute: &'static str,
expected: &'static str,
) -> CostAuthoringError {
CostAuthoringError::InvalidValue {
entity,
attribute,
reason: format!("expected {expected}"),
}
}
fn leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
impl CalendarDate {
fn check(self) -> Result<(), CostAuthoringError> {
const ENTITY: &str = "IFCCALENDARDATE";
if !(1..=12).contains(&self.month) {
return Err(invalid(ENTITY, "MonthComponent", "a month in 1..=12"));
}
let last = match self.month {
4 | 6 | 9 | 11 => 30,
2 if leap_year(self.year) => 29,
2 => 28,
_ => 31,
};
if !(1..=last).contains(&self.day) {
return Err(invalid(
ENTITY,
"DayComponent",
"a day that exists in the month (IfcValidCalendarDate)",
));
}
Ok(())
}
fn stage(self, tx: &mut Transaction) -> EntityId {
tx.create(Entity::new(
"IFCCALENDARDATE",
vec![
Value::Integer(self.day),
Value::Integer(self.month),
Value::Integer(self.year),
],
))
}
}
impl LocalTime {
fn check(self) -> Result<(), CostAuthoringError> {
const ENTITY: &str = "IFCLOCALTIME";
if !(0..24).contains(&self.hour) {
return Err(invalid(ENTITY, "HourComponent", "an hour in 0..24"));
}
if self.minute.is_some_and(|m| !(0..=59).contains(&m)) {
return Err(invalid(ENTITY, "MinuteComponent", "a minute in 0..=59"));
}
if let Some(second) = self.second {
if !second.is_finite() || !(0.0..60.0).contains(&second) {
return Err(invalid(
ENTITY,
"SecondComponent",
"a finite second in 0.0..60.0",
));
}
if self.minute.is_none() {
return Err(invalid(
ENTITY,
"MinuteComponent",
"a minute when a second is given (IfcValidTime)",
));
}
}
Ok(())
}
fn stage(self, tx: &mut Transaction) -> EntityId {
tx.create(Entity::new(
"IFCLOCALTIME",
vec![
Value::Integer(self.hour),
self.minute.map_or(Value::Null, Value::Integer),
self.second.map_or(Value::Null, Value::Real),
Value::Null,
Value::Null,
],
))
}
}
impl<'a> DateTimeValue<'a> {
pub(super) fn check(self) -> Result<(), CostAuthoringError> {
match self {
Self::Text(_) => Ok(()),
Self::Date(date) => date.check(),
Self::Time(time) => time.check(),
Self::DateAndTime(date, time) => {
date.check()?;
time.check()
}
}
}
pub(super) const fn record_type(self) -> Option<&'static str> {
match self {
Self::Text(_) => None,
Self::Date(_) => Some("IFCCALENDARDATE"),
Self::Time(_) => Some("IFCLOCALTIME"),
Self::DateAndTime(..) => Some("IFCDATEANDTIME"),
}
}
pub(super) fn provisional(self, placeholder: EntityId) -> Value {
match self {
Self::Text(text) => Value::Text(text.into()),
_ => Value::Ref(placeholder),
}
}
pub(super) fn stage(self, tx: &mut Transaction) -> Option<EntityId> {
match self {
Self::Text(_) => None,
Self::Date(date) => Some(date.stage(tx)),
Self::Time(time) => Some(time.stage(tx)),
Self::DateAndTime(date, time) => {
let date = date.stage(tx);
let time = time.stage(tx);
Some(tx.create(Entity::new(
"IFCDATEANDTIME",
vec![Value::Ref(date), Value::Ref(time)],
)))
}
}
}
}
pub(super) const PLACEHOLDERS: [EntityId; 3] = [
EntityId(u64::MAX),
EntityId(u64::MAX - 1),
EntityId(u64::MAX - 2),
];
pub(super) fn patch(
tx: &mut Transaction,
record: &mut Entity,
dates: &[Option<DateTimeValue<'_>>],
) {
for (date, placeholder) in dates.iter().zip(PLACEHOLDERS) {
let Some(staged) = date.and_then(|date| date.stage(tx)) else {
continue;
};
for slot in &mut record.attributes {
if *slot == Value::Ref(placeholder) {
*slot = Value::Ref(staged);
}
}
}
}