@@ -44,22 +44,65 @@ cfg_langinfo! {
4444 DEFAULT_FORMAT_CACHE . get_or_init( || {
4545 // Try to get locale format string
4646 if let Some ( format) = get_locale_format_string( ) {
47- let format_with_tz = ensure_timezone_in_format( & format) ;
48- return Box :: leak( format_with_tz. into_boxed_str( ) ) ;
47+ // Validate that the format is complete (contains date and time components)
48+ // On some platforms (e.g., macOS ARM64), nl_langinfo may return minimal formats
49+ if is_format_complete( & format) {
50+ let format_with_tz = ensure_timezone_in_format( & format) ;
51+ return Box :: leak( format_with_tz. into_boxed_str( ) ) ;
52+ }
4953 }
5054
5155 // Fallback: use 24-hour format as safe default
5256 "%a %b %e %X %Z %Y"
5357 } )
5458 }
5559
60+ /// Checks if a format string contains both date and time components.
61+ /// Returns false for minimal formats that would produce incomplete output.
62+ fn is_format_complete( format: & str ) -> bool {
63+ // Check for date components (at least one of: day, month, or year)
64+ let has_date = format. contains( "%d" ) || format. contains( "%e" )
65+ || format. contains( "%b" ) || format. contains( "%B" )
66+ || format. contains( "%m" )
67+ || format. contains( "%Y" ) || format. contains( "%y" )
68+ || format. contains( "%F" ) || format. contains( "%x" ) || format. contains( "%D" ) ;
69+
70+ // Check for time components (at least one of: hour, minute, or time format)
71+ let has_time = format. contains( "%H" ) || format. contains( "%I" )
72+ || format. contains( "%M" )
73+ || format. contains( "%T" ) || format. contains( "%X" )
74+ || format. contains( "%R" ) || format. contains( "%r" )
75+ || format. contains( "%l" ) || format. contains( "%k" )
76+ || format. contains( "%p" ) || format. contains( "%P" ) ;
77+
78+ has_date && has_time
79+ }
80+
5681 /// Retrieves the date/time format string from the system locale
5782 fn get_locale_format_string( ) -> Option <String > {
5883 unsafe {
5984 // Set locale from environment variables
6085 libc:: setlocale( libc:: LC_TIME , c"" . as_ptr( ) ) ;
6186
62- // Get the date/time format string
87+ // Try _DATE_FMT (GNU extension) first as it typically includes %Z
88+ // and is what 'locale date_fmt' returns.
89+ // On glibc, _DATE_FMT is 0x2006C (some systems use 0x2003B)
90+ #[ cfg( target_os = "linux" ) ]
91+ const _DATE_FMT: libc:: nl_item = 0x2006C ;
92+
93+ #[ cfg( target_os = "linux" ) ]
94+ {
95+ let date_fmt_ptr = libc:: nl_langinfo( _DATE_FMT) ;
96+ if !date_fmt_ptr. is_null( ) {
97+ if let Ok ( format) = CStr :: from_ptr( date_fmt_ptr) . to_str( ) {
98+ if !format. is_empty( ) {
99+ return Some ( format. to_string( ) ) ;
100+ }
101+ }
102+ }
103+ }
104+
105+ // Fallback to POSIX D_T_FMT
63106 let d_t_fmt_ptr = libc:: nl_langinfo( libc:: D_T_FMT ) ;
64107 if d_t_fmt_ptr. is_null( ) {
65108 return None ;
@@ -112,6 +155,23 @@ mod tests {
112155 cfg_langinfo ! {
113156 use super :: * ;
114157
158+ /// Helper function to expand a format string with a known test date
159+ ///
160+ /// Uses a fixed test date: Monday, January 15, 2024, 14:30:45 UTC
161+ /// This allows us to validate format strings by checking their expanded output
162+ /// rather than looking for literal format codes.
163+ fn expand_format_with_test_date( format: & str ) -> String {
164+ use jiff:: fmt:: strtime;
165+
166+ // Create test timestamp: Monday, January 15, 2024, 14:30:45 UTC
167+ // Use parse_date to get a proper Zoned timestamp (same as production code)
168+ let test_date = crate :: parse_date( "2024-01-15 14:30:45 UTC" )
169+ . expect( "Test date parse should never fail" ) ;
170+
171+ // Expand the format string with the test date
172+ strtime:: format( format, & test_date) . unwrap_or_default( )
173+ }
174+
115175 #[ test]
116176 fn test_locale_detection( ) {
117177 // Just verify the function doesn't panic
@@ -121,10 +181,31 @@ mod tests {
121181 #[ test]
122182 fn test_default_format_contains_valid_codes( ) {
123183 let format = get_locale_default_format( ) ;
124- assert!( format. contains( "%a" ) ) ; // abbreviated weekday
125- assert!( format. contains( "%b" ) ) ; // abbreviated month
126- assert!( format. contains( "%Y" ) || format. contains( "%y" ) ) ; // year (4-digit or 2-digit)
127- assert!( format. contains( "%Z" ) ) ; // timezone
184+
185+ let expanded = expand_format_with_test_date( format) ;
186+
187+ // Verify expanded output contains expected components
188+ // Test date: Monday, January 15, 2024, 14:30:45
189+ assert!(
190+ expanded. contains( "Mon" ) || expanded. contains( "Monday" ) ,
191+ "Expanded format should contain weekday name, got: {expanded}"
192+ ) ;
193+
194+ assert!(
195+ expanded. contains( "Jan" ) || expanded. contains( "January" ) ,
196+ "Expanded format should contain month name, got: {expanded}"
197+ ) ;
198+
199+ assert!(
200+ expanded. contains( "2024" ) || expanded. contains( "24" ) ,
201+ "Expanded format should contain year, got: {expanded}"
202+ ) ;
203+
204+ // Keep literal %Z check - this is enforced by ensure_timezone_in_format()
205+ assert!(
206+ format. contains( "%Z" ) ,
207+ "Format string must contain %Z timezone (enforced by ensure_timezone_in_format)"
208+ ) ;
128209 }
129210
130211 #[ test]
@@ -135,25 +216,34 @@ mod tests {
135216 // The format should not be empty
136217 assert!( !format. is_empty( ) , "Locale format should not be empty" ) ;
137218
138- // Should contain date/time components
139- let has_date_component = format. contains( "%a" )
140- || format. contains( "%A" )
141- || format. contains( "%b" )
142- || format. contains( "%B" )
143- || format. contains( "%d" )
144- || format. contains( "%e" ) ;
145- assert!( has_date_component, "Format should contain date components" ) ;
146-
147- // Should contain time component (hour)
148- let has_time_component = format. contains( "%H" )
149- || format. contains( "%I" )
150- || format. contains( "%k" )
151- || format. contains( "%l" )
152- || format. contains( "%r" )
153- || format. contains( "%R" )
154- || format. contains( "%T" )
155- || format. contains( "%X" ) ;
156- assert!( has_time_component, "Format should contain time components" ) ;
219+ let expanded = expand_format_with_test_date( format) ;
220+
221+ // Verify expanded output contains date components
222+ // Test date: Monday, January 15, 2024
223+ let has_date_component = expanded. contains( "15" ) // day
224+ || expanded. contains( "Jan" ) // month name
225+ || expanded. contains( "January" ) // full month
226+ || expanded. contains( "Mon" ) // weekday
227+ || expanded. contains( "Monday" ) ; // full weekday
228+
229+ assert!(
230+ has_date_component,
231+ "Expanded format should contain date components, got: {expanded}"
232+ ) ;
233+
234+ // Verify expanded output contains time components
235+ // Test time: 14:30:45
236+ let has_time_component = expanded. contains( "14" ) // 24-hour
237+ || expanded. contains( "02" ) // 12-hour
238+ || expanded. contains( "30" ) // minutes
239+ || expanded. contains( ':' ) // time separator
240+ || expanded. contains( "PM" ) // AM/PM indicator
241+ || expanded. contains( "pm" ) ;
242+
243+ assert!(
244+ has_time_component,
245+ "Expanded format should contain time components, got: {expanded}"
246+ ) ;
157247 }
158248
159249 #[ test]
0 commit comments