Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 276 additions & 9 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use jiff::tz::{TimeZone, TimeZoneDatabase};
use jiff::{Timestamp, Zoned};
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
Expand Down Expand Up @@ -399,16 +400,25 @@
// Format all the dates
for date in dates {
match date {
// TODO: Switch to lenient formatting.
Ok(date) => match strtime::format(format_string, &date) {
Ok(s) => println!("{s}"),
Err(e) => {
return Err(USimpleError::new(
1,
translate!("date-error-invalid-format", "format" => format_string, "error" => e),
));
Ok(date) => {
#[cfg(unix)]
if matches!(settings.format, Format::Custom(_) | Format::Default) {
if let Ok(s) = format_using_strftime(format_string, &date) {
println!("{s}");
continue;
}
}

match strtime::format(format_string, &date) {
Ok(s) => println!("{s}"),
Err(e) => {
return Err(USimpleError::new(
1,
translate!("date-error-invalid-format", "format" => format_string, "error" => e),
));
}
}
},
}
Err((input, _err)) => show!(USimpleError::new(
1,
translate!("date-error-invalid-date", "date" => input)
Expand Down Expand Up @@ -830,3 +840,260 @@
assert_eq!(parse_military_timezone_with_offset("9m"), None); // Starts with digit
}
}
#[cfg(unix)]
fn is_ethiopian_locale() -> bool {
for var in ["LC_ALL", "LC_TIME", "LANG"] {
if let Ok(val) = std::env::var(var) {
if val.starts_with("am_ET") {
return true;
}
}
}
false
}

#[cfg(unix)]
fn gregorian_to_ethiopian(y: i32, m: i32, d: i32) -> (i32, i32, i32) {
let (m, y) = if m <= 2 { (m + 12, y - 1) } else { (m, y) };
let jdn = (1461 * (y + 4800)) / 4 + (367 * (m - 2)) / 12 - (3 * ((y + 4900) / 100)) / 4 + d
- 32075;

let n = jdn - 1724221;
let n_cycle = n / 1461;
let r = n % 1461;
let y_rel = r / 365;
let y_rel = if r == 1460 { 3 } else { y_rel };
let year = 4 * n_cycle + y_rel + 1;
let day_of_year = r - y_rel * 365;
let month = day_of_year / 30 + 1;
let day = day_of_year % 30 + 1;
(year, month, day)
}

#[cfg(unix)]
fn format_using_strftime(format: &str, date: &Zoned) -> UResult<String> {
use nix::libc;

// Preprocess format string to handle extensions not supported by standard strftime
// or where we want to ensure specific behavior (like %N).
// specific specifiers: %N, %q, %:z, %::z, %:::z
// We use jiff to format these specific parts.

let mut new_fmt = String::with_capacity(format.len());
let mut chars = format.chars().peekable();

while let Some(c) = chars.next() {
if c == '%' {
if let Some(&next) = chars.peek() {
match next {
'N' => {
chars.next();
let nanos = date.timestamp().subsec_nanosecond();
// eprintln!("DEBUG: Nanos: {}, Full Date: {:?}", nanos, date);
let s = format!("{:09}", nanos);
new_fmt.push_str(&s);
}
's' => {
chars.next();
let s = date.timestamp().as_second().to_string();
new_fmt.push_str(&s);
}
'q' => {
chars.next();
let q = (date.month() - 1) / 3 + 1;
new_fmt.push_str(&q.to_string());
}
'z' => {
chars.next();
// %z -> +hhmm
// jiff %z matches this
new_fmt.push_str(
&jiff::fmt::strtime::format("%z", date)
.map_err(|e| USimpleError::new(1, e.to_string()))?,
);
}
'#' => {
chars.next(); // eat #
if let Some(&n2) = chars.peek() {
if n2 == 'z' {
chars.next();
// %#z -> treated as %z
new_fmt.push_str(
&jiff::fmt::strtime::format("%z", date)
.map_err(|e| USimpleError::new(1, e.to_string()))?,
);
} else {
new_fmt.push_str("%#");
}
} else {
new_fmt.push_str("%#");
}
}
':' => {
// Check for :z, ::z, :::z
chars.next(); // eat :
if let Some(&n2) = chars.peek() {
if n2 == 'z' {
chars.next();
// %:z
new_fmt.push_str(
&jiff::fmt::strtime::format("%:z", date)
.map_err(|e| USimpleError::new(1, e.to_string()))?,
);
} else if n2 == ':' {
chars.next();
if let Some(&n3) = chars.peek() {
if n3 == 'z' {
chars.next();
// %::z
new_fmt.push_str(
&jiff::fmt::strtime::format("%::z", date)
.map_err(|e| USimpleError::new(1, e.to_string()))?,
);
} else if n3 == ':' {
chars.next();
if let Some(&n4) = chars.peek() {
if n4 == 'z' {
chars.next();
// %:::z
new_fmt.push_str(
&jiff::fmt::strtime::format("%:::z", date)
.map_err(|e| {
USimpleError::new(1, e.to_string())
})?,
);
} else {
new_fmt.push_str("%:::");
}
} else {
new_fmt.push_str("%:::");
}
} else {
new_fmt.push_str("%::");
}
} else {
new_fmt.push_str("%::");
}
} else {
new_fmt.push_str("%:");
}
} else {
new_fmt.push_str("%:");
}
}
// Handle standard escape %%
'%' => {
chars.next();
new_fmt.push_str("%%");
}
_ => {
new_fmt.push('%');
// Let strftime handle the next char, just loop around
}
}
} else {
new_fmt.push('%');
}
} else {
new_fmt.push(c);
}
}

let format_string = new_fmt;

// Convert jiff::Zoned to libc::tm
// Use mem::zeroed to handle platform differences in struct fields
let mut tm: libc::tm = unsafe { std::mem::zeroed() };

tm.tm_sec = date.second() as i32;
tm.tm_min = date.minute() as i32;
tm.tm_hour = date.hour() as i32;

if is_ethiopian_locale() {
let (y, m, d) =
gregorian_to_ethiopian(date.year() as i32, date.month() as i32, date.day() as i32);
tm.tm_year = y - 1900;
tm.tm_mon = m - 1;
tm.tm_mday = d;

Check failure on line 1017 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'mday' (file:'src/uu/date/src/date.rs', line:1017)
} else {
tm.tm_mday = date.day() as i32;

Check failure on line 1019 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'mday' (file:'src/uu/date/src/date.rs', line:1019)
tm.tm_mon = date.month() as i32 - 1; // tm_mon is 0-11
tm.tm_year = date.year() as i32 - 1900; // tm_year is years since 1900
}

tm.tm_wday = date.weekday().to_sunday_zero_offset() as i32;

Check failure on line 1024 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'wday' (file:'src/uu/date/src/date.rs', line:1024)
tm.tm_yday = date.day_of_year() as i32 - 1; // tm_yday is 0-365

Check failure on line 1025 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'yday' (file:'src/uu/date/src/date.rs', line:1025)

Check failure on line 1025 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'yday' (file:'src/uu/date/src/date.rs', line:1025)
tm.tm_isdst = -1; // Let libraries determine if needed, though for formatting typically unused/ignored or uses global if zone not set

// We need to keep the CString for tm_zone valid during strftime usage
// So we declare it here
let _zone_cstring;

// Set timezone fields on supported platforms
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
{
tm.tm_gmtoff = date.offset().seconds() as _;

Check failure on line 1042 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'gmtoff' (file:'src/uu/date/src/date.rs', line:1042)

// Populate tm_zone
// We can get the abbreviation from date.time_zone().
// Note: date.time_zone() returns a TimeZone, we need the abbreviation for the specific instant?
// date.datetime() returns civil time.
// jiff::Zoned has `time_zone()` and `offset()`.
// The abbreviation usually depends on whether DST is active.
// checking `date` (Zoned) string representation usually includes it?
// `jiff` doesn't seem to expose `abbreviation()` directly on Zoned nicely?
// Wait, standard `strftime` (%Z) looks at `tm_zone`.
// How do we get abbreviation from jiff::Zoned?
// `date.time_zone()` is the TZDB entry.

Check failure on line 1054 in src/uu/date/src/date.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'TZDB' (file:'src/uu/date/src/date.rs', line:1054)
// `date.offset()` is the offset.
// We can try to format with %Z using jiff and use that string?
if let Ok(abbrev_string) = jiff::fmt::strtime::format("%Z", date) {
_zone_cstring = CString::new(abbrev_string).ok();
if let Some(ref nz) = _zone_cstring {
tm.tm_zone = nz.as_ptr() as *mut i8;
}
} else {
_zone_cstring = None;
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
)))]
{
_zone_cstring = None;
}

let format_c = CString::new(format_string).map_err(|e| {
USimpleError::new(1, format!("Invalid format string: {e}"))
})?;

let mut buffer = vec![0u8; 1024];
let ret = unsafe {
libc::strftime(
buffer.as_mut_ptr() as *mut _,
buffer.len(),
format_c.as_ptr(),
&tm as *const _,
)
};

if ret == 0 {
return Err(USimpleError::new(1, "strftime failed or result too large"));
}

let c_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) };
let s = c_str.to_string_lossy().into_owned();
Ok(s)
}
7 changes: 7 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,13 @@ fn test_date_set_valid_4() {
}

#[test]
#[cfg(unix)]
fn test_invalid_format_string() {
new_ucmd!().arg("+%!").succeeds().stdout_is("!\n");
}

#[test]
#[cfg(not(unix))]
fn test_invalid_format_string() {
let result = new_ucmd!().arg("+%!").fails();
result.no_stdout();
Expand Down
Loading