use crate::error::PdfError;
use crate::forms::calculations::{CalculationEngine, FieldValue};
use crate::objects::{Dictionary, Object};
use chrono::{DateTime, NaiveDate, Utc};
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
use std::fmt;
#[derive(Debug, Clone)]
pub struct FormCalculationSystem {
engine: CalculationEngine,
js_calculations: HashMap<String, JavaScriptCalculation>,
field_formats: HashMap<String, FieldFormat>,
events: Vec<CalculationEvent>,
settings: CalculationSettings,
}
#[derive(Debug, Clone)]
pub enum JavaScriptCalculation {
SimpleCalculate {
operation: SimpleOperation,
fields: Vec<String>,
},
PercentCalculate {
base_field: String,
percent_field: String,
mode: PercentMode,
},
DateCalculate {
start_date_field: String,
days_field: Option<String>,
format: String,
},
RangeCalculate {
field: String,
min: Option<f64>,
max: Option<f64>,
},
NumberCalculate {
field: String,
decimals: usize,
sep_style: SeparatorStyle,
currency: Option<String>,
},
Custom {
script: String,
dependencies: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SimpleOperation {
Sum, Product, Average, Minimum, Maximum, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PercentMode {
PercentOf,
PercentageOf,
AddPercent,
SubtractPercent,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SeparatorStyle {
CommaPeriod,
PeriodComma,
SpacePeriod,
ApostrophePeriod,
None,
}
#[derive(Debug, Clone)]
pub enum FieldFormat {
Number {
decimals: usize,
separator: SeparatorStyle,
negative_style: NegativeStyle,
currency: Option<String>,
},
Percent { decimals: usize },
Date { format: String },
Time { format: String },
Special { format_type: SpecialFormat },
Custom { format_string: String },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NegativeStyle {
MinusBlack, RedParentheses, BlackParentheses, MinusRed, }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpecialFormat {
ZipCode, ZipCodePlus4, PhoneNumber, SSN, }
#[derive(Debug, Clone)]
pub struct CalculationEvent {
timestamp: DateTime<Utc>,
field: String,
event_type: EventType,
old_value: Option<FieldValue>,
new_value: Option<FieldValue>,
}
impl CalculationEvent {
pub fn timestamp(&self) -> &DateTime<Utc> {
&self.timestamp
}
pub fn field(&self) -> &str {
&self.field
}
pub fn event_type(&self) -> &EventType {
&self.event_type
}
pub fn old_value(&self) -> Option<&FieldValue> {
self.old_value.as_ref()
}
pub fn new_value(&self) -> Option<&FieldValue> {
self.new_value.as_ref()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EventType {
ValueChanged,
CalculationTriggered,
ValidationFailed,
FormatApplied,
DependencyUpdated,
}
#[derive(Debug, Clone)]
pub struct CalculationSettings {
pub auto_recalculate: bool,
pub max_depth: usize,
pub log_events: bool,
pub decimal_precision: usize,
}
impl Default for CalculationSettings {
fn default() -> Self {
Self {
auto_recalculate: true,
max_depth: 100,
log_events: true,
decimal_precision: 2,
}
}
}
impl Default for FormCalculationSystem {
fn default() -> Self {
Self {
engine: CalculationEngine::new(),
js_calculations: HashMap::new(),
field_formats: HashMap::new(),
events: Vec::new(),
settings: CalculationSettings::default(),
}
}
}
impl FormCalculationSystem {
pub fn new() -> Self {
Self::default()
}
pub fn with_settings(settings: CalculationSettings) -> Self {
Self {
settings,
..Self::default()
}
}
pub fn get_field_value(&self, field_name: &str) -> Option<&FieldValue> {
self.engine.get_field_value(field_name)
}
pub fn settings(&self) -> &CalculationSettings {
&self.settings
}
pub fn set_field_value(
&mut self,
field_name: impl Into<String>,
value: FieldValue,
) -> Result<(), PdfError> {
let field_name = field_name.into();
validate_calculation_value(&value)?;
let order = if self.settings.auto_recalculate {
self.recalculation_order(&field_name)?
} else {
Vec::new()
};
let previous_engine = (!order.is_empty()).then(|| self.engine.clone());
let previous_events = self.events.len();
if self.settings.log_events {
let old_value = self.engine.get_field_value(&field_name).cloned();
self.events.push(CalculationEvent {
timestamp: Utc::now(),
field: field_name.clone(),
event_type: EventType::ValueChanged,
old_value,
new_value: Some(value.clone()),
});
}
self.engine.set_field_value(field_name.clone(), value);
for field in order {
if let Err(error) = self.calculate_js_field(&field) {
if let Some(previous_engine) = previous_engine {
self.engine = previous_engine;
}
self.events.truncate(previous_events);
return Err(error);
}
}
Ok(())
}
pub fn add_js_calculation(
&mut self,
field_name: impl Into<String>,
calculation: JavaScriptCalculation,
) -> Result<(), PdfError> {
let field_name = field_name.into();
let dependencies = self.extract_js_dependencies(&calculation);
if self.would_create_cycle(&field_name, &dependencies) {
return Err(PdfError::InvalidStructure(format!(
"Circular dependency detected for field '{}'",
field_name
)));
}
let previous_engine = self.engine.clone();
let previous_events = self.events.len();
let previous = self.js_calculations.insert(field_name.clone(), calculation);
let result = self.calculate_js_field(&field_name).and_then(|()| {
if self.settings.auto_recalculate {
self.recalculate_js_fields(&field_name)
} else {
Ok(())
}
});
if let Err(error) = result {
match previous {
Some(calculation) => {
self.js_calculations.insert(field_name, calculation);
}
None => {
self.js_calculations.remove(&field_name);
}
}
self.engine = previous_engine;
self.events.truncate(previous_events);
return Err(error);
}
Ok(())
}
fn extract_js_dependencies(&self, calc: &JavaScriptCalculation) -> HashSet<String> {
let mut deps = HashSet::new();
match calc {
JavaScriptCalculation::SimpleCalculate { fields, .. } => {
deps.extend(fields.iter().cloned());
}
JavaScriptCalculation::PercentCalculate {
base_field,
percent_field,
..
} => {
deps.insert(base_field.clone());
deps.insert(percent_field.clone());
}
JavaScriptCalculation::DateCalculate {
start_date_field,
days_field,
..
} => {
deps.insert(start_date_field.clone());
if let Some(df) = days_field {
deps.insert(df.clone());
}
}
JavaScriptCalculation::RangeCalculate { field, .. } => {
deps.insert(field.clone());
}
JavaScriptCalculation::NumberCalculate { field, .. } => {
deps.insert(field.clone());
}
JavaScriptCalculation::Custom { dependencies, .. } => {
deps.extend(dependencies.iter().cloned());
}
}
deps
}
fn would_create_cycle(&self, field: &str, new_deps: &HashSet<String>) -> bool {
for dep in new_deps {
if dep == field {
return true; }
if self.depends_on(dep, field) {
return true;
}
}
false
}
fn depends_on(&self, field_a: &str, field_b: &str) -> bool {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(field_a.to_string());
while let Some(current) = queue.pop_front() {
if current == field_b {
return true;
}
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
if let Some(calc) = self.js_calculations.get(¤t) {
let deps = self.extract_js_dependencies(calc);
for dep in deps {
queue.push_back(dep);
}
}
}
false
}
fn calculate_js_field(&mut self, field_name: &str) -> Result<(), PdfError> {
if let Some(calculation) = self.js_calculations.get(field_name).cloned() {
let value = self.evaluate_js_calculation(&calculation)?;
validate_calculation_value(&value)?;
let formatted_value = if let Some(format) = self.field_formats.get(field_name) {
self.apply_format(value, format)?
} else {
value
};
validate_calculation_value(&formatted_value)?;
let old_value = self.engine.get_field_value(field_name).cloned();
self.engine.set_field_value(field_name, formatted_value);
if self.settings.log_events {
self.events.push(CalculationEvent {
timestamp: Utc::now(),
field: field_name.to_string(),
event_type: EventType::CalculationTriggered,
old_value,
new_value: self.engine.get_field_value(field_name).cloned(),
});
}
}
Ok(())
}
fn evaluate_js_calculation(
&self,
calc: &JavaScriptCalculation,
) -> Result<FieldValue, PdfError> {
match calc {
JavaScriptCalculation::SimpleCalculate { operation, fields } => {
let values: Vec<f64> = fields
.iter()
.filter_map(|f| self.engine.get_field_value(f))
.map(|v| v.to_number())
.collect();
if values.is_empty() {
return Ok(FieldValue::Number(0.0));
}
let result = match operation {
SimpleOperation::Sum => values.iter().sum(),
SimpleOperation::Product => values.iter().product(),
SimpleOperation::Average => values.iter().sum::<f64>() / values.len() as f64,
SimpleOperation::Minimum => {
values.iter().cloned().fold(f64::INFINITY, f64::min)
}
SimpleOperation::Maximum => {
values.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
}
};
Ok(FieldValue::Number(result))
}
JavaScriptCalculation::PercentCalculate {
base_field,
percent_field,
mode,
} => {
let base = self
.engine
.get_field_value(base_field)
.map(|v| v.to_number())
.unwrap_or(0.0);
let percent = self
.engine
.get_field_value(percent_field)
.map(|v| v.to_number())
.unwrap_or(0.0);
let result = match mode {
PercentMode::PercentOf => base * (percent / 100.0),
PercentMode::PercentageOf => {
if base != 0.0 {
(percent / base) * 100.0
} else {
0.0
}
}
PercentMode::AddPercent => base * (1.0 + percent / 100.0),
PercentMode::SubtractPercent => base * (1.0 - percent / 100.0),
};
Ok(FieldValue::Number(result))
}
JavaScriptCalculation::DateCalculate {
start_date_field,
days_field,
format: _,
} => {
let start_date_str = self
.engine
.get_field_value(start_date_field)
.map(|v| v.to_string())
.unwrap_or_default();
if let Ok(date) = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d") {
let days = if let Some(df) = days_field {
self.engine
.get_field_value(df)
.map(|v| v.to_number() as i64)
.unwrap_or(0)
} else {
0
};
let duration = chrono::Duration::try_days(days).ok_or_else(|| {
PdfError::InvalidOperation("date offset exceeds supported range".into())
})?;
let result_date = date.checked_add_signed(duration).ok_or_else(|| {
PdfError::InvalidOperation("calculated date exceeds supported range".into())
})?;
Ok(FieldValue::Text(result_date.format("%Y-%m-%d").to_string()))
} else {
Ok(FieldValue::Text(String::new()))
}
}
JavaScriptCalculation::RangeCalculate { field, min, max } => {
let value = self
.engine
.get_field_value(field)
.map(|v| v.to_number())
.unwrap_or(0.0);
if min.is_some_and(|v| !v.is_finite())
|| max.is_some_and(|v| !v.is_finite())
|| matches!((min, max), (Some(lo), Some(hi)) if lo > hi)
{
return Err(PdfError::InvalidOperation(
"invalid calculation range".into(),
));
}
let clamped = match (min, max) {
(Some(min_val), Some(max_val)) => value.clamp(*min_val, *max_val),
(Some(min_val), None) => value.max(*min_val),
(None, Some(max_val)) => value.min(*max_val),
(None, None) => value,
};
Ok(FieldValue::Number(clamped))
}
JavaScriptCalculation::NumberCalculate {
field,
decimals,
sep_style: _,
currency: _,
} => {
let value = self
.engine
.get_field_value(field)
.map(|v| v.to_number())
.unwrap_or(0.0);
let rounded = round_calculation_number(value, *decimals)?;
Ok(FieldValue::Number(rounded))
}
JavaScriptCalculation::Custom { script, .. } => {
self.evaluate_custom_script(script)
}
}
}
fn evaluate_custom_script(&self, script: &str) -> Result<FieldValue, PdfError> {
if script.contains('+') {
let parts: Vec<&str> = script.split('+').collect();
if parts.len() == 2 {
let field1 = parts[0].trim();
let field2 = parts[1].trim();
let val1 = self
.engine
.get_field_value(field1)
.map(|v| v.to_number())
.unwrap_or(0.0);
let val2 = self
.engine
.get_field_value(field2)
.map(|v| v.to_number())
.unwrap_or(0.0);
return Ok(FieldValue::Number(val1 + val2));
}
}
Ok(FieldValue::Empty)
}
fn recalculate_js_fields(&mut self, changed_field: &str) -> Result<(), PdfError> {
let order = self.recalculation_order(changed_field)?;
for field in order {
self.calculate_js_field(&field)?;
}
Ok(())
}
fn recalculation_order(&self, changed_field: &str) -> Result<Vec<String>, PdfError> {
let dependencies: HashMap<_, _> = self
.js_calculations
.iter()
.map(|(name, calculation)| (name.as_str(), self.extract_js_dependencies(calculation)))
.collect();
let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
for (&name, inputs) in &dependencies {
for input in inputs {
dependents.entry(input.as_str()).or_default().push(name);
}
}
let mut affected = HashSet::new();
let mut queue = VecDeque::from([changed_field]);
while let Some(input) = queue.pop_front() {
for &name in dependents.get(input).into_iter().flatten() {
if affected.insert(name) {
queue.push_back(name);
}
}
}
let mut pending: HashMap<_, usize> = affected
.iter()
.map(|&name| {
let count = dependencies[name]
.iter()
.filter(|input| affected.contains(input.as_str()))
.count();
(name, count)
})
.collect();
let mut ready: BTreeSet<_> = pending
.iter()
.filter_map(|(&name, &count)| (count == 0).then_some(name))
.collect();
let mut depths: HashMap<&str, usize> = HashMap::new();
let mut order = Vec::with_capacity(affected.len());
while let Some(name) = ready.pop_first() {
let depth = dependencies[name]
.iter()
.filter_map(|input| depths.get(input.as_str()))
.copied()
.max()
.unwrap_or(0)
+ 1;
if depth > self.settings.max_depth {
return Err(PdfError::InvalidOperation(format!(
"calculation depth exceeds {}",
self.settings.max_depth
)));
}
depths.insert(name, depth);
order.push(name.to_string());
for &dependent in dependents.get(name).into_iter().flatten() {
if let Some(count) = pending.get_mut(dependent) {
*count -= 1;
if *count == 0 {
ready.insert(dependent);
}
}
}
}
if order.len() != affected.len() {
return Err(PdfError::InvalidStructure(
"cyclic calculation dependencies".into(),
));
}
Ok(order)
}
fn apply_format(
&self,
value: FieldValue,
format: &FieldFormat,
) -> Result<FieldValue, PdfError> {
match format {
FieldFormat::Number { decimals, .. } => {
let num = value.to_number();
let rounded = round_calculation_number(num, *decimals)?;
Ok(FieldValue::Number(rounded))
}
FieldFormat::Percent { decimals } => {
let num = value.to_number();
let rounded = round_calculation_number(num * 100.0, *decimals)?;
Ok(FieldValue::Text(format!("{}%", rounded)))
}
_ => Ok(value),
}
}
pub fn set_field_format(&mut self, field_name: impl Into<String>, format: FieldFormat) {
self.field_formats.insert(field_name.into(), format);
}
pub fn get_summary(&self) -> CalculationSystemSummary {
CalculationSystemSummary {
total_fields: self.engine.get_summary().total_fields,
js_calculations: self.js_calculations.len(),
formatted_fields: self.field_formats.len(),
events_logged: self.events.len(),
}
}
pub fn get_recent_events(&self, count: usize) -> Vec<&CalculationEvent> {
let start = self.events.len().saturating_sub(count);
self.events[start..].iter().collect()
}
pub fn clear_events(&mut self) {
self.events.clear();
}
pub fn to_pdf_dict(&self) -> Dictionary {
let mut dict = Dictionary::new();
let calc_order: Vec<Object> = self
.js_calculations
.keys()
.map(|k| Object::String(k.clone()))
.collect();
if !calc_order.is_empty() {
dict.set("CO", Object::Array(calc_order));
}
dict
}
}
fn validate_calculation_value(value: &FieldValue) -> Result<(), PdfError> {
if matches!(value, FieldValue::Number(number) if !number.is_finite()) {
return Err(PdfError::InvalidOperation(
"non-finite calculation value".into(),
));
}
Ok(())
}
fn round_calculation_number(value: f64, decimals: usize) -> Result<f64, PdfError> {
let exponent = i32::try_from(decimals)
.map_err(|_| PdfError::InvalidOperation("unsupported decimal precision".into()))?;
let factor = 10_f64.powi(exponent);
let result = (value * factor).round() / factor;
if !factor.is_finite() || !result.is_finite() {
return Err(PdfError::InvalidOperation(
"numeric formatting overflow".into(),
));
}
Ok(result)
}
#[derive(Debug, Clone)]
pub struct CalculationSystemSummary {
pub total_fields: usize,
pub js_calculations: usize,
pub formatted_fields: usize,
pub events_logged: usize,
}
impl fmt::Display for CalculationSystemSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Calculation System Summary:\n\
- Total fields: {}\n\
- JavaScript calculations: {}\n\
- Formatted fields: {}\n\
- Events logged: {}",
self.total_fields, self.js_calculations, self.formatted_fields, self.events_logged
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_calculate() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("field1", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("field2", FieldValue::Number(20.0))
.unwrap();
system
.set_field_value("field3", FieldValue::Number(30.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec![
"field1".to_string(),
"field2".to_string(),
"field3".to_string(),
],
};
system.add_js_calculation("total", calc).unwrap();
let total = system.engine.get_field_value("total").unwrap();
assert_eq!(total.to_number(), 60.0);
}
#[test]
fn test_percent_calculate() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(100.0))
.unwrap();
system
.set_field_value("percent", FieldValue::Number(15.0))
.unwrap();
let calc = JavaScriptCalculation::PercentCalculate {
base_field: "base".to_string(),
percent_field: "percent".to_string(),
mode: PercentMode::PercentOf,
};
system.add_js_calculation("result", calc).unwrap();
let result = system.engine.get_field_value("result").unwrap();
assert_eq!(result.to_number(), 15.0);
}
#[test]
fn test_range_calculate() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("value", FieldValue::Number(150.0))
.unwrap();
let calc = JavaScriptCalculation::RangeCalculate {
field: "value".to_string(),
min: Some(0.0),
max: Some(100.0),
};
system.add_js_calculation("clamped", calc).unwrap();
let clamped = system.engine.get_field_value("clamped").unwrap();
assert_eq!(clamped.to_number(), 100.0);
}
#[test]
fn test_circular_dependency_detection() {
let mut system = FormCalculationSystem::new();
let calc1 = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["fieldB".to_string()],
};
system.add_js_calculation("fieldA", calc1).unwrap();
let calc2 = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["fieldA".to_string()],
};
let result = system.add_js_calculation("fieldB", calc2);
assert!(result.is_err());
}
#[test]
fn test_event_logging() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("test", FieldValue::Number(42.0))
.unwrap();
assert_eq!(system.events.len(), 1);
assert_eq!(system.events[0].event_type, EventType::ValueChanged);
assert_eq!(system.events[0].field, "test");
}
#[test]
fn test_default_calculation_settings() {
let settings = CalculationSettings::default();
assert!(settings.auto_recalculate);
assert_eq!(settings.max_depth, 100);
assert!(settings.log_events);
assert_eq!(settings.decimal_precision, 2);
}
#[test]
fn test_form_calculation_system_default() {
let system = FormCalculationSystem::default();
let summary = system.get_summary();
assert_eq!(summary.total_fields, 0);
assert_eq!(summary.js_calculations, 0);
assert_eq!(summary.formatted_fields, 0);
assert_eq!(summary.events_logged, 0);
}
#[test]
fn test_with_settings() {
let settings = CalculationSettings {
auto_recalculate: false,
max_depth: 50,
log_events: false,
decimal_precision: 4,
};
let system = FormCalculationSystem::with_settings(settings.clone());
assert!(!system.settings.auto_recalculate);
assert_eq!(system.settings.max_depth, 50);
}
#[test]
fn test_simple_calculate_product() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("a", FieldValue::Number(2.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(3.0))
.unwrap();
system
.set_field_value("c", FieldValue::Number(4.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Product,
fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
};
system.add_js_calculation("product", calc).unwrap();
let result = system.engine.get_field_value("product").unwrap();
assert_eq!(result.to_number(), 24.0);
}
#[test]
fn test_simple_calculate_average() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("a", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(20.0))
.unwrap();
system
.set_field_value("c", FieldValue::Number(30.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Average,
fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
};
system.add_js_calculation("avg", calc).unwrap();
let result = system.engine.get_field_value("avg").unwrap();
assert_eq!(result.to_number(), 20.0);
}
#[test]
fn test_simple_calculate_minimum() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("a", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(5.0))
.unwrap();
system
.set_field_value("c", FieldValue::Number(15.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Minimum,
fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
};
system.add_js_calculation("min", calc).unwrap();
let result = system.engine.get_field_value("min").unwrap();
assert_eq!(result.to_number(), 5.0);
}
#[test]
fn test_simple_calculate_maximum() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("a", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(5.0))
.unwrap();
system
.set_field_value("c", FieldValue::Number(15.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Maximum,
fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
};
system.add_js_calculation("max", calc).unwrap();
let result = system.engine.get_field_value("max").unwrap();
assert_eq!(result.to_number(), 15.0);
}
#[test]
fn test_simple_calculate_empty_fields() {
let mut system = FormCalculationSystem::new();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec![],
};
system.add_js_calculation("empty_sum", calc).unwrap();
let result = system.engine.get_field_value("empty_sum").unwrap();
assert_eq!(result.to_number(), 0.0);
}
#[test]
fn test_percent_calculate_percentage_of() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(200.0))
.unwrap();
system
.set_field_value("value", FieldValue::Number(50.0))
.unwrap();
let calc = JavaScriptCalculation::PercentCalculate {
base_field: "base".to_string(),
percent_field: "value".to_string(),
mode: PercentMode::PercentageOf,
};
system.add_js_calculation("percentage", calc).unwrap();
let result = system.engine.get_field_value("percentage").unwrap();
assert_eq!(result.to_number(), 25.0);
}
#[test]
fn test_percent_calculate_percentage_of_zero_base() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(0.0))
.unwrap();
system
.set_field_value("value", FieldValue::Number(50.0))
.unwrap();
let calc = JavaScriptCalculation::PercentCalculate {
base_field: "base".to_string(),
percent_field: "value".to_string(),
mode: PercentMode::PercentageOf,
};
system.add_js_calculation("percentage", calc).unwrap();
let result = system.engine.get_field_value("percentage").unwrap();
assert_eq!(result.to_number(), 0.0);
}
#[test]
fn test_percent_calculate_add_percent() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(100.0))
.unwrap();
system
.set_field_value("percent", FieldValue::Number(10.0))
.unwrap();
let calc = JavaScriptCalculation::PercentCalculate {
base_field: "base".to_string(),
percent_field: "percent".to_string(),
mode: PercentMode::AddPercent,
};
system.add_js_calculation("with_tax", calc).unwrap();
let result = system.engine.get_field_value("with_tax").unwrap();
assert!((result.to_number() - 110.0).abs() < 0.0001);
}
#[test]
fn test_percent_calculate_subtract_percent() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(100.0))
.unwrap();
system
.set_field_value("percent", FieldValue::Number(20.0))
.unwrap();
let calc = JavaScriptCalculation::PercentCalculate {
base_field: "base".to_string(),
percent_field: "percent".to_string(),
mode: PercentMode::SubtractPercent,
};
system.add_js_calculation("discount", calc).unwrap();
let result = system.engine.get_field_value("discount").unwrap();
assert_eq!(result.to_number(), 80.0);
}
#[test]
fn test_range_calculate_min_only() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("value", FieldValue::Number(-10.0))
.unwrap();
let calc = JavaScriptCalculation::RangeCalculate {
field: "value".to_string(),
min: Some(0.0),
max: None,
};
system.add_js_calculation("clamped", calc).unwrap();
let result = system.engine.get_field_value("clamped").unwrap();
assert_eq!(result.to_number(), 0.0);
}
#[test]
fn test_range_calculate_max_only() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("value", FieldValue::Number(150.0))
.unwrap();
let calc = JavaScriptCalculation::RangeCalculate {
field: "value".to_string(),
min: None,
max: Some(100.0),
};
system.add_js_calculation("clamped", calc).unwrap();
let result = system.engine.get_field_value("clamped").unwrap();
assert_eq!(result.to_number(), 100.0);
}
#[test]
fn test_range_calculate_no_limits() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("value", FieldValue::Number(999.0))
.unwrap();
let calc = JavaScriptCalculation::RangeCalculate {
field: "value".to_string(),
min: None,
max: None,
};
system.add_js_calculation("passthrough", calc).unwrap();
let result = system.engine.get_field_value("passthrough").unwrap();
assert_eq!(result.to_number(), 999.0);
}
#[test]
fn test_number_calculate() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("value", FieldValue::Number(123.456789))
.unwrap();
let calc = JavaScriptCalculation::NumberCalculate {
field: "value".to_string(),
decimals: 2,
sep_style: SeparatorStyle::CommaPeriod,
currency: Some("$".to_string()),
};
system.add_js_calculation("formatted", calc).unwrap();
let result = system.engine.get_field_value("formatted").unwrap();
assert!((result.to_number() - 123.46).abs() < 0.001);
}
#[test]
fn test_date_calculate() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("start_date", FieldValue::Text("2024-01-01".to_string()))
.unwrap();
system
.set_field_value("days", FieldValue::Number(10.0))
.unwrap();
let calc = JavaScriptCalculation::DateCalculate {
start_date_field: "start_date".to_string(),
days_field: Some("days".to_string()),
format: "%Y-%m-%d".to_string(),
};
system.add_js_calculation("end_date", calc).unwrap();
let result = system.engine.get_field_value("end_date").unwrap();
assert_eq!(result.to_string(), "2024-01-11");
}
#[test]
fn test_date_calculate_invalid_date() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("start_date", FieldValue::Text("invalid".to_string()))
.unwrap();
let calc = JavaScriptCalculation::DateCalculate {
start_date_field: "start_date".to_string(),
days_field: None,
format: "%Y-%m-%d".to_string(),
};
system.add_js_calculation("end_date", calc).unwrap();
let result = system.engine.get_field_value("end_date").unwrap();
assert_eq!(result.to_string(), "");
}
#[test]
fn test_date_calculate_no_days_field() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("start_date", FieldValue::Text("2024-06-15".to_string()))
.unwrap();
let calc = JavaScriptCalculation::DateCalculate {
start_date_field: "start_date".to_string(),
days_field: None,
format: "%Y-%m-%d".to_string(),
};
system.add_js_calculation("end_date", calc).unwrap();
let result = system.engine.get_field_value("end_date").unwrap();
assert_eq!(result.to_string(), "2024-06-15");
}
#[test]
fn test_custom_script_addition() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("a", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(20.0))
.unwrap();
let calc = JavaScriptCalculation::Custom {
script: "a + b".to_string(),
dependencies: vec!["a".to_string(), "b".to_string()],
};
system.add_js_calculation("custom_result", calc).unwrap();
let result = system.engine.get_field_value("custom_result").unwrap();
assert_eq!(result.to_number(), 30.0);
}
#[test]
fn test_custom_script_unsupported() {
let mut system = FormCalculationSystem::new();
let calc = JavaScriptCalculation::Custom {
script: "some unsupported script".to_string(),
dependencies: vec![],
};
system.add_js_calculation("unsupported", calc).unwrap();
let result = system.engine.get_field_value("unsupported").unwrap();
assert_eq!(result.to_number(), 0.0);
}
#[test]
fn test_self_reference_detection() {
let mut system = FormCalculationSystem::new();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["selfField".to_string()],
};
let result = system.add_js_calculation("selfField", calc);
assert!(result.is_err());
}
#[test]
fn test_field_format_number() {
let mut system = FormCalculationSystem::new();
system.set_field_format(
"price",
FieldFormat::Number {
decimals: 2,
separator: SeparatorStyle::CommaPeriod,
negative_style: NegativeStyle::MinusBlack,
currency: Some("$".to_string()),
},
);
system
.set_field_value("raw_price", FieldValue::Number(123.456))
.unwrap();
let calc = JavaScriptCalculation::NumberCalculate {
field: "raw_price".to_string(),
decimals: 2,
sep_style: SeparatorStyle::CommaPeriod,
currency: Some("$".to_string()),
};
system.add_js_calculation("price", calc).unwrap();
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_field_format_percent() {
let mut system = FormCalculationSystem::new();
system.set_field_format("rate", FieldFormat::Percent { decimals: 1 });
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_apply_format_number() {
let system = FormCalculationSystem::new();
let format = FieldFormat::Number {
decimals: 2,
separator: SeparatorStyle::CommaPeriod,
negative_style: NegativeStyle::MinusBlack,
currency: None,
};
let result = system
.apply_format(FieldValue::Number(123.456789), &format)
.unwrap();
assert!((result.to_number() - 123.46).abs() < 0.001);
}
#[test]
fn test_apply_format_percent() {
let system = FormCalculationSystem::new();
let format = FieldFormat::Percent { decimals: 1 };
let result = system
.apply_format(FieldValue::Number(0.5), &format)
.unwrap();
assert!(result.to_string().contains("50"));
}
#[test]
fn test_get_recent_events() {
let mut system = FormCalculationSystem::new();
for i in 0..10 {
system
.set_field_value(format!("field{}", i), FieldValue::Number(i as f64))
.unwrap();
}
let recent = system.get_recent_events(5);
assert_eq!(recent.len(), 5);
}
#[test]
fn test_get_recent_events_more_than_available() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("field1", FieldValue::Number(1.0))
.unwrap();
system
.set_field_value("field2", FieldValue::Number(2.0))
.unwrap();
let recent = system.get_recent_events(100);
assert_eq!(recent.len(), 2);
}
#[test]
fn test_clear_events() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("field1", FieldValue::Number(1.0))
.unwrap();
assert!(!system.events.is_empty());
system.clear_events();
assert!(system.events.is_empty());
}
#[test]
fn test_to_pdf_dict() {
let mut system = FormCalculationSystem::new();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["a".to_string(), "b".to_string()],
};
system.add_js_calculation("total", calc).unwrap();
let dict = system.to_pdf_dict();
assert!(dict.get("CO").is_some());
}
#[test]
fn test_to_pdf_dict_empty() {
let system = FormCalculationSystem::new();
let dict = system.to_pdf_dict();
assert!(dict.get("CO").is_none());
}
#[test]
fn test_calculation_system_summary_display() {
let summary = CalculationSystemSummary {
total_fields: 10,
js_calculations: 5,
formatted_fields: 3,
events_logged: 20,
};
let display = format!("{}", summary);
assert!(display.contains("Total fields: 10"));
assert!(display.contains("JavaScript calculations: 5"));
assert!(display.contains("Formatted fields: 3"));
assert!(display.contains("Events logged: 20"));
}
#[test]
fn test_auto_recalculate_disabled() {
let settings = CalculationSettings {
auto_recalculate: false,
..Default::default()
};
let mut system = FormCalculationSystem::with_settings(settings);
system
.set_field_value("a", FieldValue::Number(10.0))
.unwrap();
system
.set_field_value("b", FieldValue::Number(20.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["a".to_string(), "b".to_string()],
};
system.add_js_calculation("sum", calc).unwrap();
system
.set_field_value("a", FieldValue::Number(50.0))
.unwrap();
let result = system.engine.get_field_value("sum").unwrap();
assert_eq!(result.to_number(), 30.0); }
#[test]
fn test_log_events_disabled() {
let settings = CalculationSettings {
log_events: false,
..Default::default()
};
let mut system = FormCalculationSystem::with_settings(settings);
system
.set_field_value("field1", FieldValue::Number(1.0))
.unwrap();
system
.set_field_value("field2", FieldValue::Number(2.0))
.unwrap();
assert!(system.events.is_empty());
}
#[test]
fn test_separator_style_variants() {
assert_eq!(SeparatorStyle::CommaPeriod, SeparatorStyle::CommaPeriod);
assert_eq!(SeparatorStyle::PeriodComma, SeparatorStyle::PeriodComma);
assert_eq!(SeparatorStyle::SpacePeriod, SeparatorStyle::SpacePeriod);
assert_eq!(
SeparatorStyle::ApostrophePeriod,
SeparatorStyle::ApostrophePeriod
);
assert_eq!(SeparatorStyle::None, SeparatorStyle::None);
}
#[test]
fn test_negative_style_variants() {
assert_eq!(NegativeStyle::MinusBlack, NegativeStyle::MinusBlack);
assert_eq!(NegativeStyle::RedParentheses, NegativeStyle::RedParentheses);
assert_eq!(
NegativeStyle::BlackParentheses,
NegativeStyle::BlackParentheses
);
assert_eq!(NegativeStyle::MinusRed, NegativeStyle::MinusRed);
}
#[test]
fn test_special_format_variants() {
assert_eq!(SpecialFormat::ZipCode, SpecialFormat::ZipCode);
assert_eq!(SpecialFormat::ZipCodePlus4, SpecialFormat::ZipCodePlus4);
assert_eq!(SpecialFormat::PhoneNumber, SpecialFormat::PhoneNumber);
assert_eq!(SpecialFormat::SSN, SpecialFormat::SSN);
}
#[test]
fn test_simple_operation_variants() {
assert_eq!(SimpleOperation::Sum, SimpleOperation::Sum);
assert_eq!(SimpleOperation::Product, SimpleOperation::Product);
assert_eq!(SimpleOperation::Average, SimpleOperation::Average);
assert_eq!(SimpleOperation::Minimum, SimpleOperation::Minimum);
assert_eq!(SimpleOperation::Maximum, SimpleOperation::Maximum);
}
#[test]
fn test_percent_mode_variants() {
assert_eq!(PercentMode::PercentOf, PercentMode::PercentOf);
assert_eq!(PercentMode::PercentageOf, PercentMode::PercentageOf);
assert_eq!(PercentMode::AddPercent, PercentMode::AddPercent);
assert_eq!(PercentMode::SubtractPercent, PercentMode::SubtractPercent);
}
#[test]
fn test_event_type_variants() {
assert_eq!(EventType::ValueChanged, EventType::ValueChanged);
assert_eq!(
EventType::CalculationTriggered,
EventType::CalculationTriggered
);
assert_eq!(EventType::ValidationFailed, EventType::ValidationFailed);
assert_eq!(EventType::FormatApplied, EventType::FormatApplied);
assert_eq!(EventType::DependencyUpdated, EventType::DependencyUpdated);
}
#[test]
fn test_recalculate_dependent_fields() {
let mut system = FormCalculationSystem::new();
system
.set_field_value("base", FieldValue::Number(100.0))
.unwrap();
let calc = JavaScriptCalculation::SimpleCalculate {
operation: SimpleOperation::Sum,
fields: vec!["base".to_string()],
};
system.add_js_calculation("derived", calc).unwrap();
let initial = system.engine.get_field_value("derived").unwrap();
assert_eq!(initial.to_number(), 100.0);
system
.set_field_value("base", FieldValue::Number(200.0))
.unwrap();
let updated = system.engine.get_field_value("derived").unwrap();
assert_eq!(updated.to_number(), 200.0);
}
#[test]
fn test_field_format_date() {
let mut system = FormCalculationSystem::new();
system.set_field_format(
"date_field",
FieldFormat::Date {
format: "%Y-%m-%d".to_string(),
},
);
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_field_format_time() {
let mut system = FormCalculationSystem::new();
system.set_field_format(
"time_field",
FieldFormat::Time {
format: "%H:%M:%S".to_string(),
},
);
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_field_format_special() {
let mut system = FormCalculationSystem::new();
system.set_field_format(
"ssn_field",
FieldFormat::Special {
format_type: SpecialFormat::SSN,
},
);
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_field_format_custom() {
let mut system = FormCalculationSystem::new();
system.set_field_format(
"custom_field",
FieldFormat::Custom {
format_string: "###-###".to_string(),
},
);
let summary = system.get_summary();
assert_eq!(summary.formatted_fields, 1);
}
#[test]
fn test_apply_format_passthrough() {
let system = FormCalculationSystem::new();
let format = FieldFormat::Date {
format: "%Y-%m-%d".to_string(),
};
let result = system
.apply_format(FieldValue::Text("2024-01-01".to_string()), &format)
.unwrap();
assert_eq!(result.to_string(), "2024-01-01");
}
}