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