1use crate::error::PdfError;
10use crate::forms::calculations::{CalculationEngine, FieldValue};
11use crate::objects::{Dictionary, Object};
12use chrono::{DateTime, NaiveDate, Utc};
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::fmt;
15
16#[derive(Debug, Clone)]
18pub struct FormCalculationSystem {
19 engine: CalculationEngine,
21 js_calculations: HashMap<String, JavaScriptCalculation>,
23 field_formats: HashMap<String, FieldFormat>,
25 events: Vec<CalculationEvent>,
27 settings: CalculationSettings,
29}
30
31#[derive(Debug, Clone)]
33pub enum JavaScriptCalculation {
34 SimpleCalculate {
36 operation: SimpleOperation,
37 fields: Vec<String>,
38 },
39 PercentCalculate {
41 base_field: String,
42 percent_field: String,
43 mode: PercentMode,
44 },
45 DateCalculate {
47 start_date_field: String,
48 days_field: Option<String>,
49 format: String,
50 },
51 RangeCalculate {
53 field: String,
54 min: Option<f64>,
55 max: Option<f64>,
56 },
57 NumberCalculate {
59 field: String,
60 decimals: usize,
61 sep_style: SeparatorStyle,
62 currency: Option<String>,
63 },
64 Custom {
66 script: String,
67 dependencies: Vec<String>,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum SimpleOperation {
74 Sum, Product, Average, Minimum, Maximum, }
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum PercentMode {
84 PercentOf,
86 PercentageOf,
88 AddPercent,
90 SubtractPercent,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96pub enum SeparatorStyle {
97 CommaPeriod,
99 PeriodComma,
101 SpacePeriod,
103 ApostrophePeriod,
105 None,
107}
108
109#[derive(Debug, Clone)]
111pub enum FieldFormat {
112 Number {
114 decimals: usize,
115 separator: SeparatorStyle,
116 negative_style: NegativeStyle,
117 currency: Option<String>,
118 },
119 Percent { decimals: usize },
121 Date { format: String },
123 Time { format: String },
125 Special { format_type: SpecialFormat },
127 Custom { format_string: String },
129}
130
131#[derive(Debug, Clone, Copy, PartialEq)]
133pub enum NegativeStyle {
134 MinusBlack, RedParentheses, BlackParentheses, MinusRed, }
139
140#[derive(Debug, Clone, Copy, PartialEq)]
142pub enum SpecialFormat {
143 ZipCode, ZipCodePlus4, PhoneNumber, SSN, }
148
149#[derive(Debug, Clone)]
151#[allow(dead_code)]
152pub struct CalculationEvent {
153 timestamp: DateTime<Utc>,
155 field: String,
157 event_type: EventType,
159 old_value: Option<FieldValue>,
161 new_value: Option<FieldValue>,
163}
164
165#[derive(Debug, Clone, PartialEq)]
167pub enum EventType {
168 ValueChanged,
169 CalculationTriggered,
170 ValidationFailed,
171 FormatApplied,
172 DependencyUpdated,
173}
174
175#[derive(Debug, Clone)]
177pub struct CalculationSettings {
178 pub auto_recalculate: bool,
180 pub max_depth: usize,
182 pub log_events: bool,
184 pub decimal_precision: usize,
186}
187
188impl Default for CalculationSettings {
189 fn default() -> Self {
190 Self {
191 auto_recalculate: true,
192 max_depth: 100,
193 log_events: true,
194 decimal_precision: 2,
195 }
196 }
197}
198
199impl Default for FormCalculationSystem {
200 fn default() -> Self {
201 Self {
202 engine: CalculationEngine::new(),
203 js_calculations: HashMap::new(),
204 field_formats: HashMap::new(),
205 events: Vec::new(),
206 settings: CalculationSettings::default(),
207 }
208 }
209}
210
211impl FormCalculationSystem {
212 pub fn new() -> Self {
214 Self::default()
215 }
216
217 pub fn with_settings(settings: CalculationSettings) -> Self {
219 Self {
220 settings,
221 ..Self::default()
222 }
223 }
224
225 pub fn set_field_value(
227 &mut self,
228 field_name: impl Into<String>,
229 value: FieldValue,
230 ) -> Result<(), PdfError> {
231 let field_name = field_name.into();
232
233 if self.settings.log_events {
235 let old_value = self.engine.get_field_value(&field_name).cloned();
236 self.events.push(CalculationEvent {
237 timestamp: Utc::now(),
238 field: field_name.clone(),
239 event_type: EventType::ValueChanged,
240 old_value,
241 new_value: Some(value.clone()),
242 });
243 }
244
245 self.engine.set_field_value(field_name.clone(), value);
247
248 if self.settings.auto_recalculate {
250 self.recalculate_js_fields(&field_name)?;
251 }
252
253 Ok(())
254 }
255
256 pub fn add_js_calculation(
258 &mut self,
259 field_name: impl Into<String>,
260 calculation: JavaScriptCalculation,
261 ) -> Result<(), PdfError> {
262 let field_name = field_name.into();
263
264 let dependencies = self.extract_js_dependencies(&calculation);
266
267 if self.would_create_cycle(&field_name, &dependencies) {
269 return Err(PdfError::InvalidStructure(format!(
270 "Circular dependency detected for field '{}'",
271 field_name
272 )));
273 }
274
275 self.js_calculations.insert(field_name.clone(), calculation);
277
278 self.calculate_js_field(&field_name)?;
280
281 Ok(())
282 }
283
284 fn extract_js_dependencies(&self, calc: &JavaScriptCalculation) -> HashSet<String> {
286 let mut deps = HashSet::new();
287
288 match calc {
289 JavaScriptCalculation::SimpleCalculate { fields, .. } => {
290 deps.extend(fields.iter().cloned());
291 }
292 JavaScriptCalculation::PercentCalculate {
293 base_field,
294 percent_field,
295 ..
296 } => {
297 deps.insert(base_field.clone());
298 deps.insert(percent_field.clone());
299 }
300 JavaScriptCalculation::DateCalculate {
301 start_date_field,
302 days_field,
303 ..
304 } => {
305 deps.insert(start_date_field.clone());
306 if let Some(df) = days_field {
307 deps.insert(df.clone());
308 }
309 }
310 JavaScriptCalculation::RangeCalculate { field, .. } => {
311 deps.insert(field.clone());
312 }
313 JavaScriptCalculation::NumberCalculate { field, .. } => {
314 deps.insert(field.clone());
315 }
316 JavaScriptCalculation::Custom { dependencies, .. } => {
317 deps.extend(dependencies.iter().cloned());
318 }
319 }
320
321 deps
322 }
323
324 fn would_create_cycle(&self, field: &str, new_deps: &HashSet<String>) -> bool {
326 for dep in new_deps {
327 if dep == field {
328 return true; }
330
331 if self.depends_on(dep, field) {
333 return true;
334 }
335 }
336
337 false
338 }
339
340 fn depends_on(&self, field_a: &str, field_b: &str) -> bool {
342 let mut visited = HashSet::new();
343 let mut queue = VecDeque::new();
344 queue.push_back(field_a.to_string());
345
346 while let Some(current) = queue.pop_front() {
347 if current == field_b {
348 return true;
349 }
350
351 if visited.contains(¤t) {
352 continue;
353 }
354 visited.insert(current.clone());
355
356 if let Some(calc) = self.js_calculations.get(¤t) {
358 let deps = self.extract_js_dependencies(calc);
359 for dep in deps {
360 queue.push_back(dep);
361 }
362 }
363 }
364
365 false
366 }
367
368 fn calculate_js_field(&mut self, field_name: &str) -> Result<(), PdfError> {
370 if let Some(calculation) = self.js_calculations.get(field_name).cloned() {
371 let value = self.evaluate_js_calculation(&calculation)?;
372
373 let formatted_value = if let Some(format) = self.field_formats.get(field_name) {
375 self.apply_format(value, format)?
376 } else {
377 value
378 };
379
380 self.engine.set_field_value(field_name, formatted_value);
381
382 if self.settings.log_events {
383 self.events.push(CalculationEvent {
384 timestamp: Utc::now(),
385 field: field_name.to_string(),
386 event_type: EventType::CalculationTriggered,
387 old_value: None,
388 new_value: self.engine.get_field_value(field_name).cloned(),
389 });
390 }
391 }
392
393 Ok(())
394 }
395
396 fn evaluate_js_calculation(
398 &self,
399 calc: &JavaScriptCalculation,
400 ) -> Result<FieldValue, PdfError> {
401 match calc {
402 JavaScriptCalculation::SimpleCalculate { operation, fields } => {
403 let values: Vec<f64> = fields
404 .iter()
405 .filter_map(|f| self.engine.get_field_value(f))
406 .map(|v| v.to_number())
407 .collect();
408
409 if values.is_empty() {
410 return Ok(FieldValue::Number(0.0));
411 }
412
413 let result = match operation {
414 SimpleOperation::Sum => values.iter().sum(),
415 SimpleOperation::Product => values.iter().product(),
416 SimpleOperation::Average => values.iter().sum::<f64>() / values.len() as f64,
417 SimpleOperation::Minimum => {
418 values.iter().cloned().fold(f64::INFINITY, f64::min)
419 }
420 SimpleOperation::Maximum => {
421 values.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
422 }
423 };
424
425 Ok(FieldValue::Number(result))
426 }
427 JavaScriptCalculation::PercentCalculate {
428 base_field,
429 percent_field,
430 mode,
431 } => {
432 let base = self
433 .engine
434 .get_field_value(base_field)
435 .map(|v| v.to_number())
436 .unwrap_or(0.0);
437 let percent = self
438 .engine
439 .get_field_value(percent_field)
440 .map(|v| v.to_number())
441 .unwrap_or(0.0);
442
443 let result = match mode {
444 PercentMode::PercentOf => base * (percent / 100.0),
445 PercentMode::PercentageOf => {
446 if base != 0.0 {
447 (percent / base) * 100.0
448 } else {
449 0.0
450 }
451 }
452 PercentMode::AddPercent => base * (1.0 + percent / 100.0),
453 PercentMode::SubtractPercent => base * (1.0 - percent / 100.0),
454 };
455
456 Ok(FieldValue::Number(result))
457 }
458 JavaScriptCalculation::DateCalculate {
459 start_date_field,
460 days_field,
461 format: _,
462 } => {
463 let start_date_str = self
465 .engine
466 .get_field_value(start_date_field)
467 .map(|v| v.to_string())
468 .unwrap_or_default();
469
470 if let Ok(date) = NaiveDate::parse_from_str(&start_date_str, "%Y-%m-%d") {
472 let days = if let Some(df) = days_field {
473 self.engine
474 .get_field_value(df)
475 .map(|v| v.to_number() as i64)
476 .unwrap_or(0)
477 } else {
478 0
479 };
480
481 let result_date = date + chrono::Duration::days(days);
482 Ok(FieldValue::Text(result_date.format("%Y-%m-%d").to_string()))
483 } else {
484 Ok(FieldValue::Text(String::new()))
485 }
486 }
487 JavaScriptCalculation::RangeCalculate { field, min, max } => {
488 let value = self
489 .engine
490 .get_field_value(field)
491 .map(|v| v.to_number())
492 .unwrap_or(0.0);
493
494 let clamped = match (min, max) {
495 (Some(min_val), Some(max_val)) => value.clamp(*min_val, *max_val),
496 (Some(min_val), None) => value.max(*min_val),
497 (None, Some(max_val)) => value.min(*max_val),
498 (None, None) => value,
499 };
500
501 Ok(FieldValue::Number(clamped))
502 }
503 JavaScriptCalculation::NumberCalculate {
504 field,
505 decimals,
506 sep_style: _,
507 currency: _,
508 } => {
509 let value = self
510 .engine
511 .get_field_value(field)
512 .map(|v| v.to_number())
513 .unwrap_or(0.0);
514
515 let factor = 10_f64.powi(*decimals as i32);
517 let rounded = (value * factor).round() / factor;
518
519 Ok(FieldValue::Number(rounded))
520 }
521 JavaScriptCalculation::Custom { script, .. } => {
522 self.evaluate_custom_script(script)
525 }
526 }
527 }
528
529 fn evaluate_custom_script(&self, script: &str) -> Result<FieldValue, PdfError> {
531 if script.contains('+') {
536 let parts: Vec<&str> = script.split('+').collect();
537 if parts.len() == 2 {
538 let field1 = parts[0].trim();
539 let field2 = parts[1].trim();
540
541 let val1 = self
542 .engine
543 .get_field_value(field1)
544 .map(|v| v.to_number())
545 .unwrap_or(0.0);
546 let val2 = self
547 .engine
548 .get_field_value(field2)
549 .map(|v| v.to_number())
550 .unwrap_or(0.0);
551
552 return Ok(FieldValue::Number(val1 + val2));
553 }
554 }
555
556 Ok(FieldValue::Empty)
557 }
558
559 fn recalculate_js_fields(&mut self, changed_field: &str) -> Result<(), PdfError> {
561 let mut fields_to_recalc = Vec::new();
562
563 for (field_name, calc) in &self.js_calculations {
565 let deps = self.extract_js_dependencies(calc);
566 if deps.contains(changed_field) {
567 fields_to_recalc.push(field_name.clone());
568 }
569 }
570
571 for field in fields_to_recalc {
573 self.calculate_js_field(&field)?;
574 }
575
576 Ok(())
577 }
578
579 fn apply_format(
581 &self,
582 value: FieldValue,
583 format: &FieldFormat,
584 ) -> Result<FieldValue, PdfError> {
585 match format {
586 FieldFormat::Number { decimals, .. } => {
587 let num = value.to_number();
588 let factor = 10_f64.powi(*decimals as i32);
589 let rounded = (num * factor).round() / factor;
590 Ok(FieldValue::Number(rounded))
591 }
592 FieldFormat::Percent { decimals } => {
593 let num = value.to_number();
594 let factor = 10_f64.powi(*decimals as i32);
595 let rounded = (num * 100.0 * factor).round() / factor;
596 Ok(FieldValue::Text(format!("{}%", rounded)))
597 }
598 _ => Ok(value),
599 }
600 }
601
602 pub fn set_field_format(&mut self, field_name: impl Into<String>, format: FieldFormat) {
604 self.field_formats.insert(field_name.into(), format);
605 }
606
607 pub fn get_summary(&self) -> CalculationSystemSummary {
609 CalculationSystemSummary {
610 total_fields: self.engine.get_summary().total_fields,
611 js_calculations: self.js_calculations.len(),
612 formatted_fields: self.field_formats.len(),
613 events_logged: self.events.len(),
614 }
615 }
616
617 pub fn get_recent_events(&self, count: usize) -> Vec<&CalculationEvent> {
619 let start = self.events.len().saturating_sub(count);
620 self.events[start..].iter().collect()
621 }
622
623 pub fn clear_events(&mut self) {
625 self.events.clear();
626 }
627
628 pub fn to_pdf_dict(&self) -> Dictionary {
630 let mut dict = Dictionary::new();
631
632 let calc_order: Vec<Object> = self
634 .js_calculations
635 .keys()
636 .map(|k| Object::String(k.clone()))
637 .collect();
638
639 if !calc_order.is_empty() {
640 dict.set("CO", Object::Array(calc_order));
641 }
642
643 dict
644 }
645}
646
647#[derive(Debug, Clone)]
649pub struct CalculationSystemSummary {
650 pub total_fields: usize,
651 pub js_calculations: usize,
652 pub formatted_fields: usize,
653 pub events_logged: usize,
654}
655
656impl fmt::Display for CalculationSystemSummary {
657 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658 write!(
659 f,
660 "Calculation System Summary:\n\
661 - Total fields: {}\n\
662 - JavaScript calculations: {}\n\
663 - Formatted fields: {}\n\
664 - Events logged: {}",
665 self.total_fields, self.js_calculations, self.formatted_fields, self.events_logged
666 )
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn test_simple_calculate() {
676 let mut system = FormCalculationSystem::new();
677
678 system
680 .set_field_value("field1", FieldValue::Number(10.0))
681 .unwrap();
682 system
683 .set_field_value("field2", FieldValue::Number(20.0))
684 .unwrap();
685 system
686 .set_field_value("field3", FieldValue::Number(30.0))
687 .unwrap();
688
689 let calc = JavaScriptCalculation::SimpleCalculate {
691 operation: SimpleOperation::Sum,
692 fields: vec![
693 "field1".to_string(),
694 "field2".to_string(),
695 "field3".to_string(),
696 ],
697 };
698
699 system.add_js_calculation("total", calc).unwrap();
700
701 let total = system.engine.get_field_value("total").unwrap();
703 assert_eq!(total.to_number(), 60.0);
704 }
705
706 #[test]
707 fn test_percent_calculate() {
708 let mut system = FormCalculationSystem::new();
709
710 system
711 .set_field_value("base", FieldValue::Number(100.0))
712 .unwrap();
713 system
714 .set_field_value("percent", FieldValue::Number(15.0))
715 .unwrap();
716
717 let calc = JavaScriptCalculation::PercentCalculate {
718 base_field: "base".to_string(),
719 percent_field: "percent".to_string(),
720 mode: PercentMode::PercentOf,
721 };
722
723 system.add_js_calculation("result", calc).unwrap();
724
725 let result = system.engine.get_field_value("result").unwrap();
726 assert_eq!(result.to_number(), 15.0);
727 }
728
729 #[test]
730 fn test_range_calculate() {
731 let mut system = FormCalculationSystem::new();
732
733 system
734 .set_field_value("value", FieldValue::Number(150.0))
735 .unwrap();
736
737 let calc = JavaScriptCalculation::RangeCalculate {
738 field: "value".to_string(),
739 min: Some(0.0),
740 max: Some(100.0),
741 };
742
743 system.add_js_calculation("clamped", calc).unwrap();
744
745 let clamped = system.engine.get_field_value("clamped").unwrap();
746 assert_eq!(clamped.to_number(), 100.0);
747 }
748
749 #[test]
750 fn test_circular_dependency_detection() {
751 let mut system = FormCalculationSystem::new();
752
753 let calc1 = JavaScriptCalculation::SimpleCalculate {
755 operation: SimpleOperation::Sum,
756 fields: vec!["fieldB".to_string()],
757 };
758 system.add_js_calculation("fieldA", calc1).unwrap();
759
760 let calc2 = JavaScriptCalculation::SimpleCalculate {
762 operation: SimpleOperation::Sum,
763 fields: vec!["fieldA".to_string()],
764 };
765 let result = system.add_js_calculation("fieldB", calc2);
766
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_event_logging() {
772 let mut system = FormCalculationSystem::new();
773
774 system
775 .set_field_value("test", FieldValue::Number(42.0))
776 .unwrap();
777
778 assert_eq!(system.events.len(), 1);
779 assert_eq!(system.events[0].event_type, EventType::ValueChanged);
780 assert_eq!(system.events[0].field, "test");
781 }
782
783 #[test]
786 fn test_default_calculation_settings() {
787 let settings = CalculationSettings::default();
788 assert!(settings.auto_recalculate);
789 assert_eq!(settings.max_depth, 100);
790 assert!(settings.log_events);
791 assert_eq!(settings.decimal_precision, 2);
792 }
793
794 #[test]
795 fn test_form_calculation_system_default() {
796 let system = FormCalculationSystem::default();
797 let summary = system.get_summary();
798 assert_eq!(summary.total_fields, 0);
799 assert_eq!(summary.js_calculations, 0);
800 assert_eq!(summary.formatted_fields, 0);
801 assert_eq!(summary.events_logged, 0);
802 }
803
804 #[test]
805 fn test_with_settings() {
806 let settings = CalculationSettings {
807 auto_recalculate: false,
808 max_depth: 50,
809 log_events: false,
810 decimal_precision: 4,
811 };
812 let system = FormCalculationSystem::with_settings(settings.clone());
813 assert!(!system.settings.auto_recalculate);
814 assert_eq!(system.settings.max_depth, 50);
815 }
816
817 #[test]
818 fn test_simple_calculate_product() {
819 let mut system = FormCalculationSystem::new();
820 system
821 .set_field_value("a", FieldValue::Number(2.0))
822 .unwrap();
823 system
824 .set_field_value("b", FieldValue::Number(3.0))
825 .unwrap();
826 system
827 .set_field_value("c", FieldValue::Number(4.0))
828 .unwrap();
829
830 let calc = JavaScriptCalculation::SimpleCalculate {
831 operation: SimpleOperation::Product,
832 fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
833 };
834 system.add_js_calculation("product", calc).unwrap();
835
836 let result = system.engine.get_field_value("product").unwrap();
837 assert_eq!(result.to_number(), 24.0);
838 }
839
840 #[test]
841 fn test_simple_calculate_average() {
842 let mut system = FormCalculationSystem::new();
843 system
844 .set_field_value("a", FieldValue::Number(10.0))
845 .unwrap();
846 system
847 .set_field_value("b", FieldValue::Number(20.0))
848 .unwrap();
849 system
850 .set_field_value("c", FieldValue::Number(30.0))
851 .unwrap();
852
853 let calc = JavaScriptCalculation::SimpleCalculate {
854 operation: SimpleOperation::Average,
855 fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
856 };
857 system.add_js_calculation("avg", calc).unwrap();
858
859 let result = system.engine.get_field_value("avg").unwrap();
860 assert_eq!(result.to_number(), 20.0);
861 }
862
863 #[test]
864 fn test_simple_calculate_minimum() {
865 let mut system = FormCalculationSystem::new();
866 system
867 .set_field_value("a", FieldValue::Number(10.0))
868 .unwrap();
869 system
870 .set_field_value("b", FieldValue::Number(5.0))
871 .unwrap();
872 system
873 .set_field_value("c", FieldValue::Number(15.0))
874 .unwrap();
875
876 let calc = JavaScriptCalculation::SimpleCalculate {
877 operation: SimpleOperation::Minimum,
878 fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
879 };
880 system.add_js_calculation("min", calc).unwrap();
881
882 let result = system.engine.get_field_value("min").unwrap();
883 assert_eq!(result.to_number(), 5.0);
884 }
885
886 #[test]
887 fn test_simple_calculate_maximum() {
888 let mut system = FormCalculationSystem::new();
889 system
890 .set_field_value("a", FieldValue::Number(10.0))
891 .unwrap();
892 system
893 .set_field_value("b", FieldValue::Number(5.0))
894 .unwrap();
895 system
896 .set_field_value("c", FieldValue::Number(15.0))
897 .unwrap();
898
899 let calc = JavaScriptCalculation::SimpleCalculate {
900 operation: SimpleOperation::Maximum,
901 fields: vec!["a".to_string(), "b".to_string(), "c".to_string()],
902 };
903 system.add_js_calculation("max", calc).unwrap();
904
905 let result = system.engine.get_field_value("max").unwrap();
906 assert_eq!(result.to_number(), 15.0);
907 }
908
909 #[test]
910 fn test_simple_calculate_empty_fields() {
911 let mut system = FormCalculationSystem::new();
912
913 let calc = JavaScriptCalculation::SimpleCalculate {
914 operation: SimpleOperation::Sum,
915 fields: vec![],
916 };
917 system.add_js_calculation("empty_sum", calc).unwrap();
918
919 let result = system.engine.get_field_value("empty_sum").unwrap();
920 assert_eq!(result.to_number(), 0.0);
921 }
922
923 #[test]
924 fn test_percent_calculate_percentage_of() {
925 let mut system = FormCalculationSystem::new();
926 system
927 .set_field_value("base", FieldValue::Number(200.0))
928 .unwrap();
929 system
930 .set_field_value("value", FieldValue::Number(50.0))
931 .unwrap();
932
933 let calc = JavaScriptCalculation::PercentCalculate {
934 base_field: "base".to_string(),
935 percent_field: "value".to_string(),
936 mode: PercentMode::PercentageOf,
937 };
938 system.add_js_calculation("percentage", calc).unwrap();
939
940 let result = system.engine.get_field_value("percentage").unwrap();
941 assert_eq!(result.to_number(), 25.0);
942 }
943
944 #[test]
945 fn test_percent_calculate_percentage_of_zero_base() {
946 let mut system = FormCalculationSystem::new();
947 system
948 .set_field_value("base", FieldValue::Number(0.0))
949 .unwrap();
950 system
951 .set_field_value("value", FieldValue::Number(50.0))
952 .unwrap();
953
954 let calc = JavaScriptCalculation::PercentCalculate {
955 base_field: "base".to_string(),
956 percent_field: "value".to_string(),
957 mode: PercentMode::PercentageOf,
958 };
959 system.add_js_calculation("percentage", calc).unwrap();
960
961 let result = system.engine.get_field_value("percentage").unwrap();
962 assert_eq!(result.to_number(), 0.0);
963 }
964
965 #[test]
966 fn test_percent_calculate_add_percent() {
967 let mut system = FormCalculationSystem::new();
968 system
969 .set_field_value("base", FieldValue::Number(100.0))
970 .unwrap();
971 system
972 .set_field_value("percent", FieldValue::Number(10.0))
973 .unwrap();
974
975 let calc = JavaScriptCalculation::PercentCalculate {
976 base_field: "base".to_string(),
977 percent_field: "percent".to_string(),
978 mode: PercentMode::AddPercent,
979 };
980 system.add_js_calculation("with_tax", calc).unwrap();
981
982 let result = system.engine.get_field_value("with_tax").unwrap();
983 assert!((result.to_number() - 110.0).abs() < 0.0001);
984 }
985
986 #[test]
987 fn test_percent_calculate_subtract_percent() {
988 let mut system = FormCalculationSystem::new();
989 system
990 .set_field_value("base", FieldValue::Number(100.0))
991 .unwrap();
992 system
993 .set_field_value("percent", FieldValue::Number(20.0))
994 .unwrap();
995
996 let calc = JavaScriptCalculation::PercentCalculate {
997 base_field: "base".to_string(),
998 percent_field: "percent".to_string(),
999 mode: PercentMode::SubtractPercent,
1000 };
1001 system.add_js_calculation("discount", calc).unwrap();
1002
1003 let result = system.engine.get_field_value("discount").unwrap();
1004 assert_eq!(result.to_number(), 80.0);
1005 }
1006
1007 #[test]
1008 fn test_range_calculate_min_only() {
1009 let mut system = FormCalculationSystem::new();
1010 system
1011 .set_field_value("value", FieldValue::Number(-10.0))
1012 .unwrap();
1013
1014 let calc = JavaScriptCalculation::RangeCalculate {
1015 field: "value".to_string(),
1016 min: Some(0.0),
1017 max: None,
1018 };
1019 system.add_js_calculation("clamped", calc).unwrap();
1020
1021 let result = system.engine.get_field_value("clamped").unwrap();
1022 assert_eq!(result.to_number(), 0.0);
1023 }
1024
1025 #[test]
1026 fn test_range_calculate_max_only() {
1027 let mut system = FormCalculationSystem::new();
1028 system
1029 .set_field_value("value", FieldValue::Number(150.0))
1030 .unwrap();
1031
1032 let calc = JavaScriptCalculation::RangeCalculate {
1033 field: "value".to_string(),
1034 min: None,
1035 max: Some(100.0),
1036 };
1037 system.add_js_calculation("clamped", calc).unwrap();
1038
1039 let result = system.engine.get_field_value("clamped").unwrap();
1040 assert_eq!(result.to_number(), 100.0);
1041 }
1042
1043 #[test]
1044 fn test_range_calculate_no_limits() {
1045 let mut system = FormCalculationSystem::new();
1046 system
1047 .set_field_value("value", FieldValue::Number(999.0))
1048 .unwrap();
1049
1050 let calc = JavaScriptCalculation::RangeCalculate {
1051 field: "value".to_string(),
1052 min: None,
1053 max: None,
1054 };
1055 system.add_js_calculation("passthrough", calc).unwrap();
1056
1057 let result = system.engine.get_field_value("passthrough").unwrap();
1058 assert_eq!(result.to_number(), 999.0);
1059 }
1060
1061 #[test]
1062 fn test_number_calculate() {
1063 let mut system = FormCalculationSystem::new();
1064 system
1065 .set_field_value("value", FieldValue::Number(123.456789))
1066 .unwrap();
1067
1068 let calc = JavaScriptCalculation::NumberCalculate {
1069 field: "value".to_string(),
1070 decimals: 2,
1071 sep_style: SeparatorStyle::CommaPeriod,
1072 currency: Some("$".to_string()),
1073 };
1074 system.add_js_calculation("formatted", calc).unwrap();
1075
1076 let result = system.engine.get_field_value("formatted").unwrap();
1077 assert!((result.to_number() - 123.46).abs() < 0.001);
1078 }
1079
1080 #[test]
1081 fn test_date_calculate() {
1082 let mut system = FormCalculationSystem::new();
1083 system
1084 .set_field_value("start_date", FieldValue::Text("2024-01-01".to_string()))
1085 .unwrap();
1086 system
1087 .set_field_value("days", FieldValue::Number(10.0))
1088 .unwrap();
1089
1090 let calc = JavaScriptCalculation::DateCalculate {
1091 start_date_field: "start_date".to_string(),
1092 days_field: Some("days".to_string()),
1093 format: "%Y-%m-%d".to_string(),
1094 };
1095 system.add_js_calculation("end_date", calc).unwrap();
1096
1097 let result = system.engine.get_field_value("end_date").unwrap();
1098 assert_eq!(result.to_string(), "2024-01-11");
1099 }
1100
1101 #[test]
1102 fn test_date_calculate_invalid_date() {
1103 let mut system = FormCalculationSystem::new();
1104 system
1105 .set_field_value("start_date", FieldValue::Text("invalid".to_string()))
1106 .unwrap();
1107
1108 let calc = JavaScriptCalculation::DateCalculate {
1109 start_date_field: "start_date".to_string(),
1110 days_field: None,
1111 format: "%Y-%m-%d".to_string(),
1112 };
1113 system.add_js_calculation("end_date", calc).unwrap();
1114
1115 let result = system.engine.get_field_value("end_date").unwrap();
1116 assert_eq!(result.to_string(), "");
1117 }
1118
1119 #[test]
1120 fn test_date_calculate_no_days_field() {
1121 let mut system = FormCalculationSystem::new();
1122 system
1123 .set_field_value("start_date", FieldValue::Text("2024-06-15".to_string()))
1124 .unwrap();
1125
1126 let calc = JavaScriptCalculation::DateCalculate {
1127 start_date_field: "start_date".to_string(),
1128 days_field: None,
1129 format: "%Y-%m-%d".to_string(),
1130 };
1131 system.add_js_calculation("end_date", calc).unwrap();
1132
1133 let result = system.engine.get_field_value("end_date").unwrap();
1134 assert_eq!(result.to_string(), "2024-06-15");
1135 }
1136
1137 #[test]
1138 fn test_custom_script_addition() {
1139 let mut system = FormCalculationSystem::new();
1140 system
1141 .set_field_value("a", FieldValue::Number(10.0))
1142 .unwrap();
1143 system
1144 .set_field_value("b", FieldValue::Number(20.0))
1145 .unwrap();
1146
1147 let calc = JavaScriptCalculation::Custom {
1148 script: "a + b".to_string(),
1149 dependencies: vec!["a".to_string(), "b".to_string()],
1150 };
1151 system.add_js_calculation("custom_result", calc).unwrap();
1152
1153 let result = system.engine.get_field_value("custom_result").unwrap();
1154 assert_eq!(result.to_number(), 30.0);
1155 }
1156
1157 #[test]
1158 fn test_custom_script_unsupported() {
1159 let mut system = FormCalculationSystem::new();
1160
1161 let calc = JavaScriptCalculation::Custom {
1162 script: "some unsupported script".to_string(),
1163 dependencies: vec![],
1164 };
1165 system.add_js_calculation("unsupported", calc).unwrap();
1166
1167 let result = system.engine.get_field_value("unsupported").unwrap();
1168 assert_eq!(result.to_number(), 0.0);
1170 }
1171
1172 #[test]
1173 fn test_self_reference_detection() {
1174 let mut system = FormCalculationSystem::new();
1175
1176 let calc = JavaScriptCalculation::SimpleCalculate {
1177 operation: SimpleOperation::Sum,
1178 fields: vec!["selfField".to_string()],
1179 };
1180 let result = system.add_js_calculation("selfField", calc);
1181
1182 assert!(result.is_err());
1183 }
1184
1185 #[test]
1186 fn test_field_format_number() {
1187 let mut system = FormCalculationSystem::new();
1188
1189 system.set_field_format(
1190 "price",
1191 FieldFormat::Number {
1192 decimals: 2,
1193 separator: SeparatorStyle::CommaPeriod,
1194 negative_style: NegativeStyle::MinusBlack,
1195 currency: Some("$".to_string()),
1196 },
1197 );
1198
1199 system
1201 .set_field_value("raw_price", FieldValue::Number(123.456))
1202 .unwrap();
1203
1204 let calc = JavaScriptCalculation::NumberCalculate {
1205 field: "raw_price".to_string(),
1206 decimals: 2,
1207 sep_style: SeparatorStyle::CommaPeriod,
1208 currency: Some("$".to_string()),
1209 };
1210 system.add_js_calculation("price", calc).unwrap();
1211
1212 let summary = system.get_summary();
1213 assert_eq!(summary.formatted_fields, 1);
1214 }
1215
1216 #[test]
1217 fn test_field_format_percent() {
1218 let mut system = FormCalculationSystem::new();
1219
1220 system.set_field_format("rate", FieldFormat::Percent { decimals: 1 });
1221
1222 let summary = system.get_summary();
1223 assert_eq!(summary.formatted_fields, 1);
1224 }
1225
1226 #[test]
1227 fn test_apply_format_number() {
1228 let system = FormCalculationSystem::new();
1229
1230 let format = FieldFormat::Number {
1231 decimals: 2,
1232 separator: SeparatorStyle::CommaPeriod,
1233 negative_style: NegativeStyle::MinusBlack,
1234 currency: None,
1235 };
1236
1237 let result = system
1238 .apply_format(FieldValue::Number(123.456789), &format)
1239 .unwrap();
1240 assert!((result.to_number() - 123.46).abs() < 0.001);
1241 }
1242
1243 #[test]
1244 fn test_apply_format_percent() {
1245 let system = FormCalculationSystem::new();
1246
1247 let format = FieldFormat::Percent { decimals: 1 };
1248
1249 let result = system
1250 .apply_format(FieldValue::Number(0.5), &format)
1251 .unwrap();
1252 assert!(result.to_string().contains("50"));
1253 }
1254
1255 #[test]
1256 fn test_get_recent_events() {
1257 let mut system = FormCalculationSystem::new();
1258
1259 for i in 0..10 {
1260 system
1261 .set_field_value(format!("field{}", i), FieldValue::Number(i as f64))
1262 .unwrap();
1263 }
1264
1265 let recent = system.get_recent_events(5);
1266 assert_eq!(recent.len(), 5);
1267 }
1268
1269 #[test]
1270 fn test_get_recent_events_more_than_available() {
1271 let mut system = FormCalculationSystem::new();
1272
1273 system
1274 .set_field_value("field1", FieldValue::Number(1.0))
1275 .unwrap();
1276 system
1277 .set_field_value("field2", FieldValue::Number(2.0))
1278 .unwrap();
1279
1280 let recent = system.get_recent_events(100);
1281 assert_eq!(recent.len(), 2);
1282 }
1283
1284 #[test]
1285 fn test_clear_events() {
1286 let mut system = FormCalculationSystem::new();
1287
1288 system
1289 .set_field_value("field1", FieldValue::Number(1.0))
1290 .unwrap();
1291 assert!(!system.events.is_empty());
1292
1293 system.clear_events();
1294 assert!(system.events.is_empty());
1295 }
1296
1297 #[test]
1298 fn test_to_pdf_dict() {
1299 let mut system = FormCalculationSystem::new();
1300
1301 let calc = JavaScriptCalculation::SimpleCalculate {
1302 operation: SimpleOperation::Sum,
1303 fields: vec!["a".to_string(), "b".to_string()],
1304 };
1305 system.add_js_calculation("total", calc).unwrap();
1306
1307 let dict = system.to_pdf_dict();
1308 assert!(dict.get("CO").is_some());
1309 }
1310
1311 #[test]
1312 fn test_to_pdf_dict_empty() {
1313 let system = FormCalculationSystem::new();
1314 let dict = system.to_pdf_dict();
1315 assert!(dict.get("CO").is_none());
1316 }
1317
1318 #[test]
1319 fn test_calculation_system_summary_display() {
1320 let summary = CalculationSystemSummary {
1321 total_fields: 10,
1322 js_calculations: 5,
1323 formatted_fields: 3,
1324 events_logged: 20,
1325 };
1326
1327 let display = format!("{}", summary);
1328 assert!(display.contains("Total fields: 10"));
1329 assert!(display.contains("JavaScript calculations: 5"));
1330 assert!(display.contains("Formatted fields: 3"));
1331 assert!(display.contains("Events logged: 20"));
1332 }
1333
1334 #[test]
1335 fn test_auto_recalculate_disabled() {
1336 let settings = CalculationSettings {
1337 auto_recalculate: false,
1338 ..Default::default()
1339 };
1340 let mut system = FormCalculationSystem::with_settings(settings);
1341
1342 system
1343 .set_field_value("a", FieldValue::Number(10.0))
1344 .unwrap();
1345 system
1346 .set_field_value("b", FieldValue::Number(20.0))
1347 .unwrap();
1348
1349 let calc = JavaScriptCalculation::SimpleCalculate {
1350 operation: SimpleOperation::Sum,
1351 fields: vec!["a".to_string(), "b".to_string()],
1352 };
1353 system.add_js_calculation("sum", calc).unwrap();
1354
1355 system
1357 .set_field_value("a", FieldValue::Number(50.0))
1358 .unwrap();
1359
1360 let result = system.engine.get_field_value("sum").unwrap();
1362 assert_eq!(result.to_number(), 30.0); }
1364
1365 #[test]
1366 fn test_log_events_disabled() {
1367 let settings = CalculationSettings {
1368 log_events: false,
1369 ..Default::default()
1370 };
1371 let mut system = FormCalculationSystem::with_settings(settings);
1372
1373 system
1374 .set_field_value("field1", FieldValue::Number(1.0))
1375 .unwrap();
1376 system
1377 .set_field_value("field2", FieldValue::Number(2.0))
1378 .unwrap();
1379
1380 assert!(system.events.is_empty());
1381 }
1382
1383 #[test]
1384 fn test_separator_style_variants() {
1385 assert_eq!(SeparatorStyle::CommaPeriod, SeparatorStyle::CommaPeriod);
1386 assert_eq!(SeparatorStyle::PeriodComma, SeparatorStyle::PeriodComma);
1387 assert_eq!(SeparatorStyle::SpacePeriod, SeparatorStyle::SpacePeriod);
1388 assert_eq!(
1389 SeparatorStyle::ApostrophePeriod,
1390 SeparatorStyle::ApostrophePeriod
1391 );
1392 assert_eq!(SeparatorStyle::None, SeparatorStyle::None);
1393 }
1394
1395 #[test]
1396 fn test_negative_style_variants() {
1397 assert_eq!(NegativeStyle::MinusBlack, NegativeStyle::MinusBlack);
1398 assert_eq!(NegativeStyle::RedParentheses, NegativeStyle::RedParentheses);
1399 assert_eq!(
1400 NegativeStyle::BlackParentheses,
1401 NegativeStyle::BlackParentheses
1402 );
1403 assert_eq!(NegativeStyle::MinusRed, NegativeStyle::MinusRed);
1404 }
1405
1406 #[test]
1407 fn test_special_format_variants() {
1408 assert_eq!(SpecialFormat::ZipCode, SpecialFormat::ZipCode);
1409 assert_eq!(SpecialFormat::ZipCodePlus4, SpecialFormat::ZipCodePlus4);
1410 assert_eq!(SpecialFormat::PhoneNumber, SpecialFormat::PhoneNumber);
1411 assert_eq!(SpecialFormat::SSN, SpecialFormat::SSN);
1412 }
1413
1414 #[test]
1415 fn test_simple_operation_variants() {
1416 assert_eq!(SimpleOperation::Sum, SimpleOperation::Sum);
1417 assert_eq!(SimpleOperation::Product, SimpleOperation::Product);
1418 assert_eq!(SimpleOperation::Average, SimpleOperation::Average);
1419 assert_eq!(SimpleOperation::Minimum, SimpleOperation::Minimum);
1420 assert_eq!(SimpleOperation::Maximum, SimpleOperation::Maximum);
1421 }
1422
1423 #[test]
1424 fn test_percent_mode_variants() {
1425 assert_eq!(PercentMode::PercentOf, PercentMode::PercentOf);
1426 assert_eq!(PercentMode::PercentageOf, PercentMode::PercentageOf);
1427 assert_eq!(PercentMode::AddPercent, PercentMode::AddPercent);
1428 assert_eq!(PercentMode::SubtractPercent, PercentMode::SubtractPercent);
1429 }
1430
1431 #[test]
1432 fn test_event_type_variants() {
1433 assert_eq!(EventType::ValueChanged, EventType::ValueChanged);
1434 assert_eq!(
1435 EventType::CalculationTriggered,
1436 EventType::CalculationTriggered
1437 );
1438 assert_eq!(EventType::ValidationFailed, EventType::ValidationFailed);
1439 assert_eq!(EventType::FormatApplied, EventType::FormatApplied);
1440 assert_eq!(EventType::DependencyUpdated, EventType::DependencyUpdated);
1441 }
1442
1443 #[test]
1444 fn test_recalculate_dependent_fields() {
1445 let mut system = FormCalculationSystem::new();
1446
1447 system
1449 .set_field_value("base", FieldValue::Number(100.0))
1450 .unwrap();
1451
1452 let calc = JavaScriptCalculation::SimpleCalculate {
1454 operation: SimpleOperation::Sum,
1455 fields: vec!["base".to_string()],
1456 };
1457 system.add_js_calculation("derived", calc).unwrap();
1458
1459 let initial = system.engine.get_field_value("derived").unwrap();
1461 assert_eq!(initial.to_number(), 100.0);
1462
1463 system
1465 .set_field_value("base", FieldValue::Number(200.0))
1466 .unwrap();
1467
1468 let updated = system.engine.get_field_value("derived").unwrap();
1469 assert_eq!(updated.to_number(), 200.0);
1470 }
1471
1472 #[test]
1473 fn test_field_format_date() {
1474 let mut system = FormCalculationSystem::new();
1475
1476 system.set_field_format(
1477 "date_field",
1478 FieldFormat::Date {
1479 format: "%Y-%m-%d".to_string(),
1480 },
1481 );
1482
1483 let summary = system.get_summary();
1484 assert_eq!(summary.formatted_fields, 1);
1485 }
1486
1487 #[test]
1488 fn test_field_format_time() {
1489 let mut system = FormCalculationSystem::new();
1490
1491 system.set_field_format(
1492 "time_field",
1493 FieldFormat::Time {
1494 format: "%H:%M:%S".to_string(),
1495 },
1496 );
1497
1498 let summary = system.get_summary();
1499 assert_eq!(summary.formatted_fields, 1);
1500 }
1501
1502 #[test]
1503 fn test_field_format_special() {
1504 let mut system = FormCalculationSystem::new();
1505
1506 system.set_field_format(
1507 "ssn_field",
1508 FieldFormat::Special {
1509 format_type: SpecialFormat::SSN,
1510 },
1511 );
1512
1513 let summary = system.get_summary();
1514 assert_eq!(summary.formatted_fields, 1);
1515 }
1516
1517 #[test]
1518 fn test_field_format_custom() {
1519 let mut system = FormCalculationSystem::new();
1520
1521 system.set_field_format(
1522 "custom_field",
1523 FieldFormat::Custom {
1524 format_string: "###-###".to_string(),
1525 },
1526 );
1527
1528 let summary = system.get_summary();
1529 assert_eq!(summary.formatted_fields, 1);
1530 }
1531
1532 #[test]
1533 fn test_apply_format_passthrough() {
1534 let system = FormCalculationSystem::new();
1535
1536 let format = FieldFormat::Date {
1538 format: "%Y-%m-%d".to_string(),
1539 };
1540
1541 let result = system
1542 .apply_format(FieldValue::Text("2024-01-01".to_string()), &format)
1543 .unwrap();
1544 assert_eq!(result.to_string(), "2024-01-01");
1545 }
1546}