shindo_coding_utils 0.3.9

A utils crates which will be used in various micro-services
Documentation
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc, Weekday};
use serde::{Deserialize, Deserializer};

pub async fn sleep(millis: u64) {
    tokio::time::sleep(std::time::Duration::from_millis(millis)).await;
}

pub fn multiply_by_1000_f64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = f64::deserialize(deserializer)?;

    Ok((value * 1000.0) as i64)
}

pub fn multiply_by_10_string<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    let value: f64 = s.parse().map_err(serde::de::Error::custom)?;
    Ok((value * 10.0) as i64)
}

pub fn multiply_by_1000_string<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    let value: f64 = s.parse().map_err(serde::de::Error::custom)?;
    Ok((value * 1000.0) as i64)
}

pub fn multiply_by_10_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = i64::deserialize(deserializer)?;
    Ok(value * 10)
}

pub fn format_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let timestamp_str = String::deserialize(deserializer)?;

    // Parse the string to a number
    let timestamp_ms = timestamp_str
        .parse::<i64>()
        .map_err(|e| serde::de::Error::custom(format!("Failed to parse timestamp: {}", e)))?;

    // Convert milliseconds to seconds and nanoseconds
    let secs = timestamp_ms / 1000;
    let nsecs = ((timestamp_ms % 1000) * 1_000_000) as u32;

    let datetime = DateTime::from_timestamp(secs, nsecs)
        .ok_or_else(|| serde::de::Error::custom("Invalid timestamp"))?;

    Ok(datetime)
}

pub fn parse_string_to_float<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    let value: f64 = s.parse().map_err(serde::de::Error::custom)?;
    Ok(value)
}

pub fn format_iso8601_datetime<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let iso8601_str = String::deserialize(deserializer)?;

    NaiveDateTime::parse_from_str(&iso8601_str, "%Y-%m-%dT%H:%M:%S%.f")
        .map_err(serde::de::Error::custom)
}

pub fn round_and_to_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    // First attempt to deserialize as Option<f64>
    let opt_value: Option<f64> = Option::deserialize(deserializer)?;

    match opt_value {
        Some(value) => {
            let rounded_value = value.round() as i64;
            Ok(rounded_value)
        }
        None => Ok(0), // Default value when input is None/null
    }
}

pub fn to_i64_and_multiply_by_1000<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let opt_value: Option<f64> = Option::deserialize(deserializer)?;

    match opt_value {
        Some(value) => {
            let rounded_value = (value * 1000.0).round() as i64;
            Ok(rounded_value)
        }
        None => Ok(0), // Default value when input is None/null
    }
}

pub fn next_working_day(mut date: DateTime<Utc>) -> DateTime<Utc> {
    date = date + Duration::days(1);
    while matches!(date.weekday(), Weekday::Sat | Weekday::Sun) {
        date = date + Duration::days(1);
    }
    date
}

pub fn previous_working_day(mut date: NaiveDate) -> NaiveDate {
    while matches!(date.weekday(), Weekday::Sat | Weekday::Sun) {
        date = date - Duration::days(1);
    }
    date
}

pub fn time_to_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: Deserializer<'de>,
{
    // Deserialize input as String (not &str, for better Serde compatibility)
    let s = String::deserialize(deserializer)?;

    // Cache now once to ensure consistency
    let now_utc = Utc::now();
    let mut local_date = now_utc.date_naive();
    // Use previous working day if today is Saturday or Sunday
    if matches!(now_utc.weekday(), Weekday::Sat | Weekday::Sun) {
        local_date = previous_working_day(local_date);
    }

    // Parse the input time string, expecting format HH:MM:SS
    let time = NaiveTime::parse_from_str(&s, "%H:%M:%S")
        .map_err(serde::de::Error::custom)?;
    let naive_local_dt = NaiveDateTime::new(local_date, time);

    // Attach timezone offset UTC+7
    let tz = chrono::FixedOffset::east_opt(7 * 3600)
        .ok_or_else(|| serde::de::Error::custom("invalid offset"))?;
    let dt_with_offset = tz.from_local_datetime(&naive_local_dt)
        .single()
        .ok_or_else(|| serde::de::Error::custom("ambiguous/nonexistent local datetime"))?;

    // Convert to UTC
    Ok(dt_with_offset.with_timezone(&Utc))
}


pub fn putthrough_order_side() -> String {
    "".to_string()
}

/// Remove these special characters
/// - \n
pub fn remove_special_character_from_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let text = String::deserialize(deserializer)?;
    Ok(text.trim().replace('\n', ""))
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{TimeZone, Utc, Weekday};

    #[test]
    fn next_working_day_monday() {
        let date = Utc.with_ymd_and_hms(2025, 7, 28, 12, 0, 0).unwrap(); // Monday
        let tomorrow = date + Duration::days(1);
        let result = next_working_day(date);
        assert_eq!(result.date_naive(), tomorrow.date_naive());
        assert_eq!(result.weekday(), Weekday::Tue);
    }

    #[test]
    fn test_previous_working_day_monday() {
        let date = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap(); // Monday
        let result = previous_working_day(date);
        assert_eq!(result, date);
        assert_eq!(result.weekday(), Weekday::Mon);
    }

    #[test]
    fn test_previous_working_day_sunday() {
        let date = NaiveDate::from_ymd_opt(2025, 7, 27).unwrap(); // Sunday
        let result = previous_working_day(date);
        assert_eq!(result, NaiveDate::from_ymd_opt(2025, 7, 25).unwrap()); // Friday
        assert_eq!(result.weekday(), Weekday::Fri);
    }

    #[test]
    fn test_previous_working_day_saturday() {
        let date = NaiveDate::from_ymd_opt(2024, 6, 8).unwrap(); // Saturday
        let result = previous_working_day(date);
        assert_eq!(result, NaiveDate::from_ymd_opt(2024, 6, 7).unwrap()); // Friday
        assert_eq!(result.weekday(), Weekday::Fri);
    }

    #[test]
    fn test_previous_working_day_wednesday() {
        let date = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap(); // Wednesday
        let result = previous_working_day(date);
        assert_eq!(result, date);
        assert_eq!(result.weekday(), Weekday::Wed);
    }
}

