// `strftime` implementation. // // See . // TODO: set this for entire crate when possible #![deny(unsafe_op_in_unsafe_fn)] use alloc::string::String; use super::{get_offset, tm}; use crate::{ c_str::CStr, platform::{ self, WriteByte, types::{c_char, c_int, size_t}, }, }; // We use the langinfo constants use crate::header::langinfo::{ ABDAY_1, ABMON_1, AM_STR, DAY_1, MON_1, PM_STR, // TODO : other constants if needed nl_item, nl_langinfo, }; /// A helper that calls `nl_langinfo(item)` and converts the returned pointer /// into a `&str`. If it fails or is null, returns an empty string "". unsafe fn langinfo_to_str(item: nl_item) -> &'static str { use core::ffi::CStr; let ptr = unsafe { nl_langinfo(item) }; if ptr.is_null() { return ""; } match unsafe { CStr::from_ptr(ptr) }.to_str() { Ok(s) => s, Err(_) => "", } } /// See . /// /// Formats time data according to the given `format` string. /// /// Use `langinfo` for locale-based day/month names, /// but still hard-codes other aspects of the "C" locale (like numeric/date /// formats) and ignores `%E` / `%O` variations. pub unsafe fn strftime(w: &mut W, format: *const c_char, t: *const tm) -> size_t { /// Helper that actually parses the format string and writes output. pub unsafe fn inner_strftime( w: &mut W, mut format: *const c_char, t: *const tm, ) -> bool { macro_rules! w { (byte $b:expr) => {{ if w.write_u8($b).is_err() { return false; } }}; (char $chr:expr) => {{ if w.write_char($chr).is_err() { return false; } }}; (recurse $fmt:expr) => {{ let mut tmp = String::with_capacity($fmt.len() + 1); tmp.push_str($fmt); tmp.push('\0'); if unsafe { !inner_strftime(w, tmp.as_ptr() as *mut c_char, t) } { return false; } }}; ($str:expr) => {{ if w.write_str($str).is_err() { return false; } }}; ($fmt:expr, $($args:expr),+) => {{ // Could use write!() if we didn't need the exact count if write!(w, $fmt, $($args),+).is_err() { return false; } }}; } while unsafe { *format } != 0 { // If the character isn't '%', just copy it out. if unsafe { *format } as u8 != b'%' { w!(byte unsafe { *format } as u8); format = unsafe { format.offset(1) }; continue; } // Skip '%' format = unsafe { format.offset(1) }; // POSIX says '%E' and '%O' can modify numeric formats for locales, // but we ignore them in this minimal "C" locale approach. if unsafe { *format } as u8 == b'E' || unsafe { *format } as u8 == b'O' { format = unsafe { format.offset(1) }; } match unsafe { *format } as u8 { // Literal '%' b'%' => w!(byte b'%'), // Newline and tab expansions b'n' => w!(byte b'\n'), b't' => w!(byte b'\t'), // Abbreviated weekday name: %a b'a' => { // `ABDAY_1 + tm_wday` is the correct langinfo ID for abbreviated weekdays let s = unsafe { langinfo_to_str(ABDAY_1 + (*t).tm_wday as i32) }; w!(s); } // Full weekday name: %A b'A' => { // `DAY_1 + tm_wday` is the correct langinfo ID for full weekdays let s = unsafe { langinfo_to_str(DAY_1 + (*t).tm_wday as i32) }; w!(s); } // Abbreviated month name: %b or %h b'b' | b'h' => { let s = unsafe { langinfo_to_str(ABMON_1 + (*t).tm_mon as i32) }; w!(s); } // Full month name: %B b'B' => { let s = unsafe { langinfo_to_str(MON_1 + (*t).tm_mon as i32) }; w!(s); } // Century: %C b'C' => { let mut year = unsafe { (*t).tm_year } / 100; if unsafe { (*t).tm_year } % 100 != 0 { year += 1; } w!("{:02}", year + 19); } // Day of month: %d b'd' => w!("{:02}", unsafe { (*t).tm_mday }), // %D => same as %m/%d/%y b'D' => w!(recurse "%m/%d/%y"), // Day of month, space-padded: %e b'e' => w!("{:2}", unsafe { (*t).tm_mday }), // ISO 8601 date: %F => %Y-%m-%d b'F' => w!(recurse "%Y-%m-%d"), // Hour (00-23): %H b'H' => w!("{:02}", unsafe { (*t).tm_hour }), // Hour (01-12): %I b'I' => w!("{:02}", (unsafe { (*t).tm_hour } + 12 - 1) % 12 + 1), // Day of year: %j b'j' => w!("{:03}", unsafe { (*t).tm_yday } + 1), // etc. b'k' => w!("{:2}", unsafe { (*t).tm_hour }), b'l' => w!("{:2}", (unsafe { (*t).tm_hour } + 12 - 1) % 12 + 1), b'm' => w!("{:02}", unsafe { (*t).tm_mon } + 1), b'M' => w!("{:02}", unsafe { (*t).tm_min }), // AM/PM (uppercase): %p b'p' => { // Get "AM" / "PM" from langinfo if unsafe { (*t).tm_hour } < 12 { w!(unsafe { langinfo_to_str(AM_STR) }); } else { w!(unsafe { langinfo_to_str(PM_STR) }); } } // am/pm (lowercase): %P b'P' => { // Convert the AM_STR / PM_STR to lowercase if unsafe { (*t).tm_hour } < 12 { let am = unsafe { langinfo_to_str(AM_STR) }.to_ascii_lowercase(); w!(&am); } else { let pm = unsafe { langinfo_to_str(PM_STR) }.to_ascii_lowercase(); w!(&pm); } } // 12-hour clock with seconds + AM/PM: %r => %I:%M:%S %p b'r' => w!(recurse "%I:%M:%S %p"), // 24-hour clock without seconds: %R => %H:%M b'R' => w!(recurse "%H:%M"), // Seconds since the Epoch: %s => calls mktime() to convert tm to time_t b's' => w!("{}", unsafe { super::mktime(t as *mut tm) }), // Seconds (00-60): %S (unchanged) b'S' => w!("{:02}", unsafe { (*t).tm_sec }), // 24-hour clock with seconds: %T => %H:%M:%S b'T' => w!(recurse "%H:%M:%S"), // Weekday (1-7, Monday=1): %u b'u' => w!("{}", (unsafe { (*t).tm_wday } + 7 - 1) % 7 + 1), // Sunday-based week of year: %U b'U' => w!( "{}", (unsafe { (*t).tm_yday } + 7 - unsafe { (*t).tm_wday }) / 7 ), // ISO-8601 week of year b'V' => w!("{}", week_of_year(unsafe { &*t })), // Weekday (0-6, Sunday=0): %w b'w' => w!("{}", unsafe { (*t).tm_wday }), // Monday-based week of year: %W b'W' => w!( "{}", (unsafe { (*t).tm_yday } + 7 - (unsafe { (*t).tm_wday } + 6) % 7) / 7 ), // Last two digits of year: %y b'y' => w!("{:02}", unsafe { (*t).tm_year } % 100), // Full year: %Y b'Y' => w!("{}", unsafe { (*t).tm_year } + 1900), // Timezone offset: %z b'z' => { let offset = unsafe { (*t).tm_gmtoff }; let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; let mins = offset.div_euclid(60); let min = mins.rem_euclid(60); let hour = mins.div_euclid(60); w!("{}{:02}{:02}", sign, hour, min) } // Timezone name: %Z b'Z' => w!( "{}", unsafe { CStr::from_ptr((*t).tm_zone) }.to_str().unwrap() ), // Date+time+TZ: %+ b'+' => w!(recurse "%a %b %d %T %Z %Y"), // Unrecognized format specifier => fail _ => return false, } // Move past the format specifier format = unsafe { format.offset(1) }; } true } // Wrap the writer in a CountingWriter to return how many bytes were written. let mut cw = platform::CountingWriter::new(w); if unsafe { !inner_strftime(&mut cw, format, t) } { return 0; } cw.written } /// Calculate number of weeks in a year as defined by ISO 8601 /// /// ## Source /// https://en.wikipedia.org/wiki/ISO_week_date fn weeks_per_year(year: c_int) -> c_int { let year = year as f64; let p_y = (year + (year / 4.) - (year / 100.) + (year / 400.)) as c_int % 7; if p_y == 4 { 53 } else { 52 } } /// Calculate the week of the year accounting for leap weeks (ISO 8601) /// /// ## Source /// https://en.wikipedia.org/wiki/ISO_week_date fn week_of_year(time: &tm) -> c_int { let week = (10 + time.tm_yday - time.tm_wday) / 7; if week <= 1 { weeks_per_year(time.tm_year - 1) } else if week > weeks_per_year(time.tm_year) { 1 } else { week } }