use crate::error::XmlError;
use ifc_model::{EntityId, Value};
pub(crate) fn infer(text: &str) -> Value {
if let Some(id) = parse_ref(text) {
return Value::Ref(id);
}
if let Ok(integer) = text.parse::<i64>() {
return Value::Integer(integer);
}
if looks_real(text) {
if let Ok(real) = text.parse::<f64>() {
if real.is_finite() {
return Value::Real(real);
}
}
}
Value::Text(text.into())
}
pub(crate) fn attribute_text(value: &Value) -> Option<String> {
let text = match value {
Value::Text(text) => text.to_string(),
Value::Integer(integer) => integer.to_string(),
Value::Real(real) => format_real(*real),
Value::Ref(id) => format_ref(*id),
Value::Null | Value::Derived | Value::Enum(_) | Value::Binary(_) => return None,
Value::Bool(_) | Value::LogicalUnknown | Value::List(_) | Value::Typed { .. } => {
return None;
}
};
same_scalar(&infer(&text), value).then_some(text)
}
pub(crate) fn element_form(value: &Value) -> Option<(&'static str, String)> {
let form = match value {
Value::Text(text) => ("string", text.to_string()),
Value::Integer(integer) => ("integer", integer.to_string()),
Value::Real(real) => ("real", format_real(*real)),
Value::Ref(id) => ("ref", format_ref(*id)),
Value::Enum(text) => ("enum", text.to_string()),
Value::Binary(text) => ("binary", text.to_string()),
Value::Bool(true) => ("logical", "true".into()),
Value::Bool(false) => ("logical", "false".into()),
Value::LogicalUnknown => ("logical", "unknown".into()),
Value::Null | Value::Derived | Value::List(_) | Value::Typed { .. } => return None,
};
Some(form)
}
pub(crate) fn decode_element(kind: &str, text: &str) -> Result<Option<Value>, XmlError> {
let value = match kind {
"list" | "typed" => return Ok(None),
"string" | "" => Value::Text(text.into()),
"enum" => Value::Enum(text.into()),
"binary" => Value::Binary(text.into()),
"logical" => match text {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
"unknown" => Value::LogicalUnknown,
_ => return Err(invalid_scalar("logical", text)),
},
"integer" => Value::Integer(text.parse().map_err(|_| invalid_scalar("integer", text))?),
"real" => {
let real: f64 = text.parse().map_err(|_| invalid_scalar("real", text))?;
if !real.is_finite() {
return Err(invalid_scalar("real", text));
}
Value::Real(real)
}
"ref" => Value::Ref(parse_ref(text).ok_or_else(|| invalid_scalar("ref", text))?),
kind => return Err(XmlError::UnknownKind(kind.into())),
};
Ok(Some(value))
}
pub(crate) fn parse_ref(text: &str) -> Option<EntityId> {
let number: u64 = text.trim().strip_prefix('i')?.parse().ok()?;
Some(EntityId(number))
}
pub(crate) fn format_ref(id: EntityId) -> String {
format!("i{}", id.0)
}
pub(crate) fn format_real(real: f64) -> String {
if real == real.trunc() && real.is_finite() && real.abs() < 1e15 {
format!("{real:.1}")
} else {
let text = format!("{real}");
if looks_real(&text) {
text
} else {
format!("{text}.0")
}
}
}
fn invalid_scalar(kind: &str, value: &str) -> XmlError {
XmlError::InvalidScalar {
kind: kind.into(),
value: value.into(),
}
}
fn looks_real(text: &str) -> bool {
text.contains('.') || text.contains('e') || text.contains('E')
}
fn same_scalar(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Real(left), Value::Real(right)) => left.to_bits() == right.to_bits(),
(left, right) => left == right,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attribute_text_is_refused_exactly_where_inference_changes_the_value() {
for text in ["0.1", "42", "i7", " i7", "1e5", "+5", "-0"] {
assert_eq!(attribute_text(&Value::Text(text.into())), None, "{text:?}");
}
for text in ["", "inf", "NaN", "1e999", "i", "i-1", "0x10", "plain"] {
assert_eq!(
attribute_text(&Value::Text(text.into())).as_deref(),
Some(text),
"{text:?}"
);
}
}
#[test]
fn inference_never_yields_a_non_finite_real() {
for text in ["1e999", "-1e999", "inf", "-inf", "NaN", "infinity"] {
assert_eq!(infer(text), Value::Text(text.into()), "{text:?}");
}
}
#[test]
fn logical_elements_reject_unrecognised_text() {
assert!(decode_element("logical", "maybe").is_err());
assert_eq!(
decode_element("logical", "unknown").unwrap(),
Some(Value::LogicalUnknown)
);
}
}