#[cfg(test)]
mod time_to_datetime_tests {
    use super::*;
    use serde::de::IntoDeserializer;
    use chrono::{NaiveDate, NaiveTime, Duration, Utc, TimeZone, Datelike, Weekday, Timelike};

    // Helper: performs conversion logic with explicit date and time (simulates internals of time_to_datetime)
    fn convert_time_to_utc(test_date: NaiveDate, input_time: &str) -> DateTime<Utc> {
        let time = NaiveTime::parse_from_str(input_time, "%H:%M:%S").unwrap();
        let naive_local_dt = NaiveDateTime::new(test_date, time);
        let tz = chrono::FixedOffset::east_opt(7 * 3600).unwrap();
        let dt_with_offset = tz.from_local_datetime(&naive_local_dt).single().unwrap();
        dt_with_offset.with_timezone(&Utc)
    }

    #[test]
    fn simple_weekday_conversion() {
        // Monday 2025-02-24
        let date = NaiveDate::from_ymd_opt(2025, 2, 24).unwrap();
        let input_time = "16:30:15";
        let result = convert_time_to_utc(date, input_time);
        assert_eq!(result.date_naive(), date);
        assert_eq!(result.hour(), 9); // 16 - 7
        assert_eq!(result.minute(), 30);
        assert_eq!(result.second(), 15);
    }

    #[test]
    fn midnight_edge_case() {
        let date = NaiveDate::from_ymd_opt(2025, 2, 24).unwrap();
        let input_time = "00:00:00";
        let result = convert_time_to_utc(date, input_time);
        // 00:00:00 +7 = previous day 17:00:00 UTC
        let expected = Utc.with_ymd_and_hms(2025, 2, 23, 17, 0, 0).unwrap();
        assert_eq!(result, expected);
    }

    #[test]
    fn sunday_uses_previous_friday() {
        // Sunday 2025-06-15
        let sunday = NaiveDate::from_ymd_opt(2025, 6, 15).unwrap();
        let prev_day = previous_working_day(sunday);
        assert_eq!(prev_day.weekday(), Weekday::Fri);
        assert_eq!(prev_day, NaiveDate::from_ymd_opt(2025, 6, 13).unwrap());
    }

    #[test]
    fn test_public_api_actual_date() {
        // For this test, we exercise the real public API for smoke
        let time_str = "08:30:00";
        let de = serde::de::value::StrDeserializer::<serde::de::value::Error>::new(time_str);
        let utc_dt = time_to_datetime(de).unwrap();
        // It's hard to assert the actual date as it depends on runtime, so just check plausible range
        assert_eq!(utc_dt.minute(), 30);
        assert_eq!(utc_dt.second(), 0);
        assert!(utc_dt.hour() <= 23);
    }

    #[test]
    fn handles_various_times() {
        let date = NaiveDate::from_ymd_opt(2025, 2, 24).unwrap();
        let cases = vec![
            ("00:00:00", 17, 0, 0), // UTC - shift
            ("07:00:00", 0, 0, 0),
            ("12:00:00", 5, 0, 0),
            ("23:59:59", 16, 59, 59),
        ];
        for (input, h, m, s) in cases {
            let result = convert_time_to_utc(date, input);
            assert_eq!(result.hour(), h);
            assert_eq!(result.minute(), m);
            assert_eq!(result.second(), s);
        }
    }
}

pub fn format_number_with_commas(n: i64) -> String {
    let mut s = n.abs().to_string();
    let mut result = String::new();
    let mut count = 0;
    while let Some(c) = s.pop() {
        if count != 0 && count % 3 == 0 {
            result.insert(0, ',');
        }
        result.insert(0, c);
        count += 1;
    }
    if n < 0 {
        result.insert(0, '-');
    }
    result
}
#[test]
fn test_format_number_with_commas_positive() {
    assert_eq!(format_number_with_commas(1398300), "1,398,300");
    assert_eq!(format_number_with_commas(1000), "1,000");
    assert_eq!(format_number_with_commas(12), "12");
    assert_eq!(format_number_with_commas(0), "0");
}

#[test]
fn test_format_number_with_commas_negative() {
    assert_eq!(format_number_with_commas(-1398300), "-1,398,300");
    assert_eq!(format_number_with_commas(-1000), "-1,000");
    assert_eq!(format_number_with_commas(-12), "-12");
    assert_eq!(format_number_with_commas(-1), "-1");
}

pub fn get_property_from_event<T>(
    payload: Option<serde_json::Value>,
    property_name: &str,
) -> Option<T>
where
    T: serde::de::DeserializeOwned,
{
    payload.and_then(|value| {
        value
            .get(property_name)
            .and_then(|prop| serde_json::from_value(prop.clone()).ok())
    })
}

#[test]
fn test_get_property_from_event() {
    let json_string = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into serde_json::Value.
    let payload: Option<serde_json::Value> = serde_json::from_str(json_string).unwrap();
    assert_eq!(
        get_property_from_event::<String>(payload, "name"),
        Some("John Doe".to_string())
    );
}