Skip to content

Commit

Permalink
fix: make time use timezone information
Browse files Browse the repository at this point in the history
When a file timestamp is displayed, it should use the timezone info from
the time of the timestamp, not from when it is displayed. This occurs when
a file timestamp is updated under daylight saving time, and displayed at a
time when not under daylight saving time.

This bug is quite subtle, as it depends on the time of construction of the
timestamp, not on the moment it is read.
  • Loading branch information
erwinvaneijk committed Jan 10, 2025
1 parent 3f72de8 commit efe1577
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 11 deletions.
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ name = "eza"
[dependencies]
rayon = "1.10.0"
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
chrono-tz = "0.10.0"
iana-time-zone = "0.1.58"
nu-ansi-term = { version = "0.50.1", features = [
"serde",
"derive_serde_style",
Expand Down
21 changes: 17 additions & 4 deletions src/output/render/times.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,33 @@ use chrono::prelude::*;
use nu_ansi_term::Style;

pub trait Render {
fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell;
fn render(self, style: Style, time_format: TimeFormat) -> TextCell;
}

impl Render for Option<NaiveDateTime> {
fn render(self, style: Style, time_offset: FixedOffset, time_format: TimeFormat) -> TextCell {
let datestamp = if let Some(time) = self {
fn render(self, style: Style, time_format: TimeFormat) -> TextCell {
let datestamp = if let Ok(timezone_str) = iana_time_zone::get_timezone() {
let timezone: chrono_tz::Tz = timezone_str.parse().unwrap();
if let Some(time) = self {
let time_offset = timezone.offset_from_utc_datetime(&time).fix();
time_format.format(&DateTime::<FixedOffset>::from_naive_utc_and_offset(
time,
time_offset,
))
} else {
String::from("-")
}
} else if let Some(time) = self {
// This is the next best thing, use the timezone now, instead of at the time of the
// timestamp.
let time_offset: FixedOffset = *Local::now().offset();
time_format.format(&DateTime::<FixedOffset>::from_naive_utc_and_offset(
time,
time_offset,
))
} else {
String::from("-")
};

TextCell::paint(style, datestamp)
}
}
7 changes: 0 additions & 7 deletions src/output/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,6 @@ impl Default for TimeTypes {
///
/// Any environment field should be able to be mocked up for test runs.
pub struct Environment {
/// The computer’s current time offset, determined from time zone.
time_offset: FixedOffset,

/// Localisation rules for formatting numbers.
numeric: locale::Numeric,

Expand All @@ -381,16 +378,13 @@ impl Environment {
}

fn load_all() -> Self {
let time_offset = *Local::now().offset();

let numeric =
locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());

#[cfg(unix)]
let users = Mutex::new(UsersCache::new());

Self {
time_offset,
numeric,
#[cfg(unix)]
users,
Expand Down Expand Up @@ -568,7 +562,6 @@ impl<'a> Table<'a> {
} else {
self.theme.ui.date.unwrap_or_default()
},
self.env.time_offset,
self.time_format.clone(),
),
}
Expand Down
30 changes: 30 additions & 0 deletions src/output/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,34 @@ mod test {
.all(|string| UnicodeWidthStr::width(string.as_str()) == max_month_width)
);
}

#[test]
fn display_timestamp_correctly_in_timezone_with_dst() {
let timezone_str = "Europe/Berlin";
let timezone: chrono_tz::Tz = timezone_str.parse().unwrap();
let time = DateTime::<Utc>::from_timestamp(1685867700, 0)
.unwrap()
.naive_utc();

let fixed_offset = timezone.offset_from_utc_datetime(&time).fix();
let real_converted = DateTime::<FixedOffset>::from_naive_utc_and_offset(time, fixed_offset);
let formatted = full(&real_converted);
let expected = "2023-06-04 10:35:00.000000000 +0200";
assert_eq!(expected, formatted);
}

#[test]
fn display_timestamp_correctly_in_timezone_without_dst() {
let timezone_str = "Europe/Berlin";
let timezone: chrono_tz::Tz = timezone_str.parse().unwrap();
let time = DateTime::<Utc>::from_timestamp(1699090500, 0)
.unwrap()
.naive_utc();

let fixed_offset = timezone.offset_from_utc_datetime(&time).fix();
let real_converted = DateTime::<FixedOffset>::from_naive_utc_and_offset(time, fixed_offset);
let formatted = full(&real_converted);
let expected = "2023-11-04 10:35:00.000000000 +0100";
assert_eq!(expected, formatted);
}
}

0 comments on commit efe1577

Please sign in to comment.