feat(engine): establish calcpad-engine workspace with Epic 1 modules
Cherry-picked and integrated the best code from 105 parallel epic branches into a clean workspace structure: - calcpad-engine/: Core Rust crate with lexer, parser, AST, interpreter, types, FFI (C ABI), pipeline, error handling, span tracking, eval context (from epic/1-5 base) - calcpad-engine/src/number.rs: Arbitrary precision arithmetic via dashu crate, WASM-compatible, exact decimals (from epic/1-4) - calcpad-engine/src/sheet_context.rs: SheetContext with dependency graph, dirty tracking, circular detection, cheap clone (from epic/1-8) - calcpad-wasm/: Thin WASM wrapper crate via wasm-bindgen, delegates to calcpad-engine (from epic/1-6) - Updated .gitignore for target/, node_modules/, build artifacts - Fixed run-pipeline.sh for macOS compat and CalcPad phases 79 tests passing across workspace.
This commit is contained in:
214
calcpad-engine/src/types.rs
Normal file
214
calcpad-engine/src/types.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
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,
|
||||
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::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<Span> 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<f64>,
|
||||
}
|
||||
|
||||
/// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
fn format_number(val: f64) -> String {
|
||||
if val == val.floor() && val.abs() < 1e15 {
|
||||
format!("{}", val as i64)
|
||||
} else {
|
||||
format!("{}", val)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user