time_format.rs

   1use time::{OffsetDateTime, UtcOffset};
   2
   3/// The formatting style for a timestamp.
   4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
   5pub enum TimestampFormat {
   6    /// Formats the timestamp as an absolute time, e.g. "2021-12-31 3:00AM".
   7    Absolute,
   8    /// Formats the timestamp as an absolute time.
   9    /// If the message is from today or yesterday the date will be replaced with "Today at x" or "Yesterday at x" respectively.
  10    /// E.g. "Today at 12:00 PM", "Yesterday at 11:00 AM", "2021-12-31 3:00AM".
  11    EnhancedAbsolute,
  12    /// Formats the timestamp as an absolute time, using month name, day of month, year. e.g. "Feb. 24, 2024".
  13    MediumAbsolute,
  14    /// Formats the timestamp as a relative time, e.g. "just now", "1 minute ago", "2 hours ago", "2 months ago".
  15    Relative,
  16}
  17
  18/// Formats a timestamp, which respects the user's date and time preferences/custom format.
  19pub fn format_localized_timestamp(
  20    timestamp: OffsetDateTime,
  21    reference: OffsetDateTime,
  22    timezone: UtcOffset,
  23    format: TimestampFormat,
  24) -> String {
  25    let timestamp_local = timestamp.to_offset(timezone);
  26    let reference_local = reference.to_offset(timezone);
  27    format_local_timestamp(timestamp_local, reference_local, format)
  28}
  29
  30/// Formats a timestamp, which respects the user's date and time preferences/custom format.
  31pub fn format_local_timestamp(
  32    timestamp: OffsetDateTime,
  33    reference: OffsetDateTime,
  34    format: TimestampFormat,
  35) -> String {
  36    match format {
  37        TimestampFormat::Absolute => format_absolute_timestamp(timestamp, reference, false),
  38        TimestampFormat::EnhancedAbsolute => format_absolute_timestamp(timestamp, reference, true),
  39        TimestampFormat::MediumAbsolute => format_absolute_timestamp_medium(timestamp, reference),
  40        TimestampFormat::Relative => format_relative_time(timestamp, reference)
  41            .unwrap_or_else(|| format_relative_date(timestamp, reference)),
  42    }
  43}
  44
  45/// Formats the date component of a timestamp
  46pub fn format_date(
  47    timestamp: OffsetDateTime,
  48    reference: OffsetDateTime,
  49    enhanced_formatting: bool,
  50) -> String {
  51    format_absolute_date(timestamp, reference, enhanced_formatting)
  52}
  53
  54/// Formats the time component of a timestamp
  55pub fn format_time(timestamp: OffsetDateTime) -> String {
  56    format_absolute_time(timestamp)
  57}
  58
  59/// Formats the date component of a timestamp in medium style
  60pub fn format_date_medium(
  61    timestamp: OffsetDateTime,
  62    reference: OffsetDateTime,
  63    enhanced_formatting: bool,
  64) -> String {
  65    format_absolute_date_medium(timestamp, reference, enhanced_formatting)
  66}
  67
  68fn format_absolute_date(
  69    timestamp: OffsetDateTime,
  70    reference: OffsetDateTime,
  71    #[allow(unused_variables)] enhanced_date_formatting: bool,
  72) -> String {
  73    #[cfg(target_os = "macos")]
  74    {
  75        if !enhanced_date_formatting {
  76            return macos::format_date(&timestamp);
  77        }
  78
  79        let timestamp_date = timestamp.date();
  80        let reference_date = reference.date();
  81        if timestamp_date == reference_date {
  82            "Today".to_string()
  83        } else if reference_date.previous_day() == Some(timestamp_date) {
  84            "Yesterday".to_string()
  85        } else {
  86            macos::format_date(&timestamp)
  87        }
  88    }
  89    #[cfg(not(target_os = "macos"))]
  90    {
  91        // todo(linux) respect user's date/time preferences
  92        // todo(windows) respect user's date/time preferences
  93        let current_locale = CURRENT_LOCALE
  94            .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
  95        format_timestamp_naive_date(
  96            timestamp,
  97            reference,
  98            is_12_hour_time_by_locale(current_locale.as_str()),
  99        )
 100    }
 101}
 102
 103fn format_absolute_time(timestamp: OffsetDateTime) -> String {
 104    #[cfg(target_os = "macos")]
 105    {
 106        macos::format_time(&timestamp)
 107    }
 108    #[cfg(not(target_os = "macos"))]
 109    {
 110        // todo(linux) respect user's date/time preferences
 111        // todo(windows) respect user's date/time preferences
 112        let current_locale = CURRENT_LOCALE
 113            .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
 114        format_timestamp_naive_time(
 115            timestamp,
 116            is_12_hour_time_by_locale(current_locale.as_str()),
 117        )
 118    }
 119}
 120
 121fn format_absolute_timestamp(
 122    timestamp: OffsetDateTime,
 123    reference: OffsetDateTime,
 124    #[allow(unused_variables)] enhanced_date_formatting: bool,
 125) -> String {
 126    #[cfg(target_os = "macos")]
 127    {
 128        if !enhanced_date_formatting {
 129            return format!(
 130                "{} {}",
 131                format_absolute_date(timestamp, reference, enhanced_date_formatting),
 132                format_absolute_time(timestamp)
 133            );
 134        }
 135
 136        let timestamp_date = timestamp.date();
 137        let reference_date = reference.date();
 138        if timestamp_date == reference_date {
 139            format!("Today at {}", format_absolute_time(timestamp))
 140        } else if reference_date.previous_day() == Some(timestamp_date) {
 141            format!("Yesterday at {}", format_absolute_time(timestamp))
 142        } else {
 143            format!(
 144                "{} {}",
 145                format_absolute_date(timestamp, reference, enhanced_date_formatting),
 146                format_absolute_time(timestamp)
 147            )
 148        }
 149    }
 150    #[cfg(not(target_os = "macos"))]
 151    {
 152        // todo(linux) respect user's date/time preferences
 153        // todo(windows) respect user's date/time preferences
 154        format_timestamp_fallback(timestamp, reference)
 155    }
 156}
 157
 158fn format_absolute_date_medium(
 159    timestamp: OffsetDateTime,
 160    reference: OffsetDateTime,
 161    enhanced_formatting: bool,
 162) -> String {
 163    #[cfg(target_os = "macos")]
 164    {
 165        if !enhanced_formatting {
 166            return macos::format_date_medium(&timestamp);
 167        }
 168
 169        let timestamp_date = timestamp.date();
 170        let reference_date = reference.date();
 171        if timestamp_date == reference_date {
 172            "Today".to_string()
 173        } else if reference_date.previous_day() == Some(timestamp_date) {
 174            "Yesterday".to_string()
 175        } else {
 176            macos::format_date_medium(&timestamp)
 177        }
 178    }
 179    #[cfg(not(target_os = "macos"))]
 180    {
 181        // todo(linux) respect user's date/time preferences
 182        // todo(windows) respect user's date/time preferences
 183        let current_locale = CURRENT_LOCALE
 184            .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
 185        if !enhanced_formatting {
 186            return format_timestamp_naive_date_medium(
 187                timestamp,
 188                is_12_hour_time_by_locale(current_locale.as_str()),
 189            );
 190        }
 191
 192        let timestamp_date = timestamp.date();
 193        let reference_date = reference.date();
 194        if timestamp_date == reference_date {
 195            "Today".to_string()
 196        } else if reference_date.previous_day() == Some(timestamp_date) {
 197            "Yesterday".to_string()
 198        } else {
 199            format_timestamp_naive_date_medium(
 200                timestamp,
 201                is_12_hour_time_by_locale(current_locale.as_str()),
 202            )
 203        }
 204    }
 205}
 206
 207fn format_absolute_timestamp_medium(
 208    timestamp: OffsetDateTime,
 209    reference: OffsetDateTime,
 210) -> String {
 211    #[cfg(target_os = "macos")]
 212    {
 213        format_absolute_date_medium(timestamp, reference, false)
 214    }
 215    #[cfg(not(target_os = "macos"))]
 216    {
 217        // todo(linux) respect user's date/time preferences
 218        // todo(windows) respect user's date/time preferences
 219        format_timestamp_fallback(timestamp, reference)
 220    }
 221}
 222
 223fn format_relative_time(timestamp: OffsetDateTime, reference: OffsetDateTime) -> Option<String> {
 224    let difference = reference - timestamp;
 225    let minutes = difference.whole_minutes();
 226    match minutes {
 227        0 => Some("Just now".to_string()),
 228        1 => Some("1 minute ago".to_string()),
 229        2..=59 => Some(format!("{} minutes ago", minutes)),
 230        _ => {
 231            let hours = difference.whole_hours();
 232            match hours {
 233                1 => Some("1 hour ago".to_string()),
 234                2..=23 => Some(format!("{} hours ago", hours)),
 235                _ => None,
 236            }
 237        }
 238    }
 239}
 240
 241fn format_relative_date(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String {
 242    let timestamp_date = timestamp.date();
 243    let reference_date = reference.date();
 244    let difference = reference_date - timestamp_date;
 245    let days = difference.whole_days();
 246    match days {
 247        0 => "Today".to_string(),
 248        1 => "Yesterday".to_string(),
 249        2..=6 => format!("{} days ago", days),
 250        _ => {
 251            let weeks = difference.whole_weeks();
 252            match weeks {
 253                1 => "1 week ago".to_string(),
 254                2..=4 => format!("{} weeks ago", weeks),
 255                _ => {
 256                    let month_diff = calculate_month_difference(timestamp, reference);
 257                    match month_diff {
 258                        0..=1 => "1 month ago".to_string(),
 259                        2..=11 => format!("{} months ago", month_diff),
 260                        months => {
 261                            let years = months / 12;
 262                            match years {
 263                                1 => "1 year ago".to_string(),
 264                                _ => format!("{years} years ago"),
 265                            }
 266                        }
 267                    }
 268                }
 269            }
 270        }
 271    }
 272}
 273
 274/// Calculates the difference in months between two timestamps.
 275/// The reference timestamp should always be greater than the timestamp.
 276fn calculate_month_difference(timestamp: OffsetDateTime, reference: OffsetDateTime) -> usize {
 277    let timestamp_year = timestamp.year();
 278    let reference_year = reference.year();
 279    let timestamp_month: u8 = timestamp.month().into();
 280    let reference_month: u8 = reference.month().into();
 281
 282    let month_diff = if reference_month >= timestamp_month {
 283        reference_month as usize - timestamp_month as usize
 284    } else {
 285        12 - timestamp_month as usize + reference_month as usize
 286    };
 287
 288    let year_diff = (reference_year - timestamp_year) as usize;
 289    if year_diff == 0 {
 290        reference_month as usize - timestamp_month as usize
 291    } else if month_diff == 0 {
 292        year_diff * 12
 293    } else if timestamp_month > reference_month {
 294        (year_diff - 1) * 12 + month_diff
 295    } else {
 296        year_diff * 12 + month_diff
 297    }
 298}
 299
 300/// Formats a timestamp, which is either in 12-hour or 24-hour time format.
 301/// Note:
 302/// This function does not respect the user's date and time preferences.
 303/// This should only be used as a fallback mechanism when the OS time formatting fails.
 304fn format_timestamp_naive_time(timestamp_local: OffsetDateTime, is_12_hour_time: bool) -> String {
 305    let timestamp_local_hour = timestamp_local.hour();
 306    let timestamp_local_minute = timestamp_local.minute();
 307
 308    let (hour, meridiem) = if is_12_hour_time {
 309        let meridiem = if timestamp_local_hour >= 12 {
 310            "PM"
 311        } else {
 312            "AM"
 313        };
 314
 315        let hour_12 = match timestamp_local_hour {
 316            0 => 12,                              // Midnight
 317            13..=23 => timestamp_local_hour - 12, // PM hours
 318            _ => timestamp_local_hour,            // AM hours
 319        };
 320
 321        (hour_12, Some(meridiem))
 322    } else {
 323        (timestamp_local_hour, None)
 324    };
 325
 326    match meridiem {
 327        Some(meridiem) => format!("{}:{:02} {}", hour, timestamp_local_minute, meridiem),
 328        None => format!("{:02}:{:02}", hour, timestamp_local_minute),
 329    }
 330}
 331
 332#[cfg(not(target_os = "macos"))]
 333fn format_timestamp_naive_date(
 334    timestamp_local: OffsetDateTime,
 335    reference_local: OffsetDateTime,
 336    is_12_hour_time: bool,
 337) -> String {
 338    let reference_local_date = reference_local.date();
 339    let timestamp_local_date = timestamp_local.date();
 340
 341    if timestamp_local_date == reference_local_date {
 342        "Today".to_string()
 343    } else if reference_local_date.previous_day() == Some(timestamp_local_date) {
 344        "Yesterday".to_string()
 345    } else {
 346        match is_12_hour_time {
 347            true => format!(
 348                "{:02}/{:02}/{}",
 349                timestamp_local_date.month() as u32,
 350                timestamp_local_date.day(),
 351                timestamp_local_date.year()
 352            ),
 353            false => format!(
 354                "{:02}/{:02}/{}",
 355                timestamp_local_date.day(),
 356                timestamp_local_date.month() as u32,
 357                timestamp_local_date.year()
 358            ),
 359        }
 360    }
 361}
 362
 363#[cfg(not(target_os = "macos"))]
 364fn format_timestamp_naive_date_medium(
 365    timestamp_local: OffsetDateTime,
 366    is_12_hour_time: bool,
 367) -> String {
 368    let timestamp_local_date = timestamp_local.date();
 369
 370    match is_12_hour_time {
 371        true => format!(
 372            "{:02}/{:02}/{}",
 373            timestamp_local_date.month() as u32,
 374            timestamp_local_date.day(),
 375            timestamp_local_date.year()
 376        ),
 377        false => format!(
 378            "{:02}/{:02}/{}",
 379            timestamp_local_date.day(),
 380            timestamp_local_date.month() as u32,
 381            timestamp_local_date.year()
 382        ),
 383    }
 384}
 385
 386pub fn format_timestamp_naive(
 387    timestamp_local: OffsetDateTime,
 388    reference_local: OffsetDateTime,
 389    is_12_hour_time: bool,
 390) -> String {
 391    let formatted_time = format_timestamp_naive_time(timestamp_local, is_12_hour_time);
 392    let reference_local_date = reference_local.date();
 393    let timestamp_local_date = timestamp_local.date();
 394
 395    if timestamp_local_date == reference_local_date {
 396        format!("Today at {}", formatted_time)
 397    } else if reference_local_date.previous_day() == Some(timestamp_local_date) {
 398        format!("Yesterday at {}", formatted_time)
 399    } else {
 400        let formatted_date = match is_12_hour_time {
 401            true => format!(
 402                "{:02}/{:02}/{}",
 403                timestamp_local_date.month() as u32,
 404                timestamp_local_date.day(),
 405                timestamp_local_date.year()
 406            ),
 407            false => format!(
 408                "{:02}/{:02}/{}",
 409                timestamp_local_date.day(),
 410                timestamp_local_date.month() as u32,
 411                timestamp_local_date.year()
 412            ),
 413        };
 414        format!("{} {}", formatted_date, formatted_time)
 415    }
 416}
 417
 418#[cfg(not(target_os = "macos"))]
 419static CURRENT_LOCALE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
 420
 421#[cfg(not(target_os = "macos"))]
 422fn format_timestamp_fallback(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String {
 423    let current_locale = CURRENT_LOCALE
 424        .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
 425
 426    let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str());
 427    format_timestamp_naive(timestamp, reference, is_12_hour_time)
 428}
 429
 430/// Returns `true` if the locale is recognized as a 12-hour time locale.
 431#[cfg(not(target_os = "macos"))]
 432fn is_12_hour_time_by_locale(locale: &str) -> bool {
 433    [
 434        "es-MX", "es-CO", "es-SV", "es-NI",
 435        "es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras
 436        "en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand
 437        "ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan
 438        "en-IN", "hi-IN", // India, Hindu
 439        "en-PK", "ur-PK", // Pakistan, Urdu
 440        "en-PH", "fil-PH", // Philippines, Filipino
 441        "bn-BD", "ccp-BD", // Bangladesh, Chakma
 442        "en-IE", "ga-IE", // Ireland, Irish
 443        "en-MY", "ms-MY", // Malaysia, Malay
 444    ]
 445    .contains(&locale)
 446}
 447
 448#[cfg(target_os = "macos")]
 449mod macos {
 450    use core_foundation::base::TCFType;
 451    use core_foundation::date::CFAbsoluteTime;
 452    use core_foundation::string::CFString;
 453    use core_foundation_sys::date_formatter::CFDateFormatterCreateStringWithAbsoluteTime;
 454    use core_foundation_sys::date_formatter::CFDateFormatterRef;
 455    use core_foundation_sys::locale::CFLocaleRef;
 456    use core_foundation_sys::{
 457        base::kCFAllocatorDefault,
 458        date_formatter::{
 459            CFDateFormatterCreate, kCFDateFormatterMediumStyle, kCFDateFormatterNoStyle,
 460            kCFDateFormatterShortStyle,
 461        },
 462        locale::CFLocaleCopyCurrent,
 463    };
 464
 465    pub fn format_time(timestamp: &time::OffsetDateTime) -> String {
 466        format_with_date_formatter(timestamp, TIME_FORMATTER.with(|f| *f))
 467    }
 468
 469    pub fn format_date(timestamp: &time::OffsetDateTime) -> String {
 470        format_with_date_formatter(timestamp, DATE_FORMATTER.with(|f| *f))
 471    }
 472
 473    pub fn format_date_medium(timestamp: &time::OffsetDateTime) -> String {
 474        format_with_date_formatter(timestamp, MEDIUM_DATE_FORMATTER.with(|f| *f))
 475    }
 476
 477    fn format_with_date_formatter(
 478        timestamp: &time::OffsetDateTime,
 479        fmt: CFDateFormatterRef,
 480    ) -> String {
 481        const UNIX_TO_CF_ABSOLUTE_TIME_OFFSET: i64 = 978307200;
 482        // Convert timestamp to macOS absolute time
 483        let timestamp_macos = timestamp.unix_timestamp() - UNIX_TO_CF_ABSOLUTE_TIME_OFFSET;
 484        let cf_absolute_time = timestamp_macos as CFAbsoluteTime;
 485        unsafe {
 486            let s = CFDateFormatterCreateStringWithAbsoluteTime(
 487                kCFAllocatorDefault,
 488                fmt,
 489                cf_absolute_time,
 490            );
 491            CFString::wrap_under_create_rule(s).to_string()
 492        }
 493    }
 494
 495    thread_local! {
 496        static CURRENT_LOCALE: CFLocaleRef = unsafe { CFLocaleCopyCurrent() };
 497        static TIME_FORMATTER: CFDateFormatterRef = unsafe {
 498            CFDateFormatterCreate(
 499                kCFAllocatorDefault,
 500                CURRENT_LOCALE.with(|locale| *locale),
 501                kCFDateFormatterNoStyle,
 502                kCFDateFormatterShortStyle,
 503            )
 504        };
 505        static DATE_FORMATTER: CFDateFormatterRef = unsafe {
 506            CFDateFormatterCreate(
 507                kCFAllocatorDefault,
 508                CURRENT_LOCALE.with(|locale| *locale),
 509                kCFDateFormatterShortStyle,
 510                kCFDateFormatterNoStyle,
 511            )
 512        };
 513
 514        static MEDIUM_DATE_FORMATTER: CFDateFormatterRef = unsafe {
 515            CFDateFormatterCreate(
 516                kCFAllocatorDefault,
 517                CURRENT_LOCALE.with(|locale| *locale),
 518                kCFDateFormatterMediumStyle,
 519                kCFDateFormatterNoStyle,
 520            )
 521        };
 522    }
 523}
 524
 525#[cfg(test)]
 526mod tests {
 527    use super::*;
 528
 529    #[test]
 530    fn test_format_date() {
 531        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 532
 533        // Test with same date (today)
 534        let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 535        assert_eq!(format_date(timestamp_today, reference, true), "Today");
 536
 537        // Test with previous day (yesterday)
 538        let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0);
 539        assert_eq!(
 540            format_date(timestamp_yesterday, reference, true),
 541            "Yesterday"
 542        );
 543
 544        // Test with other date
 545        let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0);
 546        let result = format_date(timestamp_other, reference, true);
 547        assert!(!result.is_empty());
 548        assert_ne!(result, "Today");
 549        assert_ne!(result, "Yesterday");
 550    }
 551
 552    #[test]
 553    fn test_format_time() {
 554        let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 555
 556        // We can't assert the exact output as it depends on the platform and locale
 557        // But we can at least confirm it doesn't panic and returns a non-empty string
 558        let result = format_time(timestamp);
 559        assert!(!result.is_empty());
 560    }
 561
 562    #[test]
 563    fn test_format_date_medium() {
 564        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 565        let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 566
 567        // Test with enhanced formatting (today)
 568        let result_enhanced = format_date_medium(timestamp, reference, true);
 569        assert_eq!(result_enhanced, "Today");
 570
 571        // Test with standard formatting
 572        let result_standard = format_date_medium(timestamp, reference, false);
 573        assert!(!result_standard.is_empty());
 574
 575        // Test yesterday with enhanced formatting
 576        let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0);
 577        let result_yesterday = format_date_medium(timestamp_yesterday, reference, true);
 578        assert_eq!(result_yesterday, "Yesterday");
 579
 580        // Test other date with enhanced formatting
 581        let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0);
 582        let result_other = format_date_medium(timestamp_other, reference, true);
 583        assert!(!result_other.is_empty());
 584        assert_ne!(result_other, "Today");
 585        assert_ne!(result_other, "Yesterday");
 586    }
 587
 588    #[test]
 589    fn test_format_absolute_time() {
 590        let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 591
 592        // We can't assert the exact output as it depends on the platform and locale
 593        // But we can at least confirm it doesn't panic and returns a non-empty string
 594        let result = format_absolute_time(timestamp);
 595        assert!(!result.is_empty());
 596    }
 597
 598    #[test]
 599    fn test_format_absolute_date() {
 600        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 601
 602        // Test with same date (today)
 603        let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 604        assert_eq!(
 605            format_absolute_date(timestamp_today, reference, true),
 606            "Today"
 607        );
 608
 609        // Test with previous day (yesterday)
 610        let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0);
 611        assert_eq!(
 612            format_absolute_date(timestamp_yesterday, reference, true),
 613            "Yesterday"
 614        );
 615
 616        // Test with other date
 617        let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0);
 618        let result = format_absolute_date(timestamp_other, reference, true);
 619        assert!(!result.is_empty());
 620        assert_ne!(result, "Today");
 621        assert_ne!(result, "Yesterday");
 622    }
 623
 624    #[test]
 625    fn test_format_absolute_date_medium() {
 626        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 627        let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 628
 629        // Test with enhanced formatting (today)
 630        let result_enhanced = format_absolute_date_medium(timestamp, reference, true);
 631        assert_eq!(result_enhanced, "Today");
 632
 633        // Test with standard formatting
 634        let result_standard = format_absolute_date_medium(timestamp, reference, false);
 635        assert!(!result_standard.is_empty());
 636
 637        // Test yesterday with enhanced formatting
 638        let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0);
 639        let result_yesterday = format_absolute_date_medium(timestamp_yesterday, reference, true);
 640        assert_eq!(result_yesterday, "Yesterday");
 641    }
 642
 643    #[test]
 644    fn test_format_timestamp_naive_time() {
 645        let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0);
 646        assert_eq!(format_timestamp_naive_time(timestamp, true), "9:30 AM");
 647        assert_eq!(format_timestamp_naive_time(timestamp, false), "09:30");
 648
 649        let timestamp_pm = create_offset_datetime(1990, 4, 12, 15, 45, 0);
 650        assert_eq!(format_timestamp_naive_time(timestamp_pm, true), "3:45 PM");
 651        assert_eq!(format_timestamp_naive_time(timestamp_pm, false), "15:45");
 652    }
 653
 654    #[test]
 655    fn test_format_24_hour_time() {
 656        let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
 657        let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
 658
 659        assert_eq!(
 660            format_timestamp_naive(timestamp, reference, false),
 661            "Today at 15:30"
 662        );
 663    }
 664
 665    #[test]
 666    fn test_format_today() {
 667        let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
 668        let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
 669
 670        assert_eq!(
 671            format_timestamp_naive(timestamp, reference, true),
 672            "Today at 3:30 PM"
 673        );
 674    }
 675
 676    #[test]
 677    fn test_format_yesterday() {
 678        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 679        let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0);
 680
 681        assert_eq!(
 682            format_timestamp_naive(timestamp, reference, true),
 683            "Yesterday at 9:00 AM"
 684        );
 685    }
 686
 687    #[test]
 688    fn test_format_yesterday_less_than_24_hours_ago() {
 689        let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0);
 690        let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0);
 691
 692        assert_eq!(
 693            format_timestamp_naive(timestamp, reference, true),
 694            "Yesterday at 8:00 PM"
 695        );
 696    }
 697
 698    #[test]
 699    fn test_format_yesterday_more_than_24_hours_ago() {
 700        let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0);
 701        let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0);
 702
 703        assert_eq!(
 704            format_timestamp_naive(timestamp, reference, true),
 705            "Yesterday at 6:00 PM"
 706        );
 707    }
 708
 709    #[test]
 710    fn test_format_yesterday_over_midnight() {
 711        let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0);
 712        let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0);
 713
 714        assert_eq!(
 715            format_timestamp_naive(timestamp, reference, true),
 716            "Yesterday at 11:55 PM"
 717        );
 718    }
 719
 720    #[test]
 721    fn test_format_yesterday_over_month() {
 722        let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0);
 723        let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0);
 724
 725        assert_eq!(
 726            format_timestamp_naive(timestamp, reference, true),
 727            "Yesterday at 8:00 PM"
 728        );
 729    }
 730
 731    #[test]
 732    fn test_format_before_yesterday() {
 733        let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0);
 734        let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0);
 735
 736        assert_eq!(
 737            format_timestamp_naive(timestamp, reference, true),
 738            "04/10/1990 8:20 PM"
 739        );
 740    }
 741
 742    #[test]
 743    fn test_relative_format_minutes() {
 744        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 745        let mut current_timestamp = reference;
 746
 747        let mut next_minute = || {
 748            current_timestamp = if current_timestamp.minute() == 0 {
 749                current_timestamp
 750                    .replace_hour(current_timestamp.hour() - 1)
 751                    .unwrap()
 752                    .replace_minute(59)
 753                    .unwrap()
 754            } else {
 755                current_timestamp
 756                    .replace_minute(current_timestamp.minute() - 1)
 757                    .unwrap()
 758            };
 759            current_timestamp
 760        };
 761
 762        assert_eq!(
 763            format_relative_time(reference, reference),
 764            Some("Just now".to_string())
 765        );
 766
 767        assert_eq!(
 768            format_relative_time(next_minute(), reference),
 769            Some("1 minute ago".to_string())
 770        );
 771
 772        for i in 2..=59 {
 773            assert_eq!(
 774                format_relative_time(next_minute(), reference),
 775                Some(format!("{} minutes ago", i))
 776            );
 777        }
 778
 779        assert_eq!(
 780            format_relative_time(next_minute(), reference),
 781            Some("1 hour ago".to_string())
 782        );
 783    }
 784
 785    #[test]
 786    fn test_relative_format_hours() {
 787        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 788        let mut current_timestamp = reference;
 789
 790        let mut next_hour = || {
 791            current_timestamp = if current_timestamp.hour() == 0 {
 792                let date = current_timestamp.date().previous_day().unwrap();
 793                current_timestamp.replace_date(date)
 794            } else {
 795                current_timestamp
 796                    .replace_hour(current_timestamp.hour() - 1)
 797                    .unwrap()
 798            };
 799            current_timestamp
 800        };
 801
 802        assert_eq!(
 803            format_relative_time(next_hour(), reference),
 804            Some("1 hour ago".to_string())
 805        );
 806
 807        for i in 2..=23 {
 808            assert_eq!(
 809                format_relative_time(next_hour(), reference),
 810                Some(format!("{} hours ago", i))
 811            );
 812        }
 813
 814        assert_eq!(format_relative_time(next_hour(), reference), None);
 815    }
 816
 817    #[test]
 818    fn test_relative_format_days() {
 819        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 820        let mut current_timestamp = reference;
 821
 822        let mut next_day = || {
 823            let date = current_timestamp.date().previous_day().unwrap();
 824            current_timestamp = current_timestamp.replace_date(date);
 825            current_timestamp
 826        };
 827
 828        assert_eq!(
 829            format_relative_date(reference, reference),
 830            "Today".to_string()
 831        );
 832
 833        assert_eq!(
 834            format_relative_date(next_day(), reference),
 835            "Yesterday".to_string()
 836        );
 837
 838        for i in 2..=6 {
 839            assert_eq!(
 840                format_relative_date(next_day(), reference),
 841                format!("{} days ago", i)
 842            );
 843        }
 844
 845        assert_eq!(format_relative_date(next_day(), reference), "1 week ago");
 846    }
 847
 848    #[test]
 849    fn test_relative_format_weeks() {
 850        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 851        let mut current_timestamp = reference;
 852
 853        let mut next_week = || {
 854            for _ in 0..7 {
 855                let date = current_timestamp.date().previous_day().unwrap();
 856                current_timestamp = current_timestamp.replace_date(date);
 857            }
 858            current_timestamp
 859        };
 860
 861        assert_eq!(
 862            format_relative_date(next_week(), reference),
 863            "1 week ago".to_string()
 864        );
 865
 866        for i in 2..=4 {
 867            assert_eq!(
 868                format_relative_date(next_week(), reference),
 869                format!("{} weeks ago", i)
 870            );
 871        }
 872
 873        assert_eq!(format_relative_date(next_week(), reference), "1 month ago");
 874    }
 875
 876    #[test]
 877    fn test_relative_format_months() {
 878        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 879        let mut current_timestamp = reference;
 880
 881        let mut next_month = || {
 882            if current_timestamp.month() == time::Month::January {
 883                current_timestamp = current_timestamp
 884                    .replace_month(time::Month::December)
 885                    .unwrap()
 886                    .replace_year(current_timestamp.year() - 1)
 887                    .unwrap();
 888            } else {
 889                current_timestamp = current_timestamp
 890                    .replace_month(current_timestamp.month().previous())
 891                    .unwrap();
 892            }
 893            current_timestamp
 894        };
 895
 896        assert_eq!(
 897            format_relative_date(next_month(), reference),
 898            "4 weeks ago".to_string()
 899        );
 900
 901        for i in 2..=11 {
 902            assert_eq!(
 903                format_relative_date(next_month(), reference),
 904                format!("{} months ago", i)
 905            );
 906        }
 907
 908        assert_eq!(format_relative_date(next_month(), reference), "1 year ago");
 909    }
 910
 911    #[test]
 912    fn test_relative_format_years() {
 913        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 914
 915        // 12 months
 916        assert_eq!(
 917            format_relative_date(create_offset_datetime(1989, 4, 12, 23, 0, 0), reference),
 918            "1 year ago"
 919        );
 920
 921        // 13 months
 922        assert_eq!(
 923            format_relative_date(create_offset_datetime(1989, 3, 12, 23, 0, 0), reference),
 924            "1 year ago"
 925        );
 926
 927        // 23 months
 928        assert_eq!(
 929            format_relative_date(create_offset_datetime(1988, 5, 12, 23, 0, 0), reference),
 930            "1 year ago"
 931        );
 932
 933        // 24 months
 934        assert_eq!(
 935            format_relative_date(create_offset_datetime(1988, 4, 12, 23, 0, 0), reference),
 936            "2 years ago"
 937        );
 938
 939        // 25 months
 940        assert_eq!(
 941            format_relative_date(create_offset_datetime(1988, 3, 12, 23, 0, 0), reference),
 942            "2 years ago"
 943        );
 944
 945        // 35 months
 946        assert_eq!(
 947            format_relative_date(create_offset_datetime(1987, 5, 12, 23, 0, 0), reference),
 948            "2 years ago"
 949        );
 950
 951        // 36 months
 952        assert_eq!(
 953            format_relative_date(create_offset_datetime(1987, 4, 12, 23, 0, 0), reference),
 954            "3 years ago"
 955        );
 956
 957        // 37 months
 958        assert_eq!(
 959            format_relative_date(create_offset_datetime(1987, 3, 12, 23, 0, 0), reference),
 960            "3 years ago"
 961        );
 962
 963        // 120 months
 964        assert_eq!(
 965            format_relative_date(create_offset_datetime(1980, 4, 12, 23, 0, 0), reference),
 966            "10 years ago"
 967        );
 968    }
 969
 970    #[test]
 971    fn test_calculate_month_difference() {
 972        let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
 973
 974        assert_eq!(calculate_month_difference(reference, reference), 0);
 975
 976        assert_eq!(
 977            calculate_month_difference(create_offset_datetime(1990, 1, 12, 23, 0, 0), reference),
 978            3
 979        );
 980
 981        assert_eq!(
 982            calculate_month_difference(create_offset_datetime(1989, 11, 12, 23, 0, 0), reference),
 983            5
 984        );
 985
 986        assert_eq!(
 987            calculate_month_difference(create_offset_datetime(1989, 4, 12, 23, 0, 0), reference),
 988            12
 989        );
 990
 991        assert_eq!(
 992            calculate_month_difference(create_offset_datetime(1989, 3, 12, 23, 0, 0), reference),
 993            13
 994        );
 995
 996        assert_eq!(
 997            calculate_month_difference(create_offset_datetime(1987, 5, 12, 23, 0, 0), reference),
 998            35
 999        );
1000
1001        assert_eq!(
1002            calculate_month_difference(create_offset_datetime(1987, 4, 12, 23, 0, 0), reference),
1003            36
1004        );
1005
1006        assert_eq!(
1007            calculate_month_difference(create_offset_datetime(1987, 3, 12, 23, 0, 0), reference),
1008            37
1009        );
1010    }
1011
1012    fn test_timezone() -> UtcOffset {
1013        UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset")
1014    }
1015
1016    fn create_offset_datetime(
1017        year: i32,
1018        month: u8,
1019        day: u8,
1020        hour: u8,
1021        minute: u8,
1022        second: u8,
1023    ) -> OffsetDateTime {
1024        let date = time::Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day)
1025            .unwrap();
1026        let time = time::Time::from_hms(hour, minute, second).unwrap();
1027        let date = date.with_time(time).assume_utc(); // Assume UTC for simplicity
1028        date.to_offset(test_timezone())
1029    }
1030}