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