From c0c02700a01760afe07990c3eedd2c69c069a32c Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Wed, 25 Mar 2026 20:38:43 +0100 Subject: [PATCH 1/4] Rename unix_to_human_time to decompose_unix_timestamp and update logic for handling negative timestamps --- modules/shared/src/time.rs | 70 +++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/modules/shared/src/time.rs b/modules/shared/src/time.rs index 10bd8e92..849ce4d6 100644 --- a/modules/shared/src/time.rs +++ b/modules/shared/src/time.rs @@ -1,4 +1,4 @@ -pub fn unix_to_human_time(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) { +pub fn decompose_unix_timestamp(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) { // Constants for calculations const SECONDS_IN_MINUTE: i64 = 60; const SECONDS_IN_HOUR: i64 = 60 * SECONDS_IN_MINUTE; @@ -6,18 +6,12 @@ pub fn unix_to_human_time(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) { const DAYS_IN_YEAR: i64 = 365; const DAYS_IN_LEAP_YEAR: i64 = 366; - // Start from 1970 - let mut year = 1970; - let mut days_since_epoch = unix_timestamp / SECONDS_IN_DAY; - let mut remaining_seconds = unix_timestamp % SECONDS_IN_DAY; + // Start from 1970. + let mut year: i64 = 1970; + let mut days_since_epoch = unix_timestamp.div_euclid(SECONDS_IN_DAY); + let mut remaining_seconds = unix_timestamp.rem_euclid(SECONDS_IN_DAY); - if remaining_seconds < 0 { - // Handle negative Unix timestamps - days_since_epoch -= 1; - remaining_seconds += SECONDS_IN_DAY; - } - - // Determine the current year + // Determine the current year for timestamps on/after and before epoch. while days_since_epoch >= if is_leap_year(year) { DAYS_IN_LEAP_YEAR @@ -33,6 +27,15 @@ pub fn unix_to_human_time(unix_timestamp: i64) -> (u16, u8, u8, u8, u8, u8) { year += 1; } + while days_since_epoch < 0 { + year -= 1; + days_since_epoch += if is_leap_year(year) { + DAYS_IN_LEAP_YEAR + } else { + DAYS_IN_YEAR + }; + } + // Determine the current month and day let mut month = 0; while days_since_epoch >= days_in_month(year, month) { @@ -74,3 +77,46 @@ pub fn days_in_month(year: i64, month: usize) -> i64 { DAYS_IN_MONTH[month] } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unix_epoch_is_correct() { + assert_eq!(decompose_unix_timestamp(0), (1970, 1, 1, 0, 0, 0)); + } + + #[test] + fn one_second_before_epoch_is_correct() { + assert_eq!(decompose_unix_timestamp(-1), (1969, 12, 31, 23, 59, 59)); + } + + #[test] + fn one_day_before_epoch_is_correct() { + assert_eq!(decompose_unix_timestamp(-86_400), (1969, 12, 31, 0, 0, 0)); + } + + #[test] + fn leap_day_2024_is_correct() { + // 2024-02-29 00:00:00 UTC + assert_eq!( + decompose_unix_timestamp(1_709_164_800), + (2024, 2, 29, 0, 0, 0) + ); + } + + #[test] + fn leap_year_rules_are_correct() { + assert!(is_leap_year(2000)); + assert!(!is_leap_year(1900)); + assert!(is_leap_year(2024)); + assert!(!is_leap_year(2023)); + } + + #[test] + fn february_days_are_correct() { + assert_eq!(days_in_month(2024, 1), 29); + assert_eq!(days_in_month(2023, 1), 28); + } +} From c0904bdadfd5121bbf0da3326a9c28f38cfffcb7 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Wed, 25 Mar 2026 20:38:50 +0100 Subject: [PATCH 2/4] Add time formatting functionality for Unix timestamps --- modules/internationalization/Cargo.toml | 1 + modules/internationalization/src/lib.rs | 2 + modules/internationalization/src/time.rs | 90 ++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 modules/internationalization/src/time.rs diff --git a/modules/internationalization/Cargo.toml b/modules/internationalization/Cargo.toml index f5c7aaba..b2bc346f 100644 --- a/modules/internationalization/Cargo.toml +++ b/modules/internationalization/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] internationalization_macros = { path = "./macros" } +shared = { path = "../shared" } [features] default = [] diff --git a/modules/internationalization/src/lib.rs b/modules/internationalization/src/lib.rs index bbe8ac55..4df413ec 100644 --- a/modules/internationalization/src/lib.rs +++ b/modules/internationalization/src/lib.rs @@ -6,8 +6,10 @@ extern crate alloc; extern crate std; mod range; +mod time; pub use range::*; +pub use time::*; pub use internationalization_macros::translate; diff --git a/modules/internationalization/src/time.rs b/modules/internationalization/src/time.rs new file mode 100644 index 00000000..199f6a86 --- /dev/null +++ b/modules/internationalization/src/time.rs @@ -0,0 +1,90 @@ +use alloc::format; +use alloc::string::String; + +/// Formats a Unix timestamp using Python-like `strftime` tokens. +/// +/// Supported tokens: +/// - `%Y` year with century (e.g. `2026`) +/// - `%m` month as zero-padded decimal (`01`..`12`) +/// - `%d` day of month as zero-padded decimal (`01`..`31`) +/// - `%H` hour (24-hour clock) as zero-padded decimal (`00`..`23`) +/// - `%I` hour (12-hour clock) as zero-padded decimal (`01`..`12`) +/// - `%M` minute as zero-padded decimal (`00`..`59`) +/// - `%S` second as zero-padded decimal (`00`..`59`) +/// - `%p` locale-independent `AM`/`PM` +/// - `%%` literal `%` +pub fn format_unix_timestamp(unix_timestamp: i64, pattern: &str) -> String { + let (year, month, day, hour, minute, second) = shared::decompose_unix_timestamp(unix_timestamp); + + let mut output = String::with_capacity(pattern.len() + 16); + let mut characters = pattern.chars(); + + while let Some(character) = characters.next() { + if character != '%' { + output.push(character); + continue; + } + + match characters.next() { + Some('Y') => output.push_str(&format!("{:04}", year)), + Some('m') => output.push_str(&format!("{:02}", month)), + Some('d') => output.push_str(&format!("{:02}", day)), + Some('H') => output.push_str(&format!("{:02}", hour)), + Some('I') => output.push_str(&format!("{:02}", hour_12(hour))), + Some('M') => output.push_str(&format!("{:02}", minute)), + Some('S') => output.push_str(&format!("{:02}", second)), + Some('p') => output.push_str(if hour < 12 { "AM" } else { "PM" }), + Some('%') => output.push('%'), + Some(other) => { + output.push('%'); + output.push(other); + } + None => output.push('%'), + } + } + + output +} + +const fn hour_12(hour_24: u8) -> u8 { + match hour_24 % 12 { + 0 => 12, + value => value, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_24_hour_time() { + let timestamp = 13 * 3600 + 5 * 60; + assert_eq!(format_unix_timestamp(timestamp, "%H:%M"), "13:05"); + } + + #[test] + fn format_12_hour_time_with_am_pm() { + let midnight = 0; + let afternoon = 13 * 3600 + 5 * 60; + + assert_eq!(format_unix_timestamp(midnight, "%I:%M %p"), "12:00 AM"); + assert_eq!(format_unix_timestamp(afternoon, "%I:%M %p"), "01:05 PM"); + } + + #[test] + fn format_date_and_time() { + assert_eq!( + format_unix_timestamp(0, "%Y-%m-%d %H:%M:%S"), + "1970-01-01 00:00:00" + ); + } + + #[test] + fn format_negative_unix_time() { + assert_eq!( + format_unix_timestamp(-1, "%Y-%m-%d %H:%M:%S"), + "1969-12-31 23:59:59" + ); + } +} From ceaac4d1baa3fcac0d8b8c6926639609d4014291 Mon Sep 17 00:00:00 2001 From: Alix ANNERAUD Date: Wed, 25 Mar 2026 20:39:12 +0100 Subject: [PATCH 3/4] Refactor time formatting by replacing unix_to_human_time with decompose_unix_timestamp --- executables/shell/graphical/src/layout.rs | 11 +++++------ modules/file_system/src/time.rs | 5 +++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/executables/shell/graphical/src/layout.rs b/executables/shell/graphical/src/layout.rs index b41905a9..d6710e2e 100644 --- a/executables/shell/graphical/src/layout.rs +++ b/executables/shell/graphical/src/layout.rs @@ -1,5 +1,5 @@ use crate::error::{Error, Result}; -use alloc::{format, string::String}; +use alloc::string::String; use core::ffi::CStr; use core::ptr::null_mut; use core::time::Duration; @@ -7,9 +7,8 @@ use xila::file_system::{AccessFlags, Path}; use xila::graphics::{self, EventKind, lvgl, symbol, theme}; use xila::log; use xila::network::InterfaceKind; -use xila::shared::unix_to_human_time; use xila::virtual_file_system::{Directory, File}; -use xila::{network, time, virtual_file_system}; +use xila::{internationalization, network, time, virtual_file_system}; const KEYBOARD_SIZE_RATIO: f64 = 3.0 / 1.0; @@ -217,10 +216,10 @@ impl Layout { } async fn update_clock(&mut self, current_time: Duration) { - let (_, _, _, hour, minute, _) = unix_to_human_time(current_time.as_secs() as i64); - graphics::lock!({ - self.clock_string = format!("{hour:02}:{minute:02}\0"); + self.clock_string = + internationalization::format_unix_timestamp(current_time.as_secs() as i64, "%H:%M"); + self.clock_string.push('\0'); unsafe { lvgl::lv_label_set_text_static(self.clock, self.clock_string.as_ptr() as *const i8); diff --git a/modules/file_system/src/time.rs b/modules/file_system/src/time.rs index e4d7ddb0..4145c2d2 100644 --- a/modules/file_system/src/time.rs +++ b/modules/file_system/src/time.rs @@ -6,7 +6,7 @@ use core::fmt::{self, Display, Formatter}; use core::time::Duration; -use shared::unix_to_human_time; +use shared::decompose_unix_timestamp; /// Represents a point in time for file system operations. /// @@ -100,7 +100,8 @@ impl From