Skip to main content

deep_time/ymdhms/
to_str.rs

1use crate::{Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, 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);
50    /// let y = x.to_ymdhms_rich(Scale::TAI);
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'n' => self.write_whitespace(buf, pos, b'n'),
225                b't' => self.write_whitespace(buf, pos, b't'),
226                b'P' => self.write_ampm(buf, pos, false),
227                b'p' => self.write_ampm(buf, pos, true),
228                b'S' => self.write_second(buf, pos, flag, width, colons, true),
229                b's' => self.write_unix_timestamp(buf, pos, flag, width, colons),
230                b'U' => self.write_week_number_sunday_based(buf, pos, flag, width, colons),
231                b'u' => self.write_weekday_number_monday_based(buf, pos, flag, width, colons),
232                b'V' => self.write_week_iso(buf, pos, flag, width, colons),
233                b'W' => self.write_week_number_monday_based(buf, pos, flag, width, colons),
234                b'w' => self.write_weekday_number_sunday_based(buf, pos, flag, width, colons),
235                b'Y' => self.write_full_year(buf, pos, flag, width, colons, true),
236                b'y' => self.write_two_digit_year(buf, pos, flag, width, colons, true),
237                b'z' => self.write_timezone_offset(buf, pos, flag, width, colons),
238                b'F' => self.write_iso_date(buf, pos),
239                b'D' => self.write_us_date_shortcut(buf, pos),
240                b'T' => self.write_time_with_seconds_shortcut(buf, pos),
241                b'R' => self.write_time_without_seconds_shortcut(buf, pos),
242                b'Z' => self.write_timezone_abbrev(buf, pos),
243
244                b'Q' => {
245                    /*
246                    we skip writing UTC fallback when:
247                    1. there's no iana
248                    2. %Q directive present
249                    3. offset isn't 0
250                    */
251                    if let Some(iana) = self.tz() {
252                        Self::write_bytes(buf, pos, iana.as_bytes());
253                    } else if let Some(abbrev) = self.tz_abbrev() {
254                        Self::write_bytes(buf, pos, abbrev.as_bytes());
255                    } else if self.offset_sec().unwrap_or_default() == 0 {
256                        Self::write_bytes(buf, pos, "UTC".as_bytes());
257                    } else if i >= fmt.len() {
258                        while *pos > 0 && matches!(buf[*pos - 1], b' ' | b'\t' | b'\n' | b'\r') {
259                            *pos -= 1;
260                        }
261                    }
262                }
263                // b'L' => Self::write_bytes(buf, pos, Scale::TAI.abbrev().as_bytes()),
264                b'*' => self.write_unbounded_year(buf, pos, flag, width, colons),
265
266                b'c' | b'r' | b'X' | b'x' => self.write_unsupported(buf, pos),
267                _ => return Err(an_err!(DtErrKind::UnknownDirective)),
268            }
269        }
270
271        Ok(())
272    }
273
274    fn write_bytes(buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, bytes: &[u8]) {
275        let len = bytes.len();
276        if *pos + len > STRFTIME_SIZE {
277            return;
278        }
279        buf[*pos..*pos + len].copy_from_slice(bytes);
280        *pos += len;
281    }
282
283    fn write_u32_padded(
284        buf: &mut [u8; STRFTIME_SIZE],
285        pos: &mut usize,
286        mut value: u32,
287        flag: u8,
288        width: Option<u8>,
289        default_pad: u8,
290    ) {
291        let w = width.unwrap_or(2) as usize;
292
293        // ── strftime semantics ─────────────────────────────────────
294        // -  = no padding (minimal width)
295        // 0  = zero-pad on the left
296        // _  = space-pad on the left
297        // (no flag) = use the caller's default_pad (usually '0')
298        let pad_char = match flag {
299            b'0' => b'0',
300            b'_' => b' ',
301            _ => default_pad,
302        };
303        let pad_left = flag != b'-';
304
305        let mut digits = [0u8; 20];
306        let mut i = 0usize;
307
308        if value == 0 {
309            digits[0] = b'0';
310            i = 1;
311        } else {
312            while value > 0 {
313                digits[i] = b'0' + (value % 10) as u8;
314                value /= 10;
315                i += 1;
316            }
317        }
318        let num_digits = i;
319        let pad_len = if pad_left && num_digits < w {
320            w - num_digits
321        } else {
322            0
323        };
324
325        if *pos + num_digits + pad_len > STRFTIME_SIZE {
326            return;
327        }
328
329        if pad_left {
330            for _ in 0..pad_len {
331                buf[*pos] = pad_char;
332                *pos += 1;
333            }
334        }
335
336        for j in (0..num_digits).rev() {
337            buf[*pos] = digits[j];
338            *pos += 1;
339        }
340        // No right-padding ever (strftime does not do this for date fields)
341    }
342
343    #[allow(unused_mut)]
344    fn write_i64(mut buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, value: i64) {
345        if value == 0 {
346            Self::write_bytes(buf, pos, b"0");
347            return;
348        }
349
350        let negative = value < 0;
351        let mut v = if negative {
352            value.wrapping_neg()
353        } else {
354            value
355        };
356
357        let mut digits = [0u8; 20];
358        let mut i = 0usize;
359        while v > 0 {
360            digits[i] = b'0' + (v % 10) as u8;
361            v /= 10;
362            i += 1;
363        }
364
365        if negative {
366            if *pos >= STRFTIME_SIZE {
367                return;
368            }
369            buf[*pos] = b'-';
370            *pos += 1;
371        }
372
373        if *pos + i > STRFTIME_SIZE {
374            return;
375        }
376        for j in (0..i).rev() {
377            buf[*pos] = digits[j];
378            *pos += 1;
379        }
380    }
381
382    fn write_i64_padded(
383        buf: &mut [u8; STRFTIME_SIZE],
384        pos: &mut usize,
385        value: i64,
386        flag: u8,
387        width: Option<u8>,
388        default_pad: u8,
389    ) {
390        let w = width.unwrap_or(4) as usize; // %Y and %G default to 4 digits
391
392        let negative = value < 0;
393        let abs_val = if negative {
394            value.wrapping_neg()
395        } else {
396            value
397        };
398
399        let mut digits = [0u8; 20];
400        let mut i = 0usize;
401
402        let mut v = abs_val;
403        if v == 0 {
404            digits[0] = b'0';
405            i = 1;
406        } else {
407            while v > 0 {
408                digits[i] = b'0' + (v % 10) as u8;
409                v /= 10;
410                i += 1;
411            }
412        }
413
414        let num_digits = i;
415        let pad_char = match flag {
416            b'-' => b' ',
417            b'0' => b'0',
418            b'_' => b' ',
419            _ => default_pad,
420        };
421        let pad_left = flag != b'-';
422        let pad_len = if pad_left && num_digits < w {
423            w - num_digits
424        } else {
425            0
426        };
427
428        if *pos + (if negative { 1 } else { 0 }) + num_digits + pad_len > STRFTIME_SIZE {
429            return;
430        }
431
432        if negative {
433            buf[*pos] = b'-';
434            *pos += 1;
435        }
436
437        if pad_left {
438            for _ in 0..pad_len {
439                buf[*pos] = pad_char;
440                *pos += 1;
441            }
442        }
443
444        for j in (0..num_digits).rev() {
445            buf[*pos] = digits[j];
446            *pos += 1;
447        }
448    }
449
450    fn write_fractional(
451        buf: &mut [u8; STRFTIME_SIZE],
452        pos: &mut usize,
453        subsec: u64,
454        width: Option<u8>,
455        trim: bool,
456    ) -> bool {
457        let w = width.unwrap_or(18).min(18) as usize;
458        if w == 0 {
459            return false;
460        }
461
462        let mut n = subsec;
463        let mut digits = [b'0'; 18];
464        for i in (0..18).rev() {
465            digits[i] = b'0' + (n % 10) as u8;
466            n /= 10;
467        }
468
469        let mut end = w;
470        if trim {
471            // Trim trailing zeros from the least-significant end of the selected width.
472            // If everything is zero after trimming, return false so the caller
473            // can suppress the decimal point entirely (perfect for RFC 3339 / ISO 8601).
474            while end > 0 && digits[end - 1] == b'0' {
475                end -= 1;
476            }
477            if end == 0 {
478                return false; // emit nothing at all (no dot, no "0")
479            }
480        }
481        Self::write_bytes(buf, pos, &digits[0..end]);
482        true
483    }
484
485    // ──────────────────────────────────────────────────────────────
486    // Individual write_ functions – one per parser directive
487    // ──────────────────────────────────────────────────────────────
488
489    #[inline]
490    pub(crate) fn write_weekday_full(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
491        let name = WEEKDAYS_FULL[self.wkday as usize];
492        Self::write_bytes(buf, pos, name);
493    }
494
495    #[inline]
496    pub(crate) fn write_weekday_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
497        let name = WEEKDAYS_ABBR[self.wkday as usize];
498        Self::write_bytes(buf, pos, name);
499    }
500
501    #[inline]
502    pub(crate) fn write_month_name_full(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
503        let name = MONTHS_FULL[self.mo as usize - 1];
504        Self::write_bytes(buf, pos, name);
505    }
506
507    #[inline]
508    pub(crate) fn write_month_name_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
509        let name = MONTHS_ABBR[self.mo as usize - 1];
510        Self::write_bytes(buf, pos, name);
511    }
512
513    #[inline]
514    pub(crate) fn write_century(
515        &self,
516        buf: &mut [u8; STRFTIME_SIZE],
517        pos: &mut usize,
518        _flag: u8,
519        _width: Option<u8>,
520        _colons: u8,
521    ) {
522        // Floor division → -123 becomes -2 (exactly matches parse_century)
523        let century = self.yr.div_euclid(100);
524        Self::write_i64(buf, pos, century);
525    }
526
527    #[inline]
528    pub(crate) fn write_day_of_month(
529        &self,
530        buf: &mut [u8; STRFTIME_SIZE],
531        pos: &mut usize,
532        flag: u8,
533        width: Option<u8>,
534        _colons: u8,
535        pad: bool,
536    ) {
537        let default_pad = if pad { b'0' } else { b' ' };
538        Self::write_u32_padded(buf, pos, self.day as u32, flag, width, default_pad);
539    }
540
541    #[inline]
542    pub(crate) fn write_fractional_seconds(
543        &self,
544        buf: &mut [u8; STRFTIME_SIZE],
545        pos: &mut usize,
546        _flag: u8,
547        width: Option<u8>,
548        _colons: u8,
549        trim: bool,
550    ) -> bool {
551        Self::write_fractional(buf, pos, self.attos, width, trim)
552    }
553
554    #[inline]
555    pub(crate) fn write_iso_week_year(
556        &self,
557        buf: &mut [u8; STRFTIME_SIZE],
558        pos: &mut usize,
559        flag: u8,
560        width: Option<u8>,
561        _colons: u8,
562    ) {
563        Self::write_i64_padded(buf, pos, self.iso_yr, flag, width, b'0');
564    }
565
566    #[inline]
567    pub(crate) fn write_two_digit_iso_week_year(
568        &self,
569        buf: &mut [u8; STRFTIME_SIZE],
570        pos: &mut usize,
571        flag: u8,
572        width: Option<u8>,
573        _colons: u8,
574    ) {
575        let yy = (self.iso_yr % 100).saturating_abs() as u32;
576        Self::write_u32_padded(buf, pos, yy, flag, width.or(Some(2)), b'0');
577    }
578
579    #[inline]
580    pub(crate) fn write_hour24(
581        &self,
582        buf: &mut [u8; STRFTIME_SIZE],
583        pos: &mut usize,
584        flag: u8,
585        width: Option<u8>,
586        _colons: u8,
587        pad: bool,
588    ) {
589        let default_pad = if pad { b'0' } else { b' ' };
590        Self::write_u32_padded(buf, pos, self.hr as u32, flag, width, default_pad);
591    }
592
593    #[inline]
594    pub(crate) fn write_hour12(
595        &self,
596        buf: &mut [u8; STRFTIME_SIZE],
597        pos: &mut usize,
598        flag: u8,
599        width: Option<u8>,
600        _colons: u8,
601    ) {
602        let hour24 = self.hr;
603        let hour12 = if hour24 == 0 {
604            12
605        } else if hour24 > 12 {
606            hour24 - 12
607        } else {
608            hour24
609        };
610        Self::write_u32_padded(buf, pos, hour12 as u32, flag, width.or(Some(2)), b'0');
611    }
612
613    #[inline]
614    pub(crate) fn write_day_of_year(
615        &self,
616        buf: &mut [u8; STRFTIME_SIZE],
617        pos: &mut usize,
618        flag: u8,
619        width: Option<u8>,
620        _colons: u8,
621    ) {
622        Self::write_u32_padded(
623            buf,
624            pos,
625            self.day_of_yr as u32,
626            flag,
627            width.or(Some(3)),
628            b'0',
629        );
630    }
631
632    #[inline]
633    pub(crate) fn write_minute(
634        &self,
635        buf: &mut [u8; STRFTIME_SIZE],
636        pos: &mut usize,
637        flag: u8,
638        width: Option<u8>,
639        _colons: u8,
640        pad: bool,
641    ) {
642        let default_pad = if pad { b'0' } else { b' ' };
643        Self::write_u32_padded(buf, pos, self.min as u32, flag, width, default_pad);
644    }
645
646    #[inline]
647    pub(crate) fn write_month_number(
648        &self,
649        buf: &mut [u8; STRFTIME_SIZE],
650        pos: &mut usize,
651        flag: u8,
652        width: Option<u8>,
653        _colons: u8,
654        pad: bool,
655    ) {
656        let default_pad = if pad { b'0' } else { b' ' };
657        Self::write_u32_padded(buf, pos, self.mo as u32, flag, width, default_pad);
658    }
659
660    #[inline]
661    pub(crate) fn write_whitespace(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, ch: u8) {
662        let bytes = if ch == b'n' { b"\n" } else { b"\t" };
663        Self::write_bytes(buf, pos, bytes);
664    }
665
666    #[inline]
667    pub(crate) fn write_ampm(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize, upper: bool) {
668        let hour = self.hr;
669        let bytes = if hour < 12 {
670            if upper { b"AM" } else { b"am" }
671        } else if upper {
672            b"PM"
673        } else {
674            b"pm"
675        };
676        Self::write_bytes(buf, pos, bytes);
677    }
678
679    #[inline]
680    pub(crate) fn write_second(
681        &self,
682        buf: &mut [u8; STRFTIME_SIZE],
683        pos: &mut usize,
684        flag: u8,
685        width: Option<u8>,
686        _colons: u8,
687        pad: bool,
688    ) {
689        let default_pad = if pad { b'0' } else { b' ' };
690        Self::write_u32_padded(buf, pos, self.sec as u32, flag, width, default_pad);
691    }
692
693    #[inline]
694    pub(crate) fn write_unix_timestamp(
695        &self,
696        buf: &mut [u8; STRFTIME_SIZE],
697        pos: &mut usize,
698        _flag: u8,
699        _width: Option<u8>,
700        _colons: u8,
701    ) {
702        let (seconds, _) = self.unix_timestamp();
703        Self::write_i64(buf, pos, seconds);
704    }
705
706    #[inline]
707    pub(crate) fn write_weekday_number_sunday_based(
708        &self,
709        buf: &mut [u8; STRFTIME_SIZE],
710        pos: &mut usize,
711        flag: u8,
712        width: Option<u8>,
713        _colons: u8,
714    ) {
715        Self::write_u32_padded(
716            buf,
717            pos,
718            self.wkday_sun() as u32,
719            flag,
720            width.or(Some(1)),
721            b'0',
722        );
723    }
724
725    #[inline]
726    pub(crate) fn write_weekday_number_monday_based(
727        &self,
728        buf: &mut [u8; STRFTIME_SIZE],
729        pos: &mut usize,
730        flag: u8,
731        width: Option<u8>,
732        _colons: u8,
733    ) {
734        Self::write_u32_padded(
735            buf,
736            pos,
737            self.wkday_mon() as u32,
738            flag,
739            width.or(Some(1)),
740            b'0',
741        );
742    }
743
744    #[inline]
745    pub(crate) fn write_week_iso(
746        &self,
747        buf: &mut [u8; STRFTIME_SIZE],
748        pos: &mut usize,
749        flag: u8,
750        width: Option<u8>,
751        _colons: u8,
752    ) {
753        Self::write_u32_padded(buf, pos, self.iso_wk as u32, flag, width.or(Some(2)), b'0');
754    }
755
756    #[inline]
757    pub(crate) fn write_week_number_sunday_based(
758        &self,
759        buf: &mut [u8; STRFTIME_SIZE],
760        pos: &mut usize,
761        flag: u8,
762        width: Option<u8>,
763        _colons: u8,
764    ) {
765        Self::write_u32_padded(
766            buf,
767            pos,
768            self.wk_of_yr_sun as u32,
769            flag,
770            width.or(Some(2)),
771            b'0',
772        );
773    }
774
775    #[inline]
776    pub(crate) fn write_week_number_monday_based(
777        &self,
778        buf: &mut [u8; STRFTIME_SIZE],
779        pos: &mut usize,
780        flag: u8,
781        width: Option<u8>,
782        _colons: u8,
783    ) {
784        Self::write_u32_padded(
785            buf,
786            pos,
787            self.wk_of_yr_mon as u32,
788            flag,
789            width.or(Some(2)),
790            b'0',
791        );
792    }
793
794    #[inline]
795    pub(crate) fn write_full_year(
796        &self,
797        buf: &mut [u8; STRFTIME_SIZE],
798        pos: &mut usize,
799        flag: u8,
800        width: Option<u8>,
801        _colons: u8,
802        _pad: bool,
803    ) {
804        Self::write_i64_padded(buf, pos, self.yr, flag, width, b'0');
805    }
806
807    #[inline]
808    pub(crate) fn write_two_digit_year(
809        &self,
810        buf: &mut [u8; STRFTIME_SIZE],
811        pos: &mut usize,
812        flag: u8,
813        width: Option<u8>,
814        _colons: u8,
815        _pad: bool,
816    ) {
817        let yy = (self.yr % 100).saturating_abs() as u32;
818        Self::write_u32_padded(buf, pos, yy, flag, width.or(Some(2)), b'0');
819    }
820
821    #[inline]
822    pub(crate) fn write_unbounded_year(
823        &self,
824        buf: &mut [u8; STRFTIME_SIZE],
825        pos: &mut usize,
826        flag: u8,
827        width: Option<u8>,
828        _colons: u8,
829    ) {
830        Self::write_i64_padded(buf, pos, self.yr, flag, width, b'0');
831    }
832
833    pub(crate) fn write_timezone_offset(
834        &self,
835        buf: &mut [u8; STRFTIME_SIZE],
836        pos: &mut usize,
837        _flag: u8,
838        _width: Option<u8>,
839        colons: u8,
840    ) {
841        let Some(offset_sec) = self.offset_sec() else {
842            return;
843        };
844        let (negative, hours, minutes) = Dt::sec_as_hhmm(offset_sec);
845        let sign = if negative { b'-' } else { b'+' };
846
847        // seconds component — only used by %::z
848        let seconds = ((offset_sec.saturating_abs() % 3600) % 60) as u8;
849
850        match colons {
851            0 => {
852                // %z     → +HHMM
853                let mut tmp = [0u8; 5];
854                tmp[0] = sign;
855                tmp[1] = b'0' + hours / 10;
856                tmp[2] = b'0' + hours % 10;
857                tmp[3] = b'0' + minutes / 10;
858                tmp[4] = b'0' + minutes % 10;
859                Self::write_bytes(buf, pos, &tmp);
860            }
861            1 => {
862                // %:z    → +HH:MM
863                let mut tmp = [0u8; 6];
864                tmp[0] = sign;
865                tmp[1] = b'0' + hours / 10;
866                tmp[2] = b'0' + hours % 10;
867                tmp[3] = b':';
868                tmp[4] = b'0' + minutes / 10;
869                tmp[5] = b'0' + minutes % 10;
870                Self::write_bytes(buf, pos, &tmp);
871            }
872            2 => {
873                // %::z   → +HH:MM:SS
874                let mut tmp = [0u8; 9];
875                tmp[0] = sign;
876                tmp[1] = b'0' + hours / 10;
877                tmp[2] = b'0' + hours % 10;
878                tmp[3] = b':';
879                tmp[4] = b'0' + minutes / 10;
880                tmp[5] = b'0' + minutes % 10;
881                tmp[6] = b':';
882                tmp[7] = b'0' + seconds / 10;
883                tmp[8] = b'0' + seconds % 10;
884                Self::write_bytes(buf, pos, &tmp);
885            }
886            _ => Self::write_bytes(buf, pos, b"+0000"),
887        }
888    }
889
890    #[inline]
891    pub(crate) fn write_timezone_abbrev(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
892        if let Some(abbrev) = self.tz_abbrev() {
893            Self::write_bytes(buf, pos, abbrev.as_bytes());
894        } else {
895            Self::write_bytes(buf, pos, b"UTC");
896        }
897    }
898
899    #[inline]
900    pub(crate) fn write_iso_date(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
901        Self::write_i64_padded(buf, pos, self.yr, b'0', Some(4), b'0');
902        Self::write_bytes(buf, pos, b"-");
903        Self::write_u32_padded(buf, pos, self.mo as u32, b'0', Some(2), b'0');
904        Self::write_bytes(buf, pos, b"-");
905        Self::write_u32_padded(buf, pos, self.day as u32, b'0', Some(2), b'0');
906    }
907
908    #[inline]
909    pub(crate) fn write_us_date_shortcut(&self, buf: &mut [u8; STRFTIME_SIZE], pos: &mut usize) {
910        Self::write_u32_padded(buf, pos, self.mo as u32, b'0', Some(2), b'0');
911        Self::write_bytes(buf, pos, b"/");
912        Self::write_u32_padded(buf, pos, self.day as u32, b'0', Some(2), b'0');
913        Self::write_bytes(buf, pos, b"/");
914        Self::write_u32_padded(
915            buf,
916            pos,
917            (self.yr % 100).saturating_abs() as u32,
918            b'0',
919            Some(2),
920            b'0',
921        );
922    }
923
924    #[inline]
925    pub(crate) fn write_time_with_seconds_shortcut(
926        &self,
927        buf: &mut [u8; STRFTIME_SIZE],
928        pos: &mut usize,
929    ) {
930        Self::write_u32_padded(buf, pos, self.hr as u32, b'0', Some(2), b'0');
931        Self::write_bytes(buf, pos, b":");
932        Self::write_u32_padded(buf, pos, self.min as u32, b'0', Some(2), b'0');
933        Self::write_bytes(buf, pos, b":");
934        Self::write_u32_padded(buf, pos, self.sec as u32, b'0', Some(2), b'0');
935    }
936
937    #[inline]
938    pub(crate) fn write_time_without_seconds_shortcut(
939        &self,
940        buf: &mut [u8; STRFTIME_SIZE],
941        pos: &mut usize,
942    ) {
943        Self::write_u32_padded(buf, pos, self.hr as u32, b'0', Some(2), b'0');
944        Self::write_bytes(buf, pos, b":");
945        Self::write_u32_padded(buf, pos, self.min as u32, b'0', Some(2), b'0');
946    }
947
948    #[inline]
949    pub(crate) fn write_unsupported(&self, _buf: &mut [u8; STRFTIME_SIZE], _pos: &mut usize) {
950        // no-op (parser already errors)
951    }
952}