Skip to main content

deep_time/ymdhms/
to_str.rs

1use crate::{Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, Scale, YmdHmsRich, an_err};
2
3pub(crate) const WEEKDAYS_FULL: [&[u8]; 7] = [
4    b"Sunday",
5    b"Monday",
6    b"Tuesday",
7    b"Wednesday",
8    b"Thursday",
9    b"Friday",
10    b"Saturday",
11];
12pub(crate) const WEEKDAYS_ABBR: [&[u8]; 7] =
13    [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
14pub(crate) const MONTHS_FULL: [&[u8]; 12] = [
15    b"January",
16    b"February",
17    b"March",
18    b"April",
19    b"May",
20    b"June",
21    b"July",
22    b"August",
23    b"September",
24    b"October",
25    b"November",
26    b"December",
27];
28pub(crate) const MONTHS_ABBR: [&[u8]; 12] = [
29    b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
30];
31
32impl YmdHmsRich {
33    /// Equivalent of strftime.
34    ///
35    /// Requires `"alloc"` feature.
36    #[cfg(feature = "alloc")]
37    pub fn to_str(&self, fmt: &str) -> Result<alloc::string::String, DtErr> {
38        let mut buf = [0u8; STRFTIME_SIZE];
39        let mut pos = 0usize;
40        self.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
41        Ok(alloc::string::String::from_utf8_lossy(&buf[0..pos]).into_owned())
42    }
43
44    /// No-allocation formatting.
45    ///
46    /// ```rust
47    /// use deep_time::{Dt, Scale};
48    ///
49    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
50    /// let y = x.to_ymd_rich();
51    /// let b = y.to_str_bin("%F").unwrap();
52    /// let s = b.as_str().unwrap();
53    ///
54    /// println!("{}", s);
55    /// ```
56    pub fn to_str_bin(&self, fmt: &str) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
57        let mut buf = [0u8; STRFTIME_SIZE];
58        let mut pos = 0usize;
59        self.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
60        LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))
61    }
62
63    pub(crate) fn format_to_buffer(
64        &self,
65        fmt: &[u8],
66        buf: &mut [u8; STRFTIME_SIZE],
67        pos: &mut usize,
68    ) -> Result<(), DtErr> {
69        let mut i = 0usize;
70
71        while i < fmt.len() {
72            let byte = fmt[i];
73
74            if byte != b'%' {
75                Self::write_bytes(buf, pos, &[byte]);
76                i += 1;
77                continue;
78            }
79
80            i += 1; // skip '%'
81
82            if i >= fmt.len() {
83                return Err(an_err!(DtErrKind::UnexpectedEnd, "after %"));
84            }
85
86            // %% → literal percent
87            if fmt[i] == b'%' {
88                Self::write_bytes(buf, pos, b"%");
89                i += 1;
90                continue;
91            }
92
93            // ── Parse optional flags (- 0 _ ~) ───────────────────────
94            // ~ means "trim trailing zeros" (only affects %f / %N fractional seconds)
95            let mut flag = b'0'; // temporary default; many directives override it via pad param
96            let mut trim_trailing = false;
97            while i < fmt.len() {
98                match fmt[i] {
99                    b'-' | b'0' | b'_' => {
100                        flag = fmt[i];
101                        i += 1;
102                    }
103                    b'~' => {
104                        trim_trailing = true;
105                        i += 1;
106                    }
107                    _ => break,
108                }
109            }
110
111            // ── Parse optional width ───────────────────────────────
112            let mut width: Option<u8> = None;
113            let width_start = i;
114            while i < fmt.len() && fmt[i].is_ascii_digit() {
115                i += 1;
116            }
117            if i > width_start
118                && let Ok(s) = core::str::from_utf8(&fmt[width_start..i])
119                && let Ok(w) = s.parse::<u8>()
120            {
121                width = Some(w);
122            }
123
124            // ── Parse optional colons (: :: :::) ───────────────────
125            let mut colons: u8 = 0;
126            while i < fmt.len() && fmt[i] == b':' {
127                colons += 1;
128                i += 1;
129            }
130
131            if i >= fmt.len() {
132                return Err(an_err!(DtErrKind::UnexpectedEnd, "after %"));
133            }
134
135            let directive = fmt[i];
136            i += 1;
137
138            // ── Special case: %.f or %.9f etc. ─────────────────────
139            if directive == b'.' {
140                let mut frac_width: Option<u8> = None;
141                let frac_start = i;
142                while i < fmt.len() && fmt[i].is_ascii_digit() {
143                    i += 1;
144                }
145                if i > frac_start
146                    && let Ok(s) = core::str::from_utf8(&fmt[frac_start..i])
147                    && let Ok(w) = s.parse::<u8>()
148                {
149                    frac_width = Some(w);
150                }
151                if i >= fmt.len() {
152                    return Err(an_err!(DtErrKind::BadFractional, "expected f or N after ."));
153                }
154
155                // optional ~ for trim trailing zeros, after width e.g. %.3~f or %.~f
156                if fmt[i] == b'~' {
157                    trim_trailing = true;
158                    i += 1;
159                }
160
161                if i >= fmt.len() {
162                    return Err(an_err!(DtErrKind::BadFractional, "expected f or N after ."));
163                }
164
165                let next = fmt[i];
166                i += 1;
167
168                if matches!(next, b'f' | b'N') {
169                    // Only print the dot for %f when width > 0.
170                    // When trim_trailing (~) is used and the fractional part is zero
171                    // after trimming, we suppress the dot entirely. This gives clean
172                    // RFC 3339 / ISO 8601 output (no ".000..." for integer seconds).
173                    let width_val = frac_width.unwrap_or(18);
174                    let add_dot = (next == b'f') && (width_val > 0);
175
176                    let dot_pos = if add_dot {
177                        let p = *pos;
178                        Self::write_bytes(buf, pos, b".");
179                        Some(p)
180                    } else {
181                        None
182                    };
183
184                    let wrote_frac = self.write_fractional_seconds(
185                        buf,
186                        pos,
187                        flag,
188                        frac_width,
189                        colons,
190                        trim_trailing,
191                    );
192
193                    if add_dot && !wrote_frac {
194                        // Nothing significant was written → remove the dot
195                        if let Some(p) = dot_pos {
196                            *pos = p;
197                        }
198                    }
199                    continue;
200                } else {
201                    return Err(an_err!(DtErrKind::BadFractional, "expected f or N after ."));
202                }
203            }
204
205            // ── Normal directives ──
206            match directive {
207                b'A' => self.write_weekday_full(buf, pos),
208                b'a' => self.write_weekday_abbrev(buf, pos),
209                b'B' => self.write_month_name_full(buf, pos),
210                b'b' | b'h' => self.write_month_name_abbrev(buf, pos),
211                b'C' => self.write_century(buf, pos, flag, width, colons),
212                b'd' | b'e' => self.write_day_of_month(buf, pos, flag, width, colons, true),
213                b'f' | b'N' => {
214                    let _ =
215                        self.write_fractional_seconds(buf, pos, flag, width, colons, trim_trailing);
216                }
217                b'G' => self.write_iso_week_year(buf, pos, flag, width, colons),
218                b'g' => self.write_two_digit_iso_week_year(buf, pos, flag, width, colons),
219                b'H' | b'k' => self.write_hour24(buf, pos, flag, width, colons, true),
220                b'I' | b'l' => self.write_hour12(buf, pos, flag, width, colons),
221                b'j' => self.write_day_of_year(buf, pos, flag, width, colons),
222                b'M' => self.write_minute(buf, pos, flag, width, colons, true),
223                b'm' => self.write_month_number(buf, pos, flag, width, colons, true),
224                b'q' => self.write_quarter(buf, pos, flag, width, colons),
225                b'n' => self.write_whitespace(buf, pos, b'n'),
226                b't' => self.write_whitespace(buf, pos, b't'),
227                b'P' => self.write_ampm(buf, pos, false),
228                b'p' => self.write_ampm(buf, pos, true),
229                b'r' => self.write_12hour_time_with_ampm(buf, pos),
230                b'S' => self.write_second(buf, pos, flag, width, colons, true),
231                b's' => self.write_unix_timestamp(buf, pos, flag, width, colons),
232                b'U' => self.write_week_number_sunday_based(buf, pos, flag, width, colons),
233                b'u' => self.write_weekday_number_monday_based(buf, pos, flag, width, colons),
234                b'V' => self.write_week_iso(buf, pos, flag, width, colons),
235                b'W' => self.write_week_number_monday_based(buf, pos, flag, width, colons),
236                b'w' => self.write_weekday_number_sunday_based(buf, pos, flag, width, colons),
237                b'Y' => self.write_full_year(buf, pos, flag, width, colons, true),
238                b'y' => self.write_two_digit_year(buf, pos, flag, width, colons, true),
239                b'z' => self.write_timezone_offset(buf, pos, flag, width, colons),
240                b'F' => self.write_iso_date(buf, pos),
241                b'D' => self.write_us_date_shortcut(buf, pos),
242                b'T' => self.write_time_with_seconds_shortcut(buf, pos),
243                b'R' => self.write_time_without_seconds_shortcut(buf, pos),
244                b'Z' => self.write_timezone_abbrev(buf, pos),
245
246                b'Q' => {
247                    /*
248                    we skip writing UTC fallback when:
249                    1. there's no iana
250                    2. %Q directive present
251                    3. offset isn't 0
252                    */
253                    if let Some(iana) = self.tz() {
254                        Self::write_bytes(buf, pos, iana.as_bytes());
255                    } else if let Some(abbrev) = self.tz_abbrev() {
256                        Self::write_bytes(buf, pos, abbrev.as_bytes());
257                    } else if self.offset_sec().unwrap_or_default() == 0 {
258                        Self::write_bytes(buf, pos, "UTC".as_bytes());
259                    } else if i >= fmt.len() {
260                        while *pos > 0 && matches!(buf[*pos - 1], b' ' | b'\t' | b'\n' | b'\r') {
261                            *pos -= 1;
262                        }
263                    }
264                }
265                b'L' => {
266                    if self.scale != Scale::UTC {
267                        Self::write_bytes(buf, pos, self.scale.abbrev().as_bytes())
268                    }
269                }
270                b'*' => self.write_unbounded_year(buf, pos, flag, width, colons),
271
272                b'c' | b'X' | b'x' => self.write_unsupported(buf, pos),
273                _ => return Err(an_err!(DtErrKind::UnknownDirective)),
274            }
275        }
276
277        Ok(())
278    }
279
280    fn write_bytes(buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, bytes: &[u8]) {
281        let len = bytes.len();
282        if *pos + len > STRFTIME_SIZE {
283            return;
284        }
285        buf[*pos..*pos + len].copy_from_slice(bytes);
286        *pos += len;
287    }
288
289    fn write_u32_padded(
290        buf: &mut [u8; STRFTIME_SIZE],
291        pos: &mut usize,
292        mut value: u32,
293        flag: u8,
294        width: Option<u8>,
295        default_pad: u8,
296    ) {
297        let w = width.unwrap_or(2) as usize;
298
299        // ── strftime semantics ─────────────────────────────────────
300        // -  = no padding (minimal width)
301        // 0  = zero-pad on the left
302        // _  = space-pad on the left
303        // (no flag) = use the caller's default_pad (usually '0')
304        let pad_char = match flag {
305            b'0' => b'0',
306            b'_' => b' ',
307            _ => default_pad,
308        };
309        let pad_left = flag != b'-';
310
311        let mut digits = [0u8; 20];
312        let mut i = 0usize;
313
314        if value == 0 {
315            digits[0] = b'0';
316            i = 1;
317        } else {
318            while value > 0 {
319                digits[i] = b'0' + (value % 10) as u8;
320                value /= 10;
321                i += 1;
322            }
323        }
324        let num_digits = i;
325        let pad_len = if pad_left && num_digits < w {
326            w - num_digits
327        } else {
328            0
329        };
330
331        if *pos + num_digits + pad_len > STRFTIME_SIZE {
332            return;
333        }
334
335        if pad_left {
336            for _ in 0..pad_len {
337                buf[*pos] = pad_char;
338                *pos += 1;
339            }
340        }
341
342        for j in (0..num_digits).rev() {
343            buf[*pos] = digits[j];
344            *pos += 1;
345        }
346        // No right-padding ever (strftime does not do this for date fields)
347    }
348
349    #[allow(unused_mut)]
350    fn write_i64(mut buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, value: i64) {
351        if value == 0 {
352            Self::write_bytes(buf, pos, b"0");
353            return;
354        }
355
356        let negative = value < 0;
357        let mut v = if negative {
358            value.wrapping_neg()
359        } else {
360            value
361        };
362
363        let mut digits = [0u8; 20];
364        let mut i = 0usize;
365        while v > 0 {
366            digits[i] = b'0' + (v % 10) as u8;
367            v /= 10;
368            i += 1;
369        }
370
371        if negative {
372            if *pos >= STRFTIME_SIZE {
373                return;
374            }
375            buf[*pos] = b'-';
376            *pos += 1;
377        }
378
379        if *pos + i > STRFTIME_SIZE {
380            return;
381        }
382        for j in (0..i).rev() {
383            buf[*pos] = digits[j];
384            *pos += 1;
385        }
386    }
387
388    fn write_i64_padded(
389        buf: &mut [u8; STRFTIME_SIZE],
390        pos: &mut usize,
391        value: i64,
392        flag: u8,
393        width: Option<u8>,
394        default_pad: u8,
395    ) {
396        let w = width.unwrap_or(4) as usize; // %Y and %G default to 4 digits
397
398        let negative = value < 0;
399        let abs_val = if negative {
400            value.wrapping_neg()
401        } else {
402            value
403        };
404
405        let mut digits = [0u8; 20];
406        let mut i = 0usize;
407
408        let mut v = abs_val;
409        if v == 0 {
410            digits[0] = b'0';
411            i = 1;
412        } else {
413            while v > 0 {
414                digits[i] = b'0' + (v % 10) as u8;
415                v /= 10;
416                i += 1;
417            }
418        }
419
420        let num_digits = i;
421        let pad_char = match flag {
422            b'-' => b' ',
423            b'0' => b'0',
424            b'_' => b' ',
425            _ => default_pad,
426        };
427        let pad_left = flag != b'-';
428        let pad_len = if pad_left && num_digits < w {
429            w - num_digits
430        } else {
431            0
432        };
433
434        if *pos + (if negative { 1 } else { 0 }) + num_digits + pad_len > STRFTIME_SIZE {
435            return;
436        }
437
438        if negative {
439            buf[*pos] = b'-';
440            *pos += 1;
441        }
442
443        if pad_left {
444            for _ in 0..pad_len {
445                buf[*pos] = pad_char;
446                *pos += 1;
447            }
448        }
449
450        for j in (0..num_digits).rev() {
451            buf[*pos] = digits[j];
452            *pos += 1;
453        }
454    }
455
456    fn write_fractional(
457        buf: &mut [u8; STRFTIME_SIZE],
458        pos: &mut usize,
459        subsec: u64,
460        width: Option<u8>,
461        trim: bool,
462    ) -> bool {
463        let w = width.unwrap_or(18).min(18) as usize;
464        if w == 0 {
465            return false;
466        }
467
468        let mut n = subsec;
469        let mut digits = [b'0'; 18];
470        for i in (0..18).rev() {
471            digits[i] = b'0' + (n % 10) as u8;
472            n /= 10;
473        }
474
475        let mut end = w;
476        if trim {
477            // Trim trailing zeros from the least-significant end of the selected width.
478            // If everything is zero after trimming, return false so the caller
479            // can suppress the decimal point entirely (perfect for RFC 3339 / ISO 8601).
480            while end > 0 && digits[end - 1] == b'0' {
481                end -= 1;
482            }
483            if end == 0 {
484                return false; // emit nothing at all (no dot, no "0")
485            }
486        }
487        Self::write_bytes(buf, pos, &digits[0..end]);
488        true
489    }
490
491    // ──────────────────────────────────────────────────────────────
492    // Individual write_ functions – one per parser directive
493    // ──────────────────────────────────────────────────────────────
494
495    #[inline]
496    pub(crate) fn write_weekday_full(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
497        let name = WEEKDAYS_FULL[self.wkday as usize];
498        Self::write_bytes(buf, pos, name);
499    }
500
501    #[inline]
502    pub(crate) fn write_weekday_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
503        let name = WEEKDAYS_ABBR[self.wkday as usize];
504        Self::write_bytes(buf, pos, name);
505    }
506
507    #[inline]
508    pub(crate) fn write_month_name_full(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
509        let name = MONTHS_FULL[self.mo as usize - 1];
510        Self::write_bytes(buf, pos, name);
511    }
512
513    #[inline]
514    pub(crate) fn write_month_name_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
515        let name = MONTHS_ABBR[self.mo as usize - 1];
516        Self::write_bytes(buf, pos, name);
517    }
518
519    #[inline]
520    pub(crate) fn write_century(
521        &self,
522        buf: &mut [u8; STRFTIME_SIZE],
523        pos: &mut usize,
524        _flag: u8,
525        _width: Option<u8>,
526        _colons: u8,
527    ) {
528        // Floor division → -123 becomes -2 (exactly matches parse_century)
529        let century = self.yr.div_euclid(100);
530        Self::write_i64(buf, pos, century);
531    }
532
533    #[inline]
534    pub(crate) fn write_day_of_month(
535        &self,
536        buf: &mut [u8; STRFTIME_SIZE],
537        pos: &mut usize,
538        flag: u8,
539        width: Option<u8>,
540        _colons: u8,
541        pad: bool,
542    ) {
543        let default_pad = if pad { b'0' } else { b' ' };
544        Self::write_u32_padded(buf, pos, self.day as u32, flag, width, default_pad);
545    }
546
547    #[inline]
548    pub(crate) fn write_fractional_seconds(
549        &self,
550        buf: &mut [u8; STRFTIME_SIZE],
551        pos: &mut usize,
552        _flag: u8,
553        width: Option<u8>,
554        _colons: u8,
555        trim: bool,
556    ) -> bool {
557        Self::write_fractional(buf, pos, self.attos, width, trim)
558    }
559
560    #[inline]
561    pub(crate) fn write_iso_week_year(
562        &self,
563        buf: &mut [u8; STRFTIME_SIZE],
564        pos: &mut usize,
565        flag: u8,
566        width: Option<u8>,
567        _colons: u8,
568    ) {
569        Self::write_i64_padded(buf, pos, self.iso_yr, flag, width, b'0');
570    }
571
572    #[inline]
573    pub(crate) fn write_two_digit_iso_week_year(
574        &self,
575        buf: &mut [u8; STRFTIME_SIZE],
576        pos: &mut usize,
577        flag: u8,
578        width: Option<u8>,
579        _colons: u8,
580    ) {
581        let yy = (self.iso_yr % 100).saturating_abs() as u32;
582        Self::write_u32_padded(buf, pos, yy, flag, width.or(Some(2)), b'0');
583    }
584
585    #[inline]
586    pub(crate) fn write_hour24(
587        &self,
588        buf: &mut [u8; STRFTIME_SIZE],
589        pos: &mut usize,
590        flag: u8,
591        width: Option<u8>,
592        _colons: u8,
593        pad: bool,
594    ) {
595        let default_pad = if pad { b'0' } else { b' ' };
596        Self::write_u32_padded(buf, pos, self.hr as u32, flag, width, default_pad);
597    }
598
599    #[inline]
600    pub(crate) fn write_hour12(
601        &self,
602        buf: &mut [u8; STRFTIME_SIZE],
603        pos: &mut usize,
604        flag: u8,
605        width: Option<u8>,
606        _colons: u8,
607    ) {
608        let hour24 = self.hr;
609        let hour12 = if hour24 == 0 {
610            12
611        } else if hour24 > 12 {
612            hour24 - 12
613        } else {
614            hour24
615        };
616        Self::write_u32_padded(buf, pos, hour12 as u32, flag, width.or(Some(2)), b'0');
617    }
618
619    #[inline]
620    pub(crate) fn write_12hour_time_with_ampm(
621        &self,
622        buf: &mut [u8; STRFTIME_SIZE],
623        pos: &mut usize,
624    ) {
625        // Hour (12-hour, zero-padded)
626        self.write_hour12(buf, pos, b'0', Some(2), 0);
627        Self::write_bytes(buf, pos, b":");
628        // Minute (zero-padded)
629        self.write_minute(buf, pos, b'0', Some(2), 0, true);
630        Self::write_bytes(buf, pos, b":");
631        // Second (zero-padded)
632        self.write_second(buf, pos, b'0', Some(2), 0, true);
633        Self::write_bytes(buf, pos, b" ");
634        // AM/PM (uppercase, matching classic POSIX %r)
635        self.write_ampm(buf, pos, true);
636    }
637
638    #[inline]
639    pub(crate) fn write_day_of_year(
640        &self,
641        buf: &mut [u8; STRFTIME_SIZE],
642        pos: &mut usize,
643        flag: u8,
644        width: Option<u8>,
645        _colons: u8,
646    ) {
647        Self::write_u32_padded(
648            buf,
649            pos,
650            self.day_of_yr as u32,
651            flag,
652            width.or(Some(3)),
653            b'0',
654        );
655    }
656
657    #[inline]
658    pub(crate) fn write_minute(
659        &self,
660        buf: &mut [u8; STRFTIME_SIZE],
661        pos: &mut usize,
662        flag: u8,
663        width: Option<u8>,
664        _colons: u8,
665        pad: bool,
666    ) {
667        let default_pad = if pad { b'0' } else { b' ' };
668        Self::write_u32_padded(buf, pos, self.min as u32, flag, width, default_pad);
669    }
670
671    #[inline]
672    pub(crate) fn write_month_number(
673        &self,
674        buf: &mut [u8; STRFTIME_SIZE],
675        pos: &mut usize,
676        flag: u8,
677        width: Option<u8>,
678        _colons: u8,
679        pad: bool,
680    ) {
681        let default_pad = if pad { b'0' } else { b' ' };
682        Self::write_u32_padded(buf, pos, self.mo as u32, flag, width, default_pad);
683    }
684
685    #[inline]
686    pub(crate) fn write_quarter(
687        &self,
688        buf: &mut [u8; STRFTIME_SIZE],
689        pos: &mut usize,
690        flag: u8,
691        width: Option<u8>,
692        _colons: u8,
693    ) {
694        // Month is 1–12, so this gives 1, 2, 3, or 4
695        let quarter = ((self.mo - 1) / 3 + 1) as u32;
696
697        // Default width is 1 (no leading zero unless requested)
698        Self::write_u32_padded(buf, pos, quarter, flag, width.or(Some(1)), b'0');
699    }
700
701    #[inline]
702    pub(crate) fn write_whitespace(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, ch: u8) {
703        let bytes = if ch == b'n' { b"\n" } else { b"\t" };
704        Self::write_bytes(buf, pos, bytes);
705    }
706
707    #[inline]
708    pub(crate) fn write_ampm(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, upper: bool) {
709        let hour = self.hr;
710        let bytes = if hour < 12 {
711            if upper { b"AM" } else { b"am" }
712        } else if upper {
713            b"PM"
714        } else {
715            b"pm"
716        };
717        Self::write_bytes(buf, pos, bytes);
718    }
719
720    #[inline]
721    pub(crate) fn write_second(
722        &self,
723        buf: &mut [u8; STRFTIME_SIZE],
724        pos: &mut usize,
725        flag: u8,
726        width: Option<u8>,
727        _colons: u8,
728        pad: bool,
729    ) {
730        let default_pad = if pad { b'0' } else { b' ' };
731        Self::write_u32_padded(buf, pos, self.sec as u32, flag, width, default_pad);
732    }
733
734    #[inline]
735    pub(crate) fn write_unix_timestamp(
736        &self,
737        buf: &mut [u8; STRFTIME_SIZE],
738        pos: &mut usize,
739        _flag: u8,
740        _width: Option<u8>,
741        _colons: u8,
742    ) {
743        let (seconds, _) = self.unix_timestamp();
744        Self::write_i64(buf, pos, seconds);
745    }
746
747    #[inline]
748    pub(crate) fn write_weekday_number_sunday_based(
749        &self,
750        buf: &mut [u8; STRFTIME_SIZE],
751        pos: &mut usize,
752        flag: u8,
753        width: Option<u8>,
754        _colons: u8,
755    ) {
756        Self::write_u32_padded(
757            buf,
758            pos,
759            self.wkday_sun() as u32,
760            flag,
761            width.or(Some(1)),
762            b'0',
763        );
764    }
765
766    #[inline]
767    pub(crate) fn write_weekday_number_monday_based(
768        &self,
769        buf: &mut [u8; STRFTIME_SIZE],
770        pos: &mut usize,
771        flag: u8,
772        width: Option<u8>,
773        _colons: u8,
774    ) {
775        Self::write_u32_padded(
776            buf,
777            pos,
778            self.wkday_mon() as u32,
779            flag,
780            width.or(Some(1)),
781            b'0',
782        );
783    }
784
785    #[inline]
786    pub(crate) fn write_week_iso(
787        &self,
788        buf: &mut [u8; STRFTIME_SIZE],
789        pos: &mut usize,
790        flag: u8,
791        width: Option<u8>,
792        _colons: u8,
793    ) {
794        Self::write_u32_padded(buf, pos, self.iso_wk as u32, flag, width.or(Some(2)), b'0');
795    }
796
797    #[inline]
798    pub(crate) fn write_week_number_sunday_based(
799        &self,
800        buf: &mut [u8; STRFTIME_SIZE],
801        pos: &mut usize,
802        flag: u8,
803        width: Option<u8>,
804        _colons: u8,
805    ) {
806        Self::write_u32_padded(
807            buf,
808            pos,
809            self.wk_of_yr_sun as u32,
810            flag,
811            width.or(Some(2)),
812            b'0',
813        );
814    }
815
816    #[inline]
817    pub(crate) fn write_week_number_monday_based(
818        &self,
819        buf: &mut [u8; STRFTIME_SIZE],
820        pos: &mut usize,
821        flag: u8,
822        width: Option<u8>,
823        _colons: u8,
824    ) {
825        Self::write_u32_padded(
826            buf,
827            pos,
828            self.wk_of_yr_mon as u32,
829            flag,
830            width.or(Some(2)),
831            b'0',
832        );
833    }
834
835    #[inline]
836    pub(crate) fn write_full_year(
837        &self,
838        buf: &mut [u8; STRFTIME_SIZE],
839        pos: &mut usize,
840        flag: u8,
841        width: Option<u8>,
842        _colons: u8,
843        _pad: bool,
844    ) {
845        Self::write_i64_padded(buf, pos, self.yr, flag, width, b'0');
846    }
847
848    #[inline]
849    pub(crate) fn write_two_digit_year(
850        &self,
851        buf: &mut [u8; STRFTIME_SIZE],
852        pos: &mut usize,
853        flag: u8,
854        width: Option<u8>,
855        _colons: u8,
856        _pad: bool,
857    ) {
858        let yy = (self.yr % 100).saturating_abs() as u32;
859        Self::write_u32_padded(buf, pos, yy, flag, width.or(Some(2)), b'0');
860    }
861
862    #[inline]
863    pub(crate) fn write_unbounded_year(
864        &self,
865        buf: &mut [u8; STRFTIME_SIZE],
866        pos: &mut usize,
867        flag: u8,
868        width: Option<u8>,
869        _colons: u8,
870    ) {
871        Self::write_i64_padded(buf, pos, self.yr, flag, width, b'0');
872    }
873
874    pub(crate) fn write_timezone_offset(
875        &self,
876        buf: &mut [u8; STRFTIME_SIZE],
877        pos: &mut usize,
878        _flag: u8,
879        _width: Option<u8>,
880        colons: u8,
881    ) {
882        let Some(offset_sec) = self.offset_sec() else {
883            return;
884        };
885        let (negative, hours, minutes) = Dt::sec_as_hhmm(offset_sec);
886        let sign = if negative { b'-' } else { b'+' };
887
888        // seconds component — only needed for %::z and %:::z+
889        let seconds = ((offset_sec.saturating_abs() % 3600) % 60) as u8;
890
891        match colons {
892            // %z → +HHMM
893            0 => {
894                let mut tmp = [0u8; 5];
895                tmp[0] = sign;
896                tmp[1] = b'0' + hours / 10;
897                tmp[2] = b'0' + hours % 10;
898                tmp[3] = b'0' + minutes / 10;
899                tmp[4] = b'0' + minutes % 10;
900                Self::write_bytes(buf, pos, &tmp);
901            }
902
903            // %:z → +HH:MM
904            1 => {
905                let mut tmp = [0u8; 6];
906                tmp[0] = sign;
907                tmp[1] = b'0' + hours / 10;
908                tmp[2] = b'0' + hours % 10;
909                tmp[3] = b':';
910                tmp[4] = b'0' + minutes / 10;
911                tmp[5] = b'0' + minutes % 10;
912                Self::write_bytes(buf, pos, &tmp);
913            }
914
915            // %::z → +HH:MM:SS (always include seconds)
916            2 => {
917                let mut tmp = [0u8; 9];
918                tmp[0] = sign;
919                tmp[1] = b'0' + hours / 10;
920                tmp[2] = b'0' + hours % 10;
921                tmp[3] = b':';
922                tmp[4] = b'0' + minutes / 10;
923                tmp[5] = b'0' + minutes % 10;
924                tmp[6] = b':';
925                tmp[7] = b'0' + seconds / 10;
926                tmp[8] = b'0' + seconds % 10;
927                Self::write_bytes(buf, pos, &tmp);
928            }
929
930            // %:::z and higher → use colons, but only show minutes/seconds when non-zero
931            _ => {
932                let mut tmp = [0u8; 9];
933                let mut len = 0usize;
934
935                tmp[len] = sign;
936                len += 1;
937                tmp[len] = b'0' + hours / 10;
938                len += 1;
939                tmp[len] = b'0' + hours % 10;
940                len += 1;
941
942                // Include minutes only if non-zero (or if seconds are present)
943                if minutes != 0 || seconds != 0 {
944                    tmp[len] = b':';
945                    len += 1;
946                    tmp[len] = b'0' + minutes / 10;
947                    len += 1;
948                    tmp[len] = b'0' + minutes % 10;
949                    len += 1;
950
951                    // Include seconds only if non-zero
952                    if seconds != 0 {
953                        tmp[len] = b':';
954                        len += 1;
955                        tmp[len] = b'0' + seconds / 10;
956                        len += 1;
957                        tmp[len] = b'0' + seconds % 10;
958                        len += 1;
959                    }
960                }
961
962                Self::write_bytes(buf, pos, &tmp[..len]);
963            }
964        }
965    }
966
967    #[inline]
968    pub(crate) fn write_timezone_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
969        if let Some(abbrev) = self.tz_abbrev() {
970            Self::write_bytes(buf, pos, abbrev.as_bytes());
971        } else {
972            Self::write_bytes(buf, pos, b"UTC");
973        }
974    }
975
976    #[inline]
977    pub(crate) fn write_iso_date(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
978        Self::write_i64_padded(buf, pos, self.yr, b'0', Some(4), b'0');
979        Self::write_bytes(buf, pos, b"-");
980        Self::write_u32_padded(buf, pos, self.mo as u32, b'0', Some(2), b'0');
981        Self::write_bytes(buf, pos, b"-");
982        Self::write_u32_padded(buf, pos, self.day as u32, b'0', Some(2), b'0');
983    }
984
985    #[inline]
986    pub(crate) fn write_us_date_shortcut(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
987        Self::write_u32_padded(buf, pos, self.mo as u32, b'0', Some(2), b'0');
988        Self::write_bytes(buf, pos, b"/");
989        Self::write_u32_padded(buf, pos, self.day as u32, b'0', Some(2), b'0');
990        Self::write_bytes(buf, pos, b"/");
991        Self::write_u32_padded(
992            buf,
993            pos,
994            (self.yr % 100).saturating_abs() as u32,
995            b'0',
996            Some(2),
997            b'0',
998        );
999    }
1000
1001    #[inline]
1002    pub(crate) fn write_time_with_seconds_shortcut(
1003        &self,
1004        buf: &mut [u8; STRFTIME_SIZE],
1005        pos: &mut usize,
1006    ) {
1007        Self::write_u32_padded(buf, pos, self.hr as u32, b'0', Some(2), b'0');
1008        Self::write_bytes(buf, pos, b":");
1009        Self::write_u32_padded(buf, pos, self.min as u32, b'0', Some(2), b'0');
1010        Self::write_bytes(buf, pos, b":");
1011        Self::write_u32_padded(buf, pos, self.sec as u32, b'0', Some(2), b'0');
1012    }
1013
1014    #[inline]
1015    pub(crate) fn write_time_without_seconds_shortcut(
1016        &self,
1017        buf: &mut [u8; STRFTIME_SIZE],
1018        pos: &mut usize,
1019    ) {
1020        Self::write_u32_padded(buf, pos, self.hr as u32, b'0', Some(2), b'0');
1021        Self::write_bytes(buf, pos, b":");
1022        Self::write_u32_padded(buf, pos, self.min as u32, b'0', Some(2), b'0');
1023    }
1024
1025    #[inline]
1026    pub(crate) fn write_unsupported(&self, _buf: &mut [u8; STRFTIME_SIZE], _pos: &mut usize) {
1027        // no-op (parser already errors)
1028    }
1029}