Skip to content

Commit 048aa32

Browse files
sachiniyerclaude
andauthored
fix(datetime): floor sub-second negative timestamps instead of snapping to epoch (#262)
## Summary `format_timestamp` converted `timestamp_ms → seconds` by integer division, which truncates toward zero. Timestamps in the (-1000, 0) ms window therefore collapsed onto the Unix epoch (1970-01-01 00:00:00) instead of formatting as the preceding second (1969-12-31 23:59:59). Swap `chrono::DateTime::from_timestamp(timestamp_ms / MS_TO_SECONDS, 0)` for `chrono::DateTime::from_timestamp_millis(timestamp_ms)`, which takes millisecond timestamps directly and floors correctly. The `MS_TO_SECONDS` constant is no longer used. ## Test plan - [x] `cargo test --lib utils::datetime` — 7 pass, including new `format_datetime_small_negative_is_not_epoch` regression test - [x] Existing positive-path tests (including sub-second `format_datetime_rounds_down_sub_second`) still pass — `from_timestamp_millis` produces the same formatted output for those inputs - [x] `cargo clippy -- -D warnings` clean - [x] `cargo fmt --check` clean Fixes #209. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/usedetail/cli/pull/262" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review"> </picture> </a> <!-- devin-review-badge-end --> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ed139d7 commit 048aa32

1 file changed

Lines changed: 13 additions & 5 deletions

File tree

‎src/utils/datetime.rs‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use chrono::Local;
22

3-
/// Conversion factor from milliseconds to seconds
4-
const MS_TO_SECONDS: i64 = 1000;
5-
63
/// Format a UTC timestamp (in milliseconds) in the machine's local timezone.
74
fn format_timestamp(timestamp_ms: i64, fmt: &str) -> String {
8-
chrono::DateTime::from_timestamp(timestamp_ms / MS_TO_SECONDS, 0).map_or_else(
5+
// `from_timestamp_millis` floors toward negative infinity, so timestamps
6+
// in (-1000, 0) correctly land in the second before the epoch instead of
7+
// collapsing to epoch via integer-division truncation.
8+
chrono::DateTime::from_timestamp_millis(timestamp_ms).map_or_else(
99
|| "-".into(),
1010
|dt| dt.with_timezone(&Local).format(fmt).to_string(),
1111
)
@@ -26,7 +26,7 @@ mod tests {
2626
use super::*;
2727

2828
fn expected_local(timestamp_ms: i64, fmt: &str) -> String {
29-
chrono::DateTime::from_timestamp(timestamp_ms / MS_TO_SECONDS, 0)
29+
chrono::DateTime::from_timestamp_millis(timestamp_ms)
3030
.expect("valid timestamp")
3131
.with_timezone(&Local)
3232
.format(fmt)
@@ -75,4 +75,12 @@ mod tests {
7575
expected_local(500, "%Y-%m-%d %H:%M:%S %Z")
7676
);
7777
}
78+
79+
#[test]
80+
fn format_datetime_small_negative_is_not_epoch() {
81+
// Timestamps in (-1000, 0) ms should land in the second *before* the
82+
// epoch, not at the epoch itself. Integer-division truncation toward
83+
// zero previously collapsed this whole window onto 1970-01-01 00:00:00.
84+
assert_ne!(format_datetime(-1), format_datetime(0));
85+
}
7886
}

0 commit comments

Comments
 (0)