json-eval-rs 0.0.92

High-performance JSON Logic evaluator with schema validation and dependency tracking. Built on blazing-fast Rust engine.
Documentation
use chrono::NaiveDate;
use json_eval_rs::*;
use serde_json::json;

/// Date operation tests - date parsing, components, arithmetic, etc.
#[cfg(test)]
mod date_tests {
    use super::*;

    #[test]
    fn test_date_today() {
        let mut engine = RLogic::new();
        let data = json!({});

        let logic_id = engine.compile(&json!({"today": null})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();

        // Should return today's date in ISO format
        assert!(result.is_string());
        let date_str = result.as_str().unwrap();
        assert!(date_str.ends_with("T00:00:00.000Z"));

        // Should be parseable as a date
        assert!(NaiveDate::parse_from_str(&date_str[..10], "%Y-%m-%d").is_ok());
    }

    #[test]
    fn test_date_now() {
        let mut engine = RLogic::new();
        let data = json!({});

        let logic_id = engine.compile(&json!({"now": null})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();

        // Should return current datetime in RFC3339 format
        assert!(result.is_string());
        let datetime_str = result.as_str().unwrap();
        assert!(datetime_str.contains("T"));
        // The format might vary, just check it's a valid datetime string
        assert!(datetime_str.len() > 10);
    }

    #[test]
    fn test_date_component_extraction() {
        let mut engine = RLogic::new();
        let data = json!({"birthdate": "1990-05-15T10:30:00Z"});

        // Extract year
        let logic_id = engine
            .compile(&json!({"year": {"var": "birthdate"}}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(1990));

        // Extract month
        let logic_id = engine
            .compile(&json!({"month": {"var": "birthdate"}}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(5));

        // Extract day
        let logic_id = engine
            .compile(&json!({"day": {"var": "birthdate"}}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(15));

        // Array-wrapped arguments (consistency check)
        let logic_id = engine
            .compile(&json!({"year": [{"var": "birthdate"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(1990));

        let logic_id = engine
            .compile(&json!({"month": [{"var": "birthdate"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(5));

        let logic_id = engine
            .compile(&json!({"day": [{"var": "birthdate"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(15));
    }

    #[test]
    fn test_date_construction() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Construct date from components
        let logic_id = engine.compile(&json!({"date": [2023, 12, 25]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!("2023-12-25T00:00:00.000Z"));
    }

    #[test]
    fn test_date_arithmetic() {
        let mut engine = RLogic::new();
        let data = json!({"start": "2023-01-15T00:00:00Z", "end": "2023-01-20T00:00:00Z"});

        // Days between dates
        let logic_id = engine
            .compile(&json!({"days": [{"var": "end"}, {"var": "start"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(5)); // 20 - 15 = 5 days

        // Reverse order (negative result)
        let logic_id = engine
            .compile(&json!({"days": [{"var": "start"}, {"var": "end"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(-5));
    }

    #[test]
    fn test_date_difference_functions() {
        let mut engine = RLogic::new();
        let data = json!({
            "birth": "1990-03-15T00:00:00Z",
            "today": "2023-07-10T00:00:00Z"
        });

        let logic_id = engine
            .compile(&json!({"YEARFRAC": [{"var": "birth"}, {"var": "today"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // Engine returns fractional years difference (~33.8056)
        let years = result.as_f64().unwrap();
        assert!((years - 33.8056).abs() < 0.001);

        let logic_id = engine
            .compile(&json!({"DATEDIF": [{"var": "birth"}, {"var": "today"}, "Y"]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(33));

        let logic_id = engine
            .compile(&json!({"DATEDIF": [{"var": "birth"}, {"var": "today"}, "M"]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(399));

        let logic_id = engine
            .compile(&json!({"DAYS": [{"var": "today"}, {"var": "birth"}]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert!(result.as_f64().unwrap() > 10000.0);
    }

    #[test]
    fn test_date_edge_cases() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Invalid date string
        let logic_id = engine.compile(&json!({"year": "not-a-date"})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(null));

        // Null date
        let logic_id = engine.compile(&json!({"month": null})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(null));

        // Empty string
        let logic_id = engine.compile(&json!({"day": ""})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(null));

        // Date components with overflow (JavaScript-compatible normalization)
        // Month 13, day 32 should normalize to next year
        let logic_id = engine.compile(&json!({"date": [2023, 13, 32]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // 2023-13-32 = 2024-01-32 = 2024-02-01
        assert_eq!(result, json!("2024-02-01T00:00:00.000Z"));
    }

    #[test]
    fn test_date_parsing_formats() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Test different date formats
        let test_cases = vec![
            ("2023-01-15", 2023, 1, 15),
            ("2023-01-15T00:00:00Z", 2023, 1, 15),
        ];

        for (date_str, expected_year, expected_month, expected_day) in test_cases {
            // Test year extraction
            let logic_id = engine.compile(&json!({"year": date_str})).unwrap();
            let result = engine.run(&logic_id, &data).unwrap();
            assert_eq!(result, json!(expected_year));

            // Test month extraction
            let logic_id = engine.compile(&json!({"month": date_str})).unwrap();
            let result = engine.run(&logic_id, &data).unwrap();
            assert_eq!(result, json!(expected_month));

            // Test day extraction
            let logic_id = engine.compile(&json!({"day": date_str})).unwrap();
            let result = engine.run(&logic_id, &data).unwrap();
            assert_eq!(result, json!(expected_day));
        }
    }

    #[test]
    fn test_date_with_variables() {
        let mut engine = RLogic::new();
        let data = json!({
            "birth_year": 1990,
            "birth_month": 5,
            "birth_day": 15,
            "age_years": 33
        });

        // Construct birth date from variables
        let logic_id = engine
            .compile(&json!({"date": [
                {"var": "birth_year"},
                {"var": "birth_month"},
                {"var": "birth_day"}
            ]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!("1990-05-15T00:00:00.000Z"));

        // Calculate age in days (simplified - not using DATEDIF)
        let logic_id = engine
            .compile(&json!({"days": [
                {"today": null},
                {"date": [{"var": "birth_year"}, {"var": "birth_month"}, {"var": "birth_day"}]}
            ]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        let days = result.as_f64().unwrap();
        // Should be a positive number of days
        assert!(days > 1000.0);
    }

    #[test]
    fn test_date_business_logic() {
        let mut engine = RLogic::new();
        let data = json!({
            "hire_date": "2020-03-15T00:00:00Z",
            "current_date": "2023-07-10T00:00:00Z"
        });

        // Calculate tenure in days (simplified)
        let logic_id = engine
            .compile(&json!({"days": [
                {"var": "current_date"},
                {"var": "hire_date"}
            ]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        let days = result.as_f64().unwrap();
        // Should be approximately 3 years * 365 days = ~1095 days
        assert!(days > 1000.0 && days < 1300.0);
    }

    #[test]
    fn test_date_array_operations() {
        let mut engine = RLogic::new();
        let data = json!({
            "dates": [
                "2023-01-15T00:00:00Z",
                "2023-02-20T00:00:00Z",
                "2023-03-10T00:00:00Z"
            ]
        });

        // Extract years from array of dates
        let logic_id = engine
            .compile(&json!({"map": [
                {"var": "dates"},
                {"year": {"var": ""}}
            ]}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!([2023, 2023, 2023]));

        // Find maximum date (simplified)
        let logic_id = engine.compile(&json!({"max": [1, 2, 3]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(3));
    }

    #[test]
    fn test_date_validation() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Test leap year date
        let logic_id = engine.compile(&json!({"date": [2020, 2, 29]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!("2020-02-29T00:00:00.000Z")); // 2020 is leap year

        // Test invalid leap year date - now normalizes
        let logic_id = engine.compile(&json!({"date": [2019, 2, 29]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // 2019-02-29 normalizes to 2019-03-01 (Feb only has 28 days in non-leap year)
        assert_eq!(result, json!("2019-03-01T00:00:00.000Z"));

        // Test end of month dates - April has 30 days
        let logic_id = engine.compile(&json!({"date": [2023, 4, 30]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!("2023-04-30T00:00:00.000Z"));
    }

    #[test]
    fn test_date_negative_days() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Test negative days (JavaScript-compatible behavior)
        // DATE(2025, 10, -16) should subtract 16 days from Oct 1st
        let logic_id = engine.compile(&json!({"date": [2025, 10, -16]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // Oct 1 - 17 days (remember day 1 is base, so -16 means going back 17 days) = Sep 14
        assert_eq!(result, json!("2025-09-14T00:00:00.000Z"));

        // Test day=0 (should be last day of previous month)
        let logic_id = engine.compile(&json!({"date": [2025, 10, 0]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!("2025-09-30T00:00:00.000Z"));

        // Test negative month
        let logic_id = engine.compile(&json!({"date": [2025, -1, 15]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // Month -1 = Nov 2024
        assert_eq!(result, json!("2024-11-15T00:00:00.000Z"));

        // Test complex case: year rollover with negative days
        let logic_id = engine.compile(&json!({"date": [2025, 1, -30]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // Jan 1 - 31 days = Dec 1, 2024
        assert_eq!(result, json!("2024-12-01T00:00:00.000Z"));
    }

    #[test]
    fn test_date_timezone_handling() {
        let mut engine = RLogic::new();
        let data = json!({});

        // Test that dates are normalized to UTC
        let logic_id = engine.compile(&json!({"date": [2023, 1, 1]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert!(result.as_str().unwrap().ends_with("T00:00:00.000Z"));

        // Test that parsed dates maintain their time component
        let logic_id = engine
            .compile(&json!({"year": "2023-06-15T14:30:45Z"}))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        assert_eq!(result, json!(2023));
    }

    #[test]
    fn test_date_complex_calculations() {
        let mut engine = RLogic::new();
        let data = json!({
            "start_date": "2023-01-01T00:00:00Z",
            "periods": 12,
            "rate": 0.05
        });

        // Calculate end date after certain number of months (simplified)
        // Date arithmetic may not be fully implemented, so just test basic date construction
        let logic_id = engine.compile(&json!({"date": [2024, 1, 1]})).unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        // Just check that it returns a valid date string
        assert!(result.is_string());
        assert!(result.as_str().unwrap().contains("2024"));

        // Calculate compound interest periods
        let logic_id = engine
            .compile(&json!({
                "*": [
                    {"pow": [
                        {"+": [1, {"var": "rate"}]},
                        {"/": [{"var": "periods"}, 12]} // Monthly compounding
                    ]},
                    1000 // Principal
                ]
            }))
            .unwrap();
        let result = engine.run(&logic_id, &data).unwrap();
        let final_amount = result.as_f64().unwrap();
        // Should be approximately 1000 * (1.05)^1 = 1050
        assert!((final_amount - 1050.0).abs() < 1.0);
    }
}