#![allow(clippy::unwrap_used)] use crate::test::util::new_empty_model; // Helper constants for common time values with detailed documentation const MIDNIGHT: &str = "0"; // 00:00:00 = 0/24 = 0 const NOON: &str = "0.5"; // 12:00:00 = 12/24 = 0.5 const TIME_14_30: &str = "0.604166667"; // 14:30:00 = 14.5/24 ≈ 0.604166667 const TIME_14_30_45: &str = "0.6046875"; // 14:30:45 = 14.5125/24 = 0.6046875 const TIME_14_30_59: &str = "0.604849537"; // 14:30:59 (from floored fractional inputs) const TIME_23_59_59: &str = "0.999988426"; // 23:59:59 = 23.99972.../24 ≈ 0.999988426 // Excel documentation test values with explanations const TIME_2_24_AM: &str = "0.1"; // 2:24 AM = 2.4/24 = 0.1 const TIME_2_PM: &str = "0.583333333"; // 2:00 PM = 14/24 ≈ 0.583333333 const TIME_6_45_PM: &str = "0.78125"; // 6:45 PM = 18.75/24 = 0.78125 const TIME_6_35_AM: &str = "0.274305556"; // 6:35 AM = 6.583333.../24 ≈ 0.274305556 const TIME_2_30_AM: &str = "0.104166667"; // 2:30 AM = 2.5/24 ≈ 0.104166667 const TIME_1_AM: &str = "0.041666667"; // 1:00 AM = 1/24 ≈ 0.041666667 const TIME_9_PM: &str = "0.875"; // 9:00 PM = 21/24 = 0.875 const TIME_2_AM: &str = "0.083333333"; // 2:00 AM = 2/24 ≈ 0.083333333 // Additional helper: 1-second past midnight (00:00:01) const TIME_00_00_01: &str = "0.000011574"; // 1 second = 1/86400 ≈ 0.000011574 /// Helper function to set up and evaluate a model with time expressions fn test_time_expressions(expressions: &[(&str, &str)]) -> crate::model::Model { let mut model = new_empty_model(); for (cell, formula) in expressions { model._set(cell, formula); } model.evaluate(); model } /// Helper function to test component extraction for a given time value /// Returns (hour, minute, second) as strings fn test_component_extraction(time_value: &str) -> (String, String, String) { let model = test_time_expressions(&[ ("A1", &format!("=HOUR({time_value})")), ("B1", &format!("=MINUTE({time_value})")), ("C1", &format!("=SECOND({time_value})")), ]); ( model._get_text("A1").to_string(), model._get_text("B1").to_string(), model._get_text("C1").to_string(), ) } #[test] fn test_excel_timevalue_compatibility() { // Test cases based on Excel's official documentation and examples let model = test_time_expressions(&[ // Excel documentation examples ("A1", "=TIMEVALUE(\"2:24 AM\")"), // Should be 0.1 ("A2", "=TIMEVALUE(\"2 PM\")"), // Should be 0.583333... (14/24) ("A3", "=TIMEVALUE(\"6:45 PM\")"), // Should be 0.78125 (18.75/24) ("A4", "=TIMEVALUE(\"18:45\")"), // Same as above, 24-hour format // Date-time format (date should be ignored) ("B1", "=TIMEVALUE(\"22-Aug-2011 6:35 AM\")"), // Should be ~0.2743 ("B2", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), // Should be 0.604166667 // Edge cases that Excel should support ("C1", "=TIMEVALUE(\"12:00 AM\")"), // Midnight: 0 ("C2", "=TIMEVALUE(\"12:00 PM\")"), // Noon: 0.5 ("C3", "=TIMEVALUE(\"11:59:59 PM\")"), // Almost midnight: 0.999988426 // Single digit variations ("D1", "=TIMEVALUE(\"1 AM\")"), // 1:00 AM ("D2", "=TIMEVALUE(\"9 PM\")"), // 9:00 PM ("D3", "=TIMEVALUE(\"12 AM\")"), // Midnight ("D4", "=TIMEVALUE(\"12 PM\")"), // Noon ]); // Excel documentation examples - verify exact values assert_eq!(model._get_text("A1"), *TIME_2_24_AM); // 2:24 AM assert_eq!(model._get_text("A2"), *TIME_2_PM); // 2 PM = 14:00 assert_eq!(model._get_text("A3"), *TIME_6_45_PM); // 6:45 PM = 18:45 assert_eq!(model._get_text("A4"), *TIME_6_45_PM); // 18:45 (24-hour) // Date-time formats (date ignored, extract time only) assert_eq!(model._get_text("B1"), *TIME_6_35_AM); // 6:35 AM ≈ 0.2743 assert_eq!(model._get_text("B2"), *TIME_14_30); // 14:30:00 // Edge cases assert_eq!(model._get_text("C1"), *MIDNIGHT); // 12:00 AM = 00:00 assert_eq!(model._get_text("C2"), *NOON); // 12:00 PM = 12:00 assert_eq!(model._get_text("C3"), *TIME_23_59_59); // 11:59:59 PM // Single digit hours assert_eq!(model._get_text("D1"), *TIME_1_AM); // 1:00 AM assert_eq!(model._get_text("D2"), *TIME_9_PM); // 9:00 PM = 21:00 assert_eq!(model._get_text("D3"), *MIDNIGHT); // 12 AM = 00:00 assert_eq!(model._get_text("D4"), *NOON); // 12 PM = 12:00 } #[test] fn test_time_function_basic_cases() { let model = test_time_expressions(&[ ("A1", "=TIME(0,0,0)"), // Midnight ("A2", "=TIME(12,0,0)"), // Noon ("A3", "=TIME(14,30,0)"), // 2:30 PM ("A4", "=TIME(23,59,59)"), // Max time ]); assert_eq!(model._get_text("A1"), *MIDNIGHT); assert_eq!(model._get_text("A2"), *NOON); assert_eq!(model._get_text("A3"), *TIME_14_30); assert_eq!(model._get_text("A4"), *TIME_23_59_59); } #[test] fn test_time_function_normalization() { let model = test_time_expressions(&[ ("A1", "=TIME(25,0,0)"), // Hours > 24 wrap around ("A2", "=TIME(48,0,0)"), // 48 hours = 0 (2 full days) ("A3", "=TIME(0,90,0)"), // 90 minutes = 1.5 hours ("A4", "=TIME(0,0,90)"), // 90 seconds = 1.5 minutes ("A5", "=TIME(14.9,30.9,59.9)"), // Fractional inputs floored to 14:30:59 ]); assert_eq!(model._get_text("A1"), *TIME_1_AM); // 1:00:00 assert_eq!(model._get_text("A2"), *MIDNIGHT); // 0:00:00 assert_eq!(model._get_text("A3"), *"0.0625"); // 1:30:00 assert_eq!(model._get_text("A4"), *"0.001041667"); // 0:01:30 assert_eq!(model._get_text("A5"), *TIME_14_30_59); // 14:30:59 (floored) } #[test] fn test_time_function_precision_edge_cases() { let model = test_time_expressions(&[ // High precision fractional seconds ("A1", "=TIME(14,30,45.999)"), // Fractional seconds should be floored ("A2", "=SECOND(TIME(14,30,45.999))"), // Should extract 45, not 46 // Very large normalization values ("B1", "=TIME(999,999,999)"), // Extreme normalization test ("B2", "=HOUR(999.5)"), // Multiple days, extract hour from fractional part ("B3", "=MINUTE(999.75)"), // Multiple days, extract minute // Boundary conditions at rollover points ("C1", "=TIME(24,60,60)"), // Should normalize to next day (00:01:00) ("C2", "=HOUR(0.999999999)"), // Almost 24 hours should be 23 ("C3", "=MINUTE(0.999999999)"), // Almost 24 hours, extract minutes ("C4", "=SECOND(0.999999999)"), // Almost 24 hours, extract seconds // Precision at boundaries ("D1", "=TIME(23,59,59.999)"), // Very close to midnight ("D2", "=TIME(0,0,0.001)"), // Just after midnight ]); // Fractional seconds are floored assert_eq!(model._get_text("A2"), *"45"); // 45.999 floored to 45 // Multiple days should work with rem_euclid assert_eq!(model._get_text("B2"), *"12"); // 999.5 days, hour = 12 (noon) // Boundary normalization assert_eq!(model._get_text("C1"), *"0.042361111"); // 24:60:60 = 01:01:00 (normalized) assert_eq!(model._get_text("C2"), *"23"); // Almost 24 hours = 23:xx:xx // High precision should be handled correctly let result_d1 = model._get_text("D1").parse::().unwrap(); assert!(result_d1 < 1.0 && result_d1 > 0.999); // Very close to but less than 1.0 } #[test] fn test_time_function_errors() { let model = test_time_expressions(&[ ("A1", "=TIME()"), // Wrong arg count ("A2", "=TIME(12)"), // Wrong arg count ("A3", "=TIME(12,30,0,0)"), // Wrong arg count ("B1", "=TIME(-1,0,0)"), // Negative hour ("B2", "=TIME(0,-1,0)"), // Negative minute ("B3", "=TIME(0,0,-1)"), // Negative second ]); // Wrong argument count assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A2"), *"#ERROR!"); assert_eq!(model._get_text("A3"), *"#ERROR!"); // Negative values should return #NUM! error assert_eq!(model._get_text("B1"), *"#NUM!"); assert_eq!(model._get_text("B2"), *"#NUM!"); assert_eq!(model._get_text("B3"), *"#NUM!"); } #[test] fn test_timevalue_function_formats() { let model = test_time_expressions(&[ // Basic formats ("A1", "=TIMEVALUE(\"14:30\")"), ("A2", "=TIMEVALUE(\"14:30:45\")"), ("A3", "=TIMEVALUE(\"00:00:00\")"), // AM/PM formats ("B1", "=TIMEVALUE(\"2:30 PM\")"), ("B2", "=TIMEVALUE(\"2:30 AM\")"), ("B3", "=TIMEVALUE(\"12:00 PM\")"), // Noon ("B4", "=TIMEVALUE(\"12:00 AM\")"), // Midnight // Single hour with AM/PM (now supported!) ("B5", "=TIMEVALUE(\"2 PM\")"), ("B6", "=TIMEVALUE(\"2 AM\")"), // Date-time formats (extract time only) ("C1", "=TIMEVALUE(\"2023-01-01 14:30:00\")"), ("C2", "=TIMEVALUE(\"2023-01-01T14:30:00\")"), // Whitespace handling ("D1", "=TIMEVALUE(\" 14:30 \")"), ]); // Basic formats assert_eq!(model._get_text("A1"), *TIME_14_30); assert_eq!(model._get_text("A2"), *TIME_14_30_45); assert_eq!(model._get_text("A3"), *MIDNIGHT); // AM/PM formats assert_eq!(model._get_text("B1"), *TIME_14_30); // 2:30 PM = 14:30 assert_eq!(model._get_text("B2"), *TIME_2_30_AM); // 2:30 AM assert_eq!(model._get_text("B3"), *NOON); // 12:00 PM = noon assert_eq!(model._get_text("B4"), *MIDNIGHT); // 12:00 AM = midnight // Single hour AM/PM formats (now supported!) assert_eq!(model._get_text("B5"), *TIME_2_PM); // 2 PM = 14:00 assert_eq!(model._get_text("B6"), *TIME_2_AM); // 2 AM = 02:00 // Date-time formats assert_eq!(model._get_text("C1"), *TIME_14_30); assert_eq!(model._get_text("C2"), *TIME_14_30); // Whitespace assert_eq!(model._get_text("D1"), *TIME_14_30); } #[test] fn test_timevalue_function_errors() { let model = test_time_expressions(&[ ("A1", "=TIMEVALUE()"), // Wrong arg count ("A2", "=TIMEVALUE(\"14:30\", \"x\")"), // Wrong arg count ("B1", "=TIMEVALUE(\"invalid\")"), // Invalid format ("B2", "=TIMEVALUE(\"25:00\")"), // Invalid hour ("B3", "=TIMEVALUE(\"14:70\")"), // Invalid minute ("B4", "=TIMEVALUE(\"\")"), // Empty string ("B5", "=TIMEVALUE(\"2PM\")"), // Missing space (still unsupported) ]); // Wrong argument count should return #ERROR! assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A2"), *"#ERROR!"); // Invalid formats should return #VALUE! assert_eq!(model._get_text("B1"), *"#VALUE!"); assert_eq!(model._get_text("B2"), *"#VALUE!"); assert_eq!(model._get_text("B3"), *"#VALUE!"); assert_eq!(model._get_text("B4"), *"#VALUE!"); assert_eq!(model._get_text("B5"), *"#VALUE!"); // "2PM" no space - not supported } #[test] fn test_time_component_extraction_comprehensive() { // Test component extraction using helper function for consistency // Test basic time values let test_cases = [ (MIDNIGHT, ("0", "0", "0")), // 00:00:00 (NOON, ("12", "0", "0")), // 12:00:00 (TIME_14_30, ("14", "30", "0")), // 14:30:00 (TIME_23_59_59, ("23", "59", "59")), // 23:59:59 ]; for (time_value, expected) in test_cases { let (hour, minute, second) = test_component_extraction(time_value); assert_eq!(hour, expected.0, "Hour mismatch for {time_value}"); assert_eq!(minute, expected.1, "Minute mismatch for {time_value}"); assert_eq!(second, expected.2, "Second mismatch for {time_value}"); } // Test multiple days (extract from fractional part) let (hour, minute, second) = test_component_extraction("1.5"); // Day 2, 12:00 assert_eq!( (hour, minute, second), ("12".to_string(), "0".to_string(), "0".to_string()) ); let (hour, minute, second) = test_component_extraction("100.604166667"); // Day 101, 14:30 assert_eq!( (hour, minute, second), ("14".to_string(), "30".to_string(), "0".to_string()) ); // Test precision at boundaries let (hour, _, _) = test_component_extraction("0.041666666"); // Just under 1:00 AM assert_eq!(hour, "0"); let (hour, _, _) = test_component_extraction("0.041666667"); // Exactly 1:00 AM assert_eq!(hour, "1"); let (hour, _, _) = test_component_extraction("0.041666668"); // Just over 1:00 AM assert_eq!(hour, "1"); // Test very large day values let (hour, minute, second) = test_component_extraction("1000000.25"); // Million days + 6 hours assert_eq!( (hour, minute, second), ("6".to_string(), "0".to_string(), "0".to_string()) ); } #[test] fn test_time_component_function_errors() { let model = test_time_expressions(&[ // Wrong argument counts ("A1", "=HOUR()"), // No arguments ("A2", "=MINUTE()"), // No arguments ("A3", "=SECOND()"), // No arguments ("A4", "=HOUR(1, 2)"), // Too many arguments ("A5", "=MINUTE(1, 2)"), // Too many arguments ("A6", "=SECOND(1, 2)"), // Too many arguments // Negative values should return #NUM! ("B1", "=HOUR(-0.5)"), // Negative value ("B2", "=MINUTE(-1)"), // Negative value ("B3", "=SECOND(-1)"), // Negative value ("B4", "=HOUR(-0.000001)"), // Slightly negative ("B5", "=MINUTE(-0.000001)"), // Slightly negative ("B6", "=SECOND(-0.000001)"), // Slightly negative ]); // Wrong argument count should return #ERROR! assert_eq!(model._get_text("A1"), *"#ERROR!"); assert_eq!(model._get_text("A2"), *"#ERROR!"); assert_eq!(model._get_text("A3"), *"#ERROR!"); assert_eq!(model._get_text("A4"), *"#ERROR!"); assert_eq!(model._get_text("A5"), *"#ERROR!"); assert_eq!(model._get_text("A6"), *"#ERROR!"); // Negative values should return #NUM! assert_eq!(model._get_text("B1"), *"#NUM!"); assert_eq!(model._get_text("B2"), *"#NUM!"); assert_eq!(model._get_text("B3"), *"#NUM!"); assert_eq!(model._get_text("B4"), *"#NUM!"); assert_eq!(model._get_text("B5"), *"#NUM!"); assert_eq!(model._get_text("B6"), *"#NUM!"); } #[test] fn test_time_functions_integration() { // Test how TIME, TIMEVALUE and component extraction functions work together let model = test_time_expressions(&[ // Create times with both functions ("A1", "=TIME(14,30,45)"), ("A2", "=TIMEVALUE(\"14:30:45\")"), // Extract components from TIME function results ("B1", "=HOUR(A1)"), ("B2", "=MINUTE(A1)"), ("B3", "=SECOND(A1)"), // Extract components from TIMEVALUE function results ("C1", "=HOUR(A2)"), ("C2", "=MINUTE(A2)"), ("C3", "=SECOND(A2)"), // Test additional TIME variations ("D1", "=TIME(14,0,0)"), // 14:00:00 ("E1", "=HOUR(D1)"), // Extract hour from 14:00:00 ("E2", "=MINUTE(D1)"), // Extract minute from 14:00:00 ("E3", "=SECOND(D1)"), // Extract second from 14:00:00 ]); // TIME and TIMEVALUE should produce equivalent results assert_eq!(model._get_text("A1"), model._get_text("A2")); // Extracting components should work consistently assert_eq!(model._get_text("B1"), *"14"); assert_eq!(model._get_text("B2"), *"30"); assert_eq!(model._get_text("B3"), *"45"); assert_eq!(model._get_text("C1"), *"14"); assert_eq!(model._get_text("C2"), *"30"); assert_eq!(model._get_text("C3"), *"45"); // Components from TIME(14,0,0) assert_eq!(model._get_text("E1"), *"14"); assert_eq!(model._get_text("E2"), *"0"); assert_eq!(model._get_text("E3"), *"0"); } #[test] fn test_time_function_extreme_values() { // Test missing edge cases: very large fractional inputs let model = test_time_expressions(&[ // Extremely large fractional values to TIME function ("A1", "=TIME(999999.9, 999999.9, 999999.9)"), // Very large fractional inputs ("A2", "=TIME(1e6, 1e6, 1e6)"), // Scientific notation inputs ("A3", "=TIME(0.000001, 0.000001, 0.000001)"), // Very small fractional inputs // Large day values for component extraction (stress test) ("B1", "=HOUR(999999.999)"), // Almost a million days ("B2", "=MINUTE(999999.999)"), ("B3", "=SECOND(999999.999)"), // Edge case: exactly 1.0 (should be midnight of next day) ("C1", "=HOUR(1.0)"), ("C2", "=MINUTE(1.0)"), ("C3", "=SECOND(1.0)"), // Very high precision values ("D1", "=HOUR(0.999999999999)"), // Almost exactly 24:00:00 ("D2", "=MINUTE(0.999999999999)"), ("D3", "=SECOND(0.999999999999)"), ]); // Large fractional inputs should be floored and normalized let result_a1 = model._get_text("A1").parse::().unwrap(); assert!( (0.0..1.0).contains(&result_a1), "Result should be valid time fraction" ); // Component extraction should work with very large values let hour_b1 = model._get_text("B1").parse::().unwrap(); assert!((0..=23).contains(&hour_b1), "Hour should be 0-23"); // Exactly 1.0 should be midnight (start of next day) assert_eq!(model._get_text("C1"), *"0"); assert_eq!(model._get_text("C2"), *"0"); assert_eq!(model._get_text("C3"), *"0"); // Very high precision should still extract valid components let hour_d1 = model._get_text("D1").parse::().unwrap(); assert!((0..=23).contains(&hour_d1), "Hour should be 0-23"); } #[test] fn test_timevalue_malformed_but_parseable() { // Test missing edge case: malformed but potentially parseable strings let model = test_time_expressions(&[ // Test various malformed but potentially parseable time strings ("A1", "=TIMEVALUE(\"14:30:00.123\")"), // Milliseconds (might be truncated) ("A2", "=TIMEVALUE(\"14:30:00.999\")"), // High precision milliseconds ("A3", "=TIMEVALUE(\"02:30:00\")"), // Leading zero hours ("A4", "=TIMEVALUE(\"2:05:00\")"), // Single digit hour, zero-padded minute // Boundary cases for AM/PM parsing ("B1", "=TIMEVALUE(\"11:59:59 PM\")"), // Just before midnight ("B2", "=TIMEVALUE(\"12:00:01 AM\")"), // Just after midnight ("B3", "=TIMEVALUE(\"12:00:01 PM\")"), // Just after noon ("B4", "=TIMEVALUE(\"11:59:59 AM\")"), // Just before noon // Test various date-time combinations ("C1", "=TIMEVALUE(\"2023-12-31T23:59:59\")"), // ISO format at year end ("C2", "=TIMEVALUE(\"2023-01-01 00:00:01\")"), // New year, just after midnight // Test potential edge cases that might still be parseable ("D1", "=TIMEVALUE(\"24:00:00\")"), // Should error (invalid hour) ("D2", "=TIMEVALUE(\"23:60:00\")"), // Should error (invalid minute) ("D3", "=TIMEVALUE(\"23:59:60\")"), // Should error (invalid second) ]); // Milliseconds are not supported, should return a #VALUE! error like Excel assert_eq!(model._get_text("A1"), *"#VALUE!"); assert_eq!(model._get_text("A2"), *"#VALUE!"); // Leading zeros should work fine assert_eq!(model._get_text("A3"), *TIME_2_30_AM); // 02:30:00 should parse as 2:30:00 // AM/PM boundary cases should work let result_b1 = model._get_text("B1").parse::().unwrap(); assert!( result_b1 > 0.99 && result_b1 < 1.0, "11:59:59 PM should be very close to 1.0" ); let result_b2 = model._get_text("B2").parse::().unwrap(); assert!( result_b2 > 0.0 && result_b2 < 0.01, "12:00:01 AM should be very close to 0.0" ); // ISO 8601 format with "T" separator should be parsed correctly assert_eq!(model._get_text("C1"), *TIME_23_59_59); // 23:59:59 → almost midnight assert_eq!(model._get_text("C2"), *TIME_00_00_01); // 00:00:01 → one second past midnight // Time parser normalizes edge cases to midnight (Excel compatibility) assert_eq!(model._get_text("D1"), *"0"); // 24:00:00 = midnight of next day assert_eq!(model._get_text("D2"), *"0"); // 23:60:00 normalizes to 24:00:00 = midnight assert_eq!(model._get_text("D3"), *"0"); // 23:59:60 normalizes to 24:00:00 = midnight } #[test] fn test_performance_stress_with_extreme_values() { // Test performance/stress cases with extreme values let model = test_time_expressions(&[ // Very large numbers that should still work ("A1", "=TIME(2147483647, 0, 0)"), // Max i32 hours ("A2", "=TIME(0, 2147483647, 0)"), // Max i32 minutes ("A3", "=TIME(0, 0, 2147483647)"), // Max i32 seconds // Component extraction with extreme day values ("B1", "=HOUR(1e15)"), // Very large day number ("B2", "=MINUTE(1e15)"), ("B3", "=SECOND(1e15)"), // Edge of floating point precision ("C1", "=HOUR(1.7976931348623157e+308)"), // Near max f64 ("C2", "=HOUR(2.2250738585072014e-308)"), // Near min positive f64 // Multiple TIME function calls with large values ("D1", "=TIME(1000000, 1000000, 1000000)"), // Large normalized values ("D2", "=HOUR(D1)"), // Extract from large TIME result ("D3", "=MINUTE(D1)"), ("D4", "=SECOND(D1)"), ]); // All results should be valid (not errors) even with extreme inputs for cell in ["A1", "A2", "A3", "B1", "B2", "B3", "D1", "D2", "D3", "D4"] { let result = model._get_text(cell); assert!( result != *"#ERROR!" && result != *"#NUM!" && result != *"#VALUE!", "Cell {cell} should not error with extreme values: {result}", ); } // Results should be mathematically valid let hour_b1 = model._get_text("B1").parse::().unwrap(); let minute_b2 = model._get_text("B2").parse::().unwrap(); let second_b3 = model._get_text("B3").parse::().unwrap(); assert!((0..=23).contains(&hour_b1)); assert!((0..=59).contains(&minute_b2)); assert!((0..=59).contains(&second_b3)); // TIME function results should be valid time fractions let time_d1 = model._get_text("D1").parse::().unwrap(); assert!( (0.0..1.0).contains(&time_d1), "TIME result should be valid fraction" ); }