use crate::span::Span; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use std::fmt; /// The result type tag for metadata purposes. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ResultType { Number, UnitValue, CurrencyValue, DateTime, TimeDelta, Boolean, Empty, NonCalculable, Error, } impl fmt::Display for ResultType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ResultType::Number => write!(f, "Number"), ResultType::UnitValue => write!(f, "UnitValue"), ResultType::CurrencyValue => write!(f, "CurrencyValue"), ResultType::DateTime => write!(f, "DateTime"), ResultType::TimeDelta => write!(f, "TimeDelta"), ResultType::Boolean => write!(f, "Boolean"), ResultType::Empty => write!(f, "Empty"), ResultType::NonCalculable => write!(f, "NonCalculable"), ResultType::Error => write!(f, "Error"), } } } /// Serializable span for JSON output. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SerializableSpan { pub start: usize, pub end: usize, } impl From for SerializableSpan { fn from(s: Span) -> Self { SerializableSpan { start: s.start, end: s.end, } } } /// Metadata attached to every evaluation result. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ResultMetadata { /// The source span of the expression that produced this result. pub span: SerializableSpan, /// The type tag of the result. pub result_type: ResultType, /// A display-formatted string of the result. pub display: String, /// The raw numeric value, if applicable. pub raw_value: Option, } /// The value payload of a calculation result. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum CalcValue { Number { value: f64 }, UnitValue { value: f64, unit: String }, CurrencyValue { amount: f64, currency: String }, DateTime { date: String }, TimeDelta { days: i64, description: String }, Boolean { value: bool }, Error { message: String, span: SerializableSpan }, } /// A complete calculation result: value + metadata. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CalcResult { pub value: CalcValue, pub metadata: ResultMetadata, } impl CalcResult { pub fn number(val: f64, span: Span) -> Self { let display = format_number(val); CalcResult { value: CalcValue::Number { value: val }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::Number, display, raw_value: Some(val), }, } } pub fn unit_value(val: f64, unit: &str, span: Span) -> Self { let display = format!("{} {}", format_number(val), unit); CalcResult { value: CalcValue::UnitValue { value: val, unit: unit.to_string(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::UnitValue, display, raw_value: Some(val), }, } } pub fn currency_value(amount: f64, currency: &str, span: Span) -> Self { let display = format_currency(amount, currency); CalcResult { value: CalcValue::CurrencyValue { amount, currency: currency.to_string(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::CurrencyValue, display, raw_value: Some(amount), }, } } pub fn datetime(date: NaiveDate, span: Span) -> Self { let display = date.format("%Y-%m-%d").to_string(); CalcResult { value: CalcValue::DateTime { date: display.clone(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::DateTime, display, raw_value: None, }, } } pub fn time_delta(days: i64, description: &str, span: Span) -> Self { let display = description.to_string(); CalcResult { value: CalcValue::TimeDelta { days, description: description.to_string(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::TimeDelta, display, raw_value: Some(days as f64), }, } } pub fn boolean(val: bool, span: Span) -> Self { let display = val.to_string(); CalcResult { value: CalcValue::Boolean { value: val }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::Boolean, display, raw_value: None, }, } } pub fn empty(span: Span) -> Self { CalcResult { value: CalcValue::Error { message: "empty input".to_string(), span: span.into(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::Empty, display: String::new(), raw_value: None, }, } } pub fn non_calculable(span: Span) -> Self { CalcResult { value: CalcValue::Error { message: "no expression found".to_string(), span: span.into(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::NonCalculable, display: String::new(), raw_value: None, }, } } pub fn error(message: &str, span: Span) -> Self { CalcResult { value: CalcValue::Error { message: message.to_string(), span: span.into(), }, metadata: ResultMetadata { span: span.into(), result_type: ResultType::Error, display: format!("Error: {}", message), raw_value: None, }, } } pub fn result_type(&self) -> ResultType { self.metadata.result_type } /// Returns true if this result is a computed value (not empty, non-calculable, or error). pub fn is_calculable(&self) -> bool { !matches!( self.metadata.result_type, ResultType::Empty | ResultType::NonCalculable | ResultType::Error ) } } fn format_number(val: f64) -> String { if val == val.floor() && val.abs() < 1e15 { format!("{}", val as i64) } else { // Round to 10 significant figures to avoid f64 noise let abs = val.abs(); let magnitude = if abs > 0.0 { abs.log10().floor() as i32 } else { 0 }; let precision = (10 - magnitude - 1).max(0) as usize; let formatted = format!("{:.prec$}", val, prec = precision); // Strip trailing zeros after decimal point if formatted.contains('.') { formatted .trim_end_matches('0') .trim_end_matches('.') .to_string() } else { formatted } } } fn format_currency(amount: f64, currency: &str) -> String { let symbol = match currency { "USD" => "$", "EUR" => "€", "GBP" => "£", "JPY" => "¥", "BRL" => "R$", _ => "", }; if symbol.is_empty() { format!("{:.2} {}", amount, currency) } else { format!("{}{:.2}", symbol, amount) } }