Skip to main content

deep_time/gregorian_time/
to_str.rs

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