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