shindo_coding_utils 0.2.8

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>,
{
    let s: &str = Deserialize::deserialize(deserializer)?;
    let mut now = Utc::now().date_naive();
    // Check if today is weekend
    if matches!(Utc::now().weekday(), Weekday::Sat | Weekday::Sun) {
        now = previous_working_day(now);
    }
    
    let time = NaiveTime::parse_from_str(s, "%H:%M:%S").map_err(serde::de::Error::custom)?;
    let naive = NaiveDateTime::new(now, time);
    Ok(Utc.from_utc_datetime(&naive))
}

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);
    }
}

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())
    );
}