notification panel: rework time formatting (#8997)
Follow up of #7994 to rework the notification panel timestamps. This PR also includes some of the changes @evrsen proposed in #8996 Here is what it looks like now: https://github.com/zed-industries/zed/assets/53836821/d85450e7-eab6-4fe7-bd11-1d76c0e87258 Release Notes: - Reworked date time formatting in the chat and the notification panel - Added hover style to notifications and hovering tooltip on timestamps --------- Co-authored-by: Evren Sen <146845123+evrsen@users.noreply.github.com>
This commit is contained in:
parent
a105b5f215
commit
fb83cf2042
5 changed files with 422 additions and 103 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9947,7 +9947,6 @@ dependencies = [
|
|||
name = "time_format"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"core-foundation",
|
||||
"core-foundation-sys 0.8.6",
|
||||
"sys-locale",
|
||||
|
|
|
@ -493,9 +493,10 @@ impl ChatPanel {
|
|||
)
|
||||
.child(
|
||||
Label::new(time_format::format_localized_timestamp(
|
||||
OffsetDateTime::now_utc(),
|
||||
message.timestamp,
|
||||
OffsetDateTime::now_utc(),
|
||||
self.local_timezone,
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
|
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
|||
use settings::{Settings, SettingsStore};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label};
|
||||
use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
|
@ -228,6 +228,20 @@ impl NotificationPanel {
|
|||
self.did_render_notification(notification_id, ¬ification, cx);
|
||||
}
|
||||
|
||||
let relative_timestamp = time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
now,
|
||||
self.local_timezone,
|
||||
time_format::TimestampFormat::Relative,
|
||||
);
|
||||
|
||||
let absolute_timestamp = time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
now,
|
||||
self.local_timezone,
|
||||
time_format::TimestampFormat::Absolute,
|
||||
);
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(ix)
|
||||
|
@ -237,6 +251,7 @@ impl NotificationPanel {
|
|||
.px_2()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.when(can_navigate, |el| {
|
||||
el.cursor(CursorStyle::PointingHand).on_click({
|
||||
let notification = notification.clone();
|
||||
|
@ -261,12 +276,17 @@ impl NotificationPanel {
|
|||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Label::new(format_timestamp(
|
||||
timestamp,
|
||||
now,
|
||||
self.local_timezone,
|
||||
))
|
||||
.color(Color::Muted),
|
||||
div()
|
||||
.id("notification_timestamp")
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().element_selected)
|
||||
.rounded_md()
|
||||
})
|
||||
.child(Label::new(relative_timestamp).color(Color::Muted))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(absolute_timestamp.clone(), cx)
|
||||
}),
|
||||
)
|
||||
.children(if let Some(is_accepted) = response {
|
||||
Some(div().flex().flex_grow().justify_end().child(Label::new(
|
||||
|
@ -757,29 +777,3 @@ impl Render for NotificationToast {
|
|||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for NotificationToast {}
|
||||
|
||||
fn format_timestamp(
|
||||
mut timestamp: OffsetDateTime,
|
||||
mut now: OffsetDateTime,
|
||||
local_timezone: UtcOffset,
|
||||
) -> String {
|
||||
timestamp = timestamp.to_offset(local_timezone);
|
||||
now = now.to_offset(local_timezone);
|
||||
|
||||
let today = now.date();
|
||||
let date = timestamp.date();
|
||||
if date == today {
|
||||
let difference = now - timestamp;
|
||||
if difference >= Duration::from_secs(3600) {
|
||||
format!("{}h", difference.whole_seconds() / 3600)
|
||||
} else if difference >= Duration::from_secs(60) {
|
||||
format!("{}m", difference.whole_seconds() / 60)
|
||||
} else {
|
||||
"just now".to_string()
|
||||
}
|
||||
} else if date.next_day() == Some(today) {
|
||||
"yesterday".to_string()
|
||||
} else {
|
||||
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ path = "src/time_format.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
sys-locale.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
|
|
|
@ -1,48 +1,156 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
|
||||
/// The formatting style for a timestamp.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TimestampFormat {
|
||||
/// Formats the timestamp as an absolute time, e.g. "2021-12-31 3:00AM".
|
||||
Absolute,
|
||||
/// Formats the timestamp as an absolute time.
|
||||
/// If the message is from today or yesterday the date will be replaced with "Today at x" or "Yesterday at x" respectively.
|
||||
/// E.g. "Today at 12:00 PM", "Yesterday at 11:00 AM", "2021-12-31 3:00AM".
|
||||
EnhancedAbsolute,
|
||||
/// Formats the timestamp as a relative time, e.g. "just now", "1 minute ago", "2 hours ago", "2 months ago".
|
||||
Relative,
|
||||
}
|
||||
|
||||
/// Formats a timestamp, which respects the user's date and time preferences/custom format.
|
||||
pub fn format_localized_timestamp(
|
||||
reference: OffsetDateTime,
|
||||
timestamp: OffsetDateTime,
|
||||
reference: OffsetDateTime,
|
||||
timezone: UtcOffset,
|
||||
format: TimestampFormat,
|
||||
) -> String {
|
||||
let timestamp_local = timestamp.to_offset(timezone);
|
||||
let reference_local = reference.to_offset(timezone);
|
||||
|
||||
match format {
|
||||
TimestampFormat::Absolute => {
|
||||
format_absolute_timestamp(timestamp_local, reference_local, false)
|
||||
}
|
||||
TimestampFormat::EnhancedAbsolute => {
|
||||
format_absolute_timestamp(timestamp_local, reference_local, true)
|
||||
}
|
||||
TimestampFormat::Relative => format_relative_time(timestamp_local, reference_local)
|
||||
.unwrap_or_else(|| format_relative_date(timestamp_local, reference_local)),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_absolute_timestamp(
|
||||
timestamp: OffsetDateTime,
|
||||
reference: OffsetDateTime,
|
||||
#[allow(unused_variables)] enhanced_date_formatting: bool,
|
||||
) -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let timestamp_local = timestamp.to_offset(timezone);
|
||||
let reference_local = reference.to_offset(timezone);
|
||||
let reference_local_date = reference_local.date();
|
||||
let timestamp_local_date = timestamp_local.date();
|
||||
if !enhanced_date_formatting {
|
||||
return format!(
|
||||
"{} {}",
|
||||
macos::format_date(×tamp),
|
||||
macos::format_time(×tamp)
|
||||
);
|
||||
}
|
||||
|
||||
let native_fmt = if timestamp_local_date == reference_local_date {
|
||||
macos::format_time(×tamp)
|
||||
} else if reference_local_date.previous_day() == Some(timestamp_local_date) {
|
||||
macos::format_time(×tamp).map(|t| format!("yesterday at {}", t).to_string())
|
||||
let timestamp_date = timestamp.date();
|
||||
let reference_date = reference.date();
|
||||
if timestamp_date == reference_date {
|
||||
format!("Today at {}", macos::format_time(×tamp))
|
||||
} else if reference_date.previous_day() == Some(timestamp_date) {
|
||||
format!("Yesterday at {}", macos::format_time(×tamp))
|
||||
} else {
|
||||
macos::format_date(×tamp)
|
||||
};
|
||||
native_fmt.unwrap_or_else(|_| format_timestamp_fallback(reference, timestamp, timezone))
|
||||
format!(
|
||||
"{} {}",
|
||||
macos::format_date(×tamp),
|
||||
macos::format_time(×tamp)
|
||||
)
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// todo(linux) respect user's date/time preferences
|
||||
// todo(windows) respect user's date/time preferences
|
||||
format_timestamp_fallback(reference, timestamp, timezone)
|
||||
format_timestamp_fallback(timestamp, reference)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_timestamp_fallback(
|
||||
reference: OffsetDateTime,
|
||||
timestamp: OffsetDateTime,
|
||||
timezone: UtcOffset,
|
||||
) -> String {
|
||||
static CURRENT_LOCALE: OnceLock<String> = OnceLock::new();
|
||||
let current_locale = CURRENT_LOCALE
|
||||
.get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
|
||||
fn format_relative_time(timestamp: OffsetDateTime, reference: OffsetDateTime) -> Option<String> {
|
||||
let difference = reference - timestamp;
|
||||
let minutes = difference.whole_minutes();
|
||||
match minutes {
|
||||
0 => Some("Just now".to_string()),
|
||||
1 => Some("1 minute ago".to_string()),
|
||||
2..=59 => Some(format!("{} minutes ago", minutes)),
|
||||
_ => {
|
||||
let hours = difference.whole_hours();
|
||||
match hours {
|
||||
1 => Some("1 hour ago".to_string()),
|
||||
2..=23 => Some(format!("{} hours ago", hours)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str());
|
||||
format_timestamp_naive(reference, timestamp, timezone, is_12_hour_time)
|
||||
fn format_relative_date(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String {
|
||||
let timestamp_date = timestamp.date();
|
||||
let reference_date = reference.date();
|
||||
let difference = reference_date - timestamp_date;
|
||||
let days = difference.whole_days();
|
||||
match days {
|
||||
0 => "Today".to_string(),
|
||||
1 => "Yesterday".to_string(),
|
||||
2..=6 => format!("{} days ago", days),
|
||||
_ => {
|
||||
let weeks = difference.whole_weeks();
|
||||
match weeks {
|
||||
1 => "1 week ago".to_string(),
|
||||
2..=4 => format!("{} weeks ago", weeks),
|
||||
_ => {
|
||||
let month_diff = calculate_month_difference(timestamp, reference);
|
||||
match month_diff {
|
||||
0..=1 => "1 month ago".to_string(),
|
||||
2..=11 => format!("{} months ago", month_diff),
|
||||
_ => {
|
||||
let timestamp_year = timestamp_date.year();
|
||||
let reference_year = reference_date.year();
|
||||
let years = reference_year - timestamp_year;
|
||||
match years {
|
||||
1 => "1 year ago".to_string(),
|
||||
_ => format!("{} years ago", years),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the difference in months between two timestamps.
|
||||
/// The reference timestamp should always be greater than the timestamp.
|
||||
fn calculate_month_difference(timestamp: OffsetDateTime, reference: OffsetDateTime) -> usize {
|
||||
let timestamp_year = timestamp.year();
|
||||
let reference_year = reference.year();
|
||||
let timestamp_month: u8 = timestamp.month().into();
|
||||
let reference_month: u8 = reference.month().into();
|
||||
|
||||
let month_diff = if reference_month >= timestamp_month {
|
||||
reference_month as usize - timestamp_month as usize
|
||||
} else {
|
||||
12 - timestamp_month as usize + reference_month as usize
|
||||
};
|
||||
|
||||
let year_diff = (reference_year - timestamp_year) as usize;
|
||||
if year_diff == 0 {
|
||||
reference_month as usize - timestamp_month as usize
|
||||
} else {
|
||||
if month_diff == 0 {
|
||||
year_diff * 12
|
||||
} else if timestamp_month > reference_month {
|
||||
(year_diff - 1) * 12 + month_diff
|
||||
} else {
|
||||
year_diff * 12 + month_diff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a timestamp, which is either in 12-hour or 24-hour time format.
|
||||
|
@ -50,20 +158,20 @@ fn format_timestamp_fallback(
|
|||
/// This function does not respect the user's date and time preferences.
|
||||
/// This should only be used as a fallback mechanism when the OS time formatting fails.
|
||||
pub fn format_timestamp_naive(
|
||||
reference: OffsetDateTime,
|
||||
timestamp: OffsetDateTime,
|
||||
timezone: UtcOffset,
|
||||
timestamp_local: OffsetDateTime,
|
||||
reference_local: OffsetDateTime,
|
||||
is_12_hour_time: bool,
|
||||
) -> String {
|
||||
let timestamp_local = timestamp.to_offset(timezone);
|
||||
let timestamp_local_hour = timestamp_local.hour();
|
||||
let timestamp_local_minute = timestamp_local.minute();
|
||||
let reference_local_date = reference_local.date();
|
||||
let timestamp_local_date = timestamp_local.date();
|
||||
|
||||
let (hour, meridiem) = if is_12_hour_time {
|
||||
let meridiem = if timestamp_local_hour >= 12 {
|
||||
"pm"
|
||||
"PM"
|
||||
} else {
|
||||
"am"
|
||||
"AM"
|
||||
};
|
||||
|
||||
let hour_12 = match timestamp_local_hour {
|
||||
|
@ -78,23 +186,11 @@ pub fn format_timestamp_naive(
|
|||
};
|
||||
|
||||
let formatted_time = match meridiem {
|
||||
Some(meridiem) => format!("{:02}:{:02} {}", hour, timestamp_local_minute, meridiem),
|
||||
Some(meridiem) => format!("{}:{:02} {}", hour, timestamp_local_minute, meridiem),
|
||||
None => format!("{:02}:{:02}", hour, timestamp_local_minute),
|
||||
};
|
||||
|
||||
let reference_local = reference.to_offset(timezone);
|
||||
let reference_local_date = reference_local.date();
|
||||
let timestamp_local_date = timestamp_local.date();
|
||||
|
||||
if timestamp_local_date == reference_local_date {
|
||||
return formatted_time;
|
||||
}
|
||||
|
||||
if reference_local_date.previous_day() == Some(timestamp_local_date) {
|
||||
return format!("yesterday at {}", formatted_time);
|
||||
}
|
||||
|
||||
match meridiem {
|
||||
let formatted_date = match meridiem {
|
||||
Some(_) => format!(
|
||||
"{:02}/{:02}/{}",
|
||||
timestamp_local_date.month() as u32,
|
||||
|
@ -107,9 +203,28 @@ pub fn format_timestamp_naive(
|
|||
timestamp_local_date.month() as u32,
|
||||
timestamp_local_date.year()
|
||||
),
|
||||
};
|
||||
|
||||
if timestamp_local_date == reference_local_date {
|
||||
format!("Today at {}", formatted_time)
|
||||
} else if reference_local_date.previous_day() == Some(timestamp_local_date) {
|
||||
format!("Yesterday at {}", formatted_time)
|
||||
} else {
|
||||
format!("{} {}", formatted_date, formatted_time)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn format_timestamp_fallback(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String {
|
||||
static CURRENT_LOCALE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
let current_locale = CURRENT_LOCALE
|
||||
.get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")));
|
||||
|
||||
let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str());
|
||||
format_timestamp_naive(timestamp, reference, is_12_hour_time)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
/// Returns `true` if the locale is recognized as a 12-hour time locale.
|
||||
fn is_12_hour_time_by_locale(locale: &str) -> bool {
|
||||
[
|
||||
|
@ -129,7 +244,6 @@ fn is_12_hour_time_by_locale(locale: &str) -> bool {
|
|||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos {
|
||||
use anyhow::Result;
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::date::CFAbsoluteTime;
|
||||
use core_foundation::string::CFString;
|
||||
|
@ -144,18 +258,18 @@ mod macos {
|
|||
locale::CFLocaleCopyCurrent,
|
||||
};
|
||||
|
||||
pub fn format_time(timestamp: &time::OffsetDateTime) -> Result<String> {
|
||||
pub fn format_time(timestamp: &time::OffsetDateTime) -> String {
|
||||
format_with_date_formatter(timestamp, TIME_FORMATTER.with(|f| *f))
|
||||
}
|
||||
|
||||
pub fn format_date(timestamp: &time::OffsetDateTime) -> Result<String> {
|
||||
pub fn format_date(timestamp: &time::OffsetDateTime) -> String {
|
||||
format_with_date_formatter(timestamp, DATE_FORMATTER.with(|f| *f))
|
||||
}
|
||||
|
||||
fn format_with_date_formatter(
|
||||
timestamp: &time::OffsetDateTime,
|
||||
fmt: CFDateFormatterRef,
|
||||
) -> Result<String> {
|
||||
) -> String {
|
||||
const UNIX_TO_CF_ABSOLUTE_TIME_OFFSET: i64 = 978307200;
|
||||
// Convert timestamp to macOS absolute time
|
||||
let timestamp_macos = timestamp.unix_timestamp() - UNIX_TO_CF_ABSOLUTE_TIME_OFFSET;
|
||||
|
@ -166,7 +280,7 @@ mod macos {
|
|||
fmt,
|
||||
cf_absolute_time,
|
||||
);
|
||||
Ok(CFString::wrap_under_create_rule(s).to_string())
|
||||
CFString::wrap_under_create_rule(s).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,8 +315,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), false),
|
||||
"15:30"
|
||||
format_timestamp_naive(timestamp, reference, false),
|
||||
"Today at 15:30"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -212,8 +326,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"03:30 pm"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Today at 3:30 PM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -223,8 +337,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"yesterday at 09:00 am"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Yesterday at 9:00 AM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -234,8 +348,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"yesterday at 08:00 pm"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Yesterday at 8:00 PM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -245,8 +359,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"yesterday at 06:00 pm"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Yesterday at 6:00 PM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -256,8 +370,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"yesterday at 11:55 pm"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Yesterday at 11:55 PM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -267,8 +381,8 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"yesterday at 08:00 pm"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"Yesterday at 8:00 PM"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -278,8 +392,219 @@ mod tests {
|
|||
let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp_naive(reference, timestamp, test_timezone(), true),
|
||||
"04/10/1990"
|
||||
format_timestamp_naive(timestamp, reference, true),
|
||||
"04/10/1990 8:20 PM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_format_minutes() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
let mut current_timestamp = reference;
|
||||
|
||||
let mut next_minute = || {
|
||||
current_timestamp = if current_timestamp.minute() == 0 {
|
||||
current_timestamp
|
||||
.replace_hour(current_timestamp.hour() - 1)
|
||||
.unwrap()
|
||||
.replace_minute(59)
|
||||
.unwrap()
|
||||
} else {
|
||||
current_timestamp
|
||||
.replace_minute(current_timestamp.minute() - 1)
|
||||
.unwrap()
|
||||
};
|
||||
current_timestamp
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format_relative_time(reference, reference),
|
||||
Some("Just now".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_relative_time(next_minute(), reference),
|
||||
Some("1 minute ago".to_string())
|
||||
);
|
||||
|
||||
for i in 2..=59 {
|
||||
assert_eq!(
|
||||
format_relative_time(next_minute(), reference),
|
||||
Some(format!("{} minutes ago", i))
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
format_relative_time(next_minute(), reference),
|
||||
Some("1 hour ago".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_format_hours() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
let mut current_timestamp = reference;
|
||||
|
||||
let mut next_hour = || {
|
||||
current_timestamp = if current_timestamp.hour() == 0 {
|
||||
let date = current_timestamp.date().previous_day().unwrap();
|
||||
current_timestamp.replace_date(date)
|
||||
} else {
|
||||
current_timestamp
|
||||
.replace_hour(current_timestamp.hour() - 1)
|
||||
.unwrap()
|
||||
};
|
||||
current_timestamp
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format_relative_time(next_hour(), reference),
|
||||
Some("1 hour ago".to_string())
|
||||
);
|
||||
|
||||
for i in 2..=23 {
|
||||
assert_eq!(
|
||||
format_relative_time(next_hour(), reference),
|
||||
Some(format!("{} hours ago", i))
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(format_relative_time(next_hour(), reference), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_format_days() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
let mut current_timestamp = reference;
|
||||
|
||||
let mut next_day = || {
|
||||
let date = current_timestamp.date().previous_day().unwrap();
|
||||
current_timestamp = current_timestamp.replace_date(date);
|
||||
current_timestamp
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format_relative_date(reference, reference),
|
||||
"Today".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_relative_date(next_day(), reference),
|
||||
"Yesterday".to_string()
|
||||
);
|
||||
|
||||
for i in 2..=6 {
|
||||
assert_eq!(
|
||||
format_relative_date(next_day(), reference),
|
||||
format!("{} days ago", i)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(format_relative_date(next_day(), reference), "1 week ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_format_weeks() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
let mut current_timestamp = reference;
|
||||
|
||||
let mut next_week = || {
|
||||
for _ in 0..7 {
|
||||
let date = current_timestamp.date().previous_day().unwrap();
|
||||
current_timestamp = current_timestamp.replace_date(date);
|
||||
}
|
||||
current_timestamp
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format_relative_date(next_week(), reference),
|
||||
"1 week ago".to_string()
|
||||
);
|
||||
|
||||
for i in 2..=4 {
|
||||
assert_eq!(
|
||||
format_relative_date(next_week(), reference),
|
||||
format!("{} weeks ago", i)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(format_relative_date(next_week(), reference), "1 month ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_format_months() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
let mut current_timestamp = reference;
|
||||
|
||||
let mut next_month = || {
|
||||
if current_timestamp.month() == time::Month::January {
|
||||
current_timestamp = current_timestamp
|
||||
.replace_month(time::Month::December)
|
||||
.unwrap()
|
||||
.replace_year(current_timestamp.year() - 1)
|
||||
.unwrap();
|
||||
} else {
|
||||
current_timestamp = current_timestamp
|
||||
.replace_month(current_timestamp.month().previous())
|
||||
.unwrap();
|
||||
}
|
||||
current_timestamp
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format_relative_date(next_month(), reference),
|
||||
"4 weeks ago".to_string()
|
||||
);
|
||||
|
||||
for i in 2..=11 {
|
||||
assert_eq!(
|
||||
format_relative_date(next_month(), reference),
|
||||
format!("{} months ago", i)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(format_relative_date(next_month(), reference), "1 year ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_month_difference() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0);
|
||||
|
||||
assert_eq!(calculate_month_difference(reference, reference), 0);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1990, 1, 12, 23, 0, 0), reference),
|
||||
3
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1989, 11, 12, 23, 0, 0), reference),
|
||||
5
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1989, 4, 12, 23, 0, 0), reference),
|
||||
12
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1989, 3, 12, 23, 0, 0), reference),
|
||||
13
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1987, 5, 12, 23, 0, 0), reference),
|
||||
35
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1987, 4, 12, 23, 0, 0), reference),
|
||||
36
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
calculate_month_difference(create_offset_datetime(1987, 3, 12, 23, 0, 0), reference),
|
||||
37
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -298,6 +623,7 @@ mod tests {
|
|||
let date = time::Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day)
|
||||
.unwrap();
|
||||
let time = time::Time::from_hms(hour, minute, second).unwrap();
|
||||
date.with_time(time).assume_utc() // Assume UTC for simplicity
|
||||
let date = date.with_time(time).assume_utc(); // Assume UTC for simplicity
|
||||
date.to_offset(test_timezone())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue