1use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use super::{MONTH_ABBR, MONTH_FULL, civil_from_days};
17
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum DateOrder {
28 Mdy,
29 Dmy,
30 Ymd,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq, Debug)]
34pub enum DateStyleKind {
35 Iso,
36 German,
37 Sql,
38 Postgres,
39}
40
41#[derive(Clone, Copy, PartialEq, Eq, Debug)]
42pub enum IntervalStyleKind {
43 Postgres,
44 SqlStandard,
45 Iso8601,
46 PostgresVerbose,
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub struct RenderStyle {
51 pub date_style: DateStyleKind,
52 pub date_order: DateOrder,
53 pub interval_style: IntervalStyleKind,
54 pub extra_float_digits: i32,
57 pub bytea_escape: bool,
63 pub mysql: bool,
68}
69
70impl Default for RenderStyle {
71 fn default() -> Self {
72 Self {
73 date_style: DateStyleKind::Iso,
74 date_order: DateOrder::Mdy,
75 interval_style: IntervalStyleKind::Postgres,
76 extra_float_digits: 1,
77 bytea_escape: false,
78 mysql: false,
79 }
80 }
81}
82
83fn dow_abbr(days: i32) -> &'static str {
85 const DOW: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
86 DOW[((days.rem_euclid(7)) as usize + 3) % 7]
87}
88
89fn hms_from_day_micros(day_micros: i64) -> String {
92 let secs = day_micros / 1_000_000;
93 let frac = day_micros % 1_000_000;
94 let hh = secs / 3600;
95 let mm = (secs / 60) % 60;
96 let ss = secs % 60;
97 if frac == 0 {
98 format!("{hh:02}:{mm:02}:{ss:02}")
99 } else {
100 let raw = format!("{frac:06}");
101 let trimmed = raw.trim_end_matches('0');
102 format!("{hh:02}:{mm:02}:{ss:02}.{trimmed}")
103 }
104}
105
106pub fn format_date_styled(days: i32, style: &RenderStyle) -> String {
112 if days == i32::MAX {
113 return "infinity".into();
114 }
115 if days == i32::MIN {
116 return "-infinity".into();
117 }
118 let (y, m, d) = civil_from_days(days);
119 let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
120 let dmy = style.date_order == DateOrder::Dmy;
121 match style.date_style {
122 DateStyleKind::Iso => format!("{y:04}-{m:02}-{d:02}{bc}"),
123 DateStyleKind::German => format!("{d:02}.{m:02}.{y:04}{bc}"),
124 DateStyleKind::Sql => {
125 if dmy {
126 format!("{d:02}/{m:02}/{y:04}{bc}")
127 } else {
128 format!("{m:02}/{d:02}/{y:04}{bc}")
129 }
130 }
131 DateStyleKind::Postgres => {
132 if dmy {
133 format!("{d:02}-{m:02}-{y:04}{bc}")
134 } else {
135 format!("{m:02}-{d:02}-{y:04}{bc}")
136 }
137 }
138 }
139}
140
141pub fn format_timestamp_styled(micros: i64, style: &RenderStyle) -> String {
145 if micros == i64::MAX {
146 return "infinity".into();
147 }
148 if micros == i64::MIN {
149 return "-infinity".into();
150 }
151 if style.date_style == DateStyleKind::Iso {
152 return format_timestamp(micros);
153 }
154 const MICROS_PER_DAY: i64 = 86_400_000_000;
155 let days = micros.div_euclid(MICROS_PER_DAY);
156 let day_micros = micros.rem_euclid(MICROS_PER_DAY);
157 let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
158 let hms = hms_from_day_micros(day_micros);
159 match style.date_style {
160 DateStyleKind::Iso => unreachable!("handled above"),
161 DateStyleKind::German | DateStyleKind::Sql => {
162 let d = format_date_styled(day_i32, style);
165 match d.strip_suffix(" BC") {
166 Some(base) => format!("{base} {hms} BC"),
167 None => format!("{d} {hms}"),
168 }
169 }
170 DateStyleKind::Postgres => {
171 let (y, m, d) = civil_from_days(day_i32);
172 let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
173 let mon = MONTH_ABBR[(m as usize).saturating_sub(1).min(11)];
174 let dow = dow_abbr(day_i32);
175 if style.date_order == DateOrder::Dmy {
176 format!("{dow} {d} {mon} {hms} {y:04}{bc}")
177 } else {
178 format!("{dow} {mon} {d} {hms} {y:04}{bc}")
179 }
180 }
181 }
182}
183
184pub fn format_timestamptz_styled(micros: i64, style: &RenderStyle) -> String {
188 format_timestamptz_tz(micros, style, 0, None)
189}
190
191pub fn format_timestamptz_tz(
197 micros: i64,
198 style: &RenderStyle,
199 offset_micros: i64,
200 abbr: Option<&str>,
201) -> String {
202 if style.date_style == DateStyleKind::Iso {
203 return format_timestamptz_at(micros, offset_micros);
204 }
205 if micros == i64::MAX || micros == i64::MIN {
206 return format_timestamp(micros);
207 }
208 let body = format_timestamp_styled(micros + offset_micros, style);
209 match abbr {
210 Some(a) => format!("{body} {a}"),
211 None if offset_micros == 0 => format!("{body} UTC"),
212 None => {
213 let total_min = (offset_micros / 60_000_000).abs();
214 let (h, m) = (total_min / 60, total_min % 60);
215 let sign = if offset_micros < 0 { '-' } else { '+' };
216 if m == 0 {
217 format!("{body} {sign}{h:02}")
218 } else {
219 format!("{body} {sign}{h:02}:{m:02}")
220 }
221 }
222 }
223}
224
225fn sql_std_time(abs_us: i64) -> String {
228 let secs = abs_us / 1_000_000;
229 let frac = abs_us % 1_000_000;
230 let h = secs / 3600;
231 let mm = (secs / 60) % 60;
232 let ss = secs % 60;
233 if frac == 0 {
234 format!("{h}:{mm:02}:{ss:02}")
235 } else {
236 let raw = format!("{frac:06}");
237 let trimmed = raw.trim_end_matches('0');
238 format!("{h}:{mm:02}:{ss:02}.{trimmed}")
239 }
240}
241
242fn secs_body(abs_us: i64) -> String {
244 let ss = abs_us / 1_000_000;
245 let frac = abs_us % 1_000_000;
246 if frac == 0 {
247 format!("{ss}")
248 } else {
249 let raw = format!("{frac:06}");
250 let trimmed = raw.trim_end_matches('0');
251 format!("{ss}.{trimmed}")
252 }
253}
254
255pub fn format_interval_styled(months: i32, days: i32, micros: i64, style: &RenderStyle) -> String {
269 match style.interval_style {
270 IntervalStyleKind::Postgres => format_interval(months, days, micros),
271 IntervalStyleKind::SqlStandard => {
272 let has_ym = months != 0;
273 let has_dt = days != 0 || micros != 0;
274 if !has_ym && !has_dt {
275 return "0".into();
276 }
277 let y = months / 12;
278 let mo = (months % 12).abs();
279 let signs: Vec<i8> = [i64::from(months), i64::from(days), micros]
283 .iter()
284 .filter(|v| **v != 0)
285 .map(|v| if *v < 0 { -1i8 } else { 1 })
286 .collect();
287 let coherent = signs.windows(2).all(|w| w[0] == w[1]);
288 if has_ym && !has_dt && coherent {
289 return format!("{y}-{mo}");
290 }
291 if !has_ym && coherent {
292 let neg = days < 0 || micros < 0;
293 let time = sql_std_time(micros.abs());
294 if days == 0 {
295 return format!("{}{time}", if neg { "-" } else { "" });
296 }
297 return format!("{days} {time}");
298 }
299 let sgn = |neg: bool| if neg { '-' } else { '+' };
302 format!(
303 "{}{}-{} {}{} {}{}",
304 sgn(months < 0),
305 y.abs(),
306 mo,
307 sgn(days < 0),
308 days.abs(),
309 sgn(micros < 0),
310 sql_std_time(micros.abs())
311 )
312 }
313 IntervalStyleKind::Iso8601 => {
314 if months == 0 && days == 0 && micros == 0 {
315 return "PT0S".into();
316 }
317 let y = months / 12;
318 let mo = months % 12;
319 let mut out = String::from("P");
320 if y != 0 {
321 out.push_str(&format!("{y}Y"));
322 }
323 if mo != 0 {
324 out.push_str(&format!("{mo}M"));
325 }
326 if days != 0 {
327 out.push_str(&format!("{days}D"));
328 }
329 if micros != 0 {
330 out.push('T');
331 let neg = micros < 0;
332 let abs = micros.abs();
333 let h = abs / 3_600_000_000;
334 let m = (abs / 60_000_000) % 60;
335 let s_us = abs % 60_000_000;
336 let sgn = if neg { "-" } else { "" };
337 if h != 0 {
338 out.push_str(&format!("{sgn}{h}H"));
339 }
340 if m != 0 {
341 out.push_str(&format!("{sgn}{m}M"));
342 }
343 if s_us != 0 {
344 out.push_str(&format!("{sgn}{}S", secs_body(s_us)));
345 }
346 }
347 out
348 }
349 IntervalStyleKind::PostgresVerbose => {
350 if months == 0 && days == 0 && micros == 0 {
351 return "@ 0".into();
352 }
353 let total = i128::from(months) * 30 * 86_400_000_000
357 + i128::from(days) * 86_400_000_000
358 + i128::from(micros);
359 let ago = total < 0;
360 let (months, days, micros) = if ago {
361 (-months, -days, -micros)
362 } else {
363 (months, days, micros)
364 };
365 let y = months / 12;
366 let mo = months % 12;
367 let neg_t = micros < 0;
368 let abs = micros.abs();
369 let h = abs / 3_600_000_000;
370 let m = (abs / 60_000_000) % 60;
371 let s_us = abs % 60_000_000;
372 let mut parts: Vec<String> = Vec::new();
373 let unit = |n: i64, singular: &'static str| -> String {
374 if n == 1 {
375 singular.into()
376 } else {
377 format!("{singular}s")
378 }
379 };
380 if y != 0 {
381 parts.push(format!("{y} {}", unit(i64::from(y), "year")));
382 }
383 if mo != 0 {
384 parts.push(format!("{mo} {}", unit(i64::from(mo), "mon")));
385 }
386 if days != 0 {
387 parts.push(format!("{days} {}", unit(i64::from(days), "day")));
388 }
389 let tsgn = if neg_t { "-" } else { "" };
390 if h != 0 {
391 parts.push(format!("{tsgn}{h} {}", unit(h, "hour")));
392 }
393 if m != 0 {
394 parts.push(format!("{tsgn}{m} {}", unit(m, "min")));
395 }
396 if s_us != 0 {
397 let body = secs_body(s_us);
398 let plural = body != "1";
399 parts.push(format!(
400 "{tsgn}{body} {}",
401 if plural { "secs" } else { "sec" }
402 ));
403 }
404 let mut out = String::from("@ ");
405 out.push_str(&parts.join(" "));
406 if ago {
407 out.push_str(" ago");
408 }
409 out
410 }
411 }
412}
413
414pub fn format_date_array_styled(items: &[Option<i32>], style: &RenderStyle) -> String {
416 array_styled(items, |d| format_date_styled(*d, style))
417}
418
419pub fn format_timestamp_array_styled(
420 items: &[Option<i64>],
421 with_tz: bool,
422 style: &RenderStyle,
423) -> String {
424 if with_tz {
425 array_styled(items, |t| format_timestamptz_styled(*t, style))
426 } else {
427 array_styled(items, |t| format_timestamp_styled(*t, style))
428 }
429}
430
431pub fn format_interval_array_styled(
432 items: &[Option<spg_storage::IntervalSpan>],
433 style: &RenderStyle,
434) -> String {
435 array_styled(items, |iv| {
436 format_interval_styled(iv.months, iv.days, iv.micros, style)
437 })
438}
439
440pub fn format_float_array_styled(items: &[Option<f64>], style: &RenderStyle) -> String {
441 array_styled(items, |f| format_float_styled(*f, style))
442}
443
444fn array_styled<T>(items: &[Option<T>], mut f: impl FnMut(&T) -> String) -> String {
445 let mut out = String::with_capacity(2 + items.len() * 12);
446 out.push('{');
447 for (i, item) in items.iter().enumerate() {
448 if i > 0 {
449 out.push(',');
450 }
451 match item {
452 None => out.push_str("NULL"),
453 Some(v) => push_array_element(&mut out, &f(v)),
454 }
455 }
456 out.push('}');
457 out
458}
459
460fn push_array_element(out: &mut String, s: &str) {
468 let needs_quote = s.is_empty()
469 || s.eq_ignore_ascii_case("null")
470 || s.chars()
471 .any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\') || c.is_whitespace());
472 if !needs_quote {
473 out.push_str(s);
474 return;
475 }
476 out.push('"');
477 for c in s.chars() {
478 if c == '"' || c == '\\' {
479 out.push('\\');
480 }
481 out.push(c);
482 }
483 out.push('"');
484}
485
486fn format_g(x: f64, prec: usize) -> String {
491 let prec = prec.max(1);
492 let sci = format!("{:.*e}", prec - 1, x);
495 let epos = sci.find('e').expect("{:e} always has an 'e'");
496 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
497 let mant = &sci[..epos];
498 if exp_val >= -4 && (exp_val as i64) < prec as i64 {
499 let decimals =
501 usize::try_from(i64::try_from(prec).unwrap_or(1) - 1 - i64::from(exp_val)).unwrap_or(0);
502 let rounded: f64 = sci.parse().unwrap_or(x);
503 let fixed = format!("{rounded:.decimals$}");
504 if fixed.contains('.') {
505 let t = fixed.trim_end_matches('0').trim_end_matches('.');
506 t.into()
507 } else {
508 fixed
509 }
510 } else {
511 let mant = if mant.contains('.') {
512 mant.trim_end_matches('0').trim_end_matches('.')
513 } else {
514 mant
515 };
516 let (sign, digits) = if exp_val < 0 {
517 ('-', format!("{}", -exp_val))
518 } else {
519 ('+', format!("{exp_val}"))
520 };
521 format!("{mant}e{sign}{digits:0>2}")
522 }
523}
524
525pub fn format_float_styled(x: f64, style: &RenderStyle) -> String {
528 if style.extra_float_digits >= 1 {
529 return format_float(x);
530 }
531 if x.is_nan() {
532 return "NaN".into();
533 }
534 if x.is_infinite() {
535 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
536 }
537 if x == 0.0 {
538 return if x.is_sign_negative() { "-0" } else { "0" }.into();
539 }
540 let prec = (15 + style.extra_float_digits).clamp(1, 17) as usize;
541 format_g(x, prec)
542}
543
544pub fn format_real_styled(x: f32, style: &RenderStyle) -> String {
547 if style.extra_float_digits >= 1 {
548 return format_real(x);
549 }
550 if x.is_nan() {
551 return "NaN".into();
552 }
553 if x.is_infinite() {
554 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
555 }
556 if x == 0.0 {
557 return if x.is_sign_negative() { "-0" } else { "0" }.into();
558 }
559 let prec = (6 + style.extra_float_digits).clamp(1, 9) as usize;
560 format_g(f64::from(x), prec)
561}
562
563pub fn format_date(days: i32) -> String {
566 if days == i32::MAX {
567 return "infinity".into();
568 }
569 if days == i32::MIN {
570 return "-infinity".into();
571 }
572 let (y, m, d) = civil_from_days(days);
573 if y <= 0 {
576 return format!("{:04}-{m:02}-{d:02} BC", 1 - y);
577 }
578 format!("{y:04}-{m:02}-{d:02}")
579}
580
581pub fn format_timestamptz(micros: i64) -> String {
592 format_timestamptz_at(micros, 0)
593}
594
595pub fn format_timestamptz_at(micros: i64, offset_micros: i64) -> String {
600 if micros == i64::MAX || micros == i64::MIN {
601 return format_timestamp(micros);
602 }
603 let base = format_timestamp(micros + offset_micros);
604 let (base, bc) = match base.strip_suffix(" BC") {
607 Some(b) => (String::from(b), " BC"),
608 None => (base, ""),
609 };
610 let mut s = String::with_capacity(base.len() + 9);
611 s.push_str(&base);
612 let total_min = (offset_micros / 60_000_000).abs();
613 let (h, m) = (total_min / 60, total_min % 60);
614 s.push(if offset_micros < 0 { '-' } else { '+' });
615 s.push_str(&alloc::format!("{h:02}"));
616 if m != 0 {
617 s.push(':');
618 s.push_str(&alloc::format!("{m:02}"));
619 }
620 s.push_str(bc);
621 s
622}
623
624pub fn format_float(x: f64) -> String {
634 if x.is_nan() {
635 return "NaN".into();
636 }
637 if x.is_infinite() {
638 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
639 }
640 if x == 0.0 {
641 return if x.is_sign_negative() { "-0" } else { "0" }.into();
642 }
643 let sci = shortest_float_sci(x); let epos = sci.find('e').expect("{:e} always has an 'e'");
645 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
646 if (-4..=14).contains(&exp_val) {
647 return fixed_from_sci(&sci, exp_val);
650 }
651 let mant = &sci[..epos];
652 let exp = &sci[epos + 1..];
653 let (sign, digits) = match exp.strip_prefix('-') {
654 Some(d) => ('-', d),
655 None => ('+', exp),
656 };
657 alloc::format!("{mant}e{sign}{digits:0>2}")
658}
659
660fn shortest_real_sci(x: f32) -> String {
677 let wide = f64::from(x);
678 let below = f64::from(next_f32(x, false));
679 let above = f64::from(next_f32(x, true));
680 let lo = (wide + below) / 2.0;
682 let hi = (wide + above) / 2.0;
683 for p in 1..=9u32 {
684 let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
685 let Ok(v) = cand.parse::<f64>() else { continue };
686 #[allow(clippy::cast_possible_truncation)]
688 if v as f32 == x && v != lo && v != hi {
689 return cand;
690 }
691 }
692 alloc::format!("{x:e}")
693}
694
695fn next_f32(x: f32, up: bool) -> f32 {
698 let bits = x.to_bits();
699 let stepped = if (x > 0.0) == up { bits + 1 } else { bits - 1 };
700 f32::from_bits(stepped)
701}
702
703fn fixed_from_sci(sci: &str, exp: i32) -> String {
705 let epos = sci.find('e').expect("{:e} always has an 'e'");
706 let (mant, _) = sci.split_at(epos);
707 let (sign, mant) = match mant.strip_prefix('-') {
708 Some(m) => ("-", m),
709 None => ("", mant),
710 };
711 let digits: String = mant.chars().filter(char::is_ascii_digit).collect();
712 let point = exp + 1; let mut out = String::from(sign);
714 if point <= 0 {
715 out.push_str("0.");
716 for _ in 0..-point {
717 out.push('0');
718 }
719 out.push_str(&digits);
720 } else if (point as usize) >= digits.len() {
721 out.push_str(&digits);
722 for _ in 0..(point as usize - digits.len()) {
723 out.push('0');
724 }
725 } else {
726 out.push_str(&digits[..point as usize]);
727 out.push('.');
728 out.push_str(&digits[point as usize..]);
729 }
730 out
731}
732
733pub fn format_real(x: f32) -> String {
738 if x.is_nan() {
739 return "NaN".into();
740 }
741 if x.is_infinite() {
742 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
743 }
744 if x == 0.0 {
745 return if x.is_sign_negative() { "-0" } else { "0" }.into();
746 }
747 let sci = shortest_real_sci(x);
748 let epos = sci.find('e').expect("{:e} always has an 'e'");
749 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
750 if (-4..=5).contains(&exp_val) {
751 return fixed_from_sci(&sci, exp_val);
755 }
756 let mant = &sci[..epos];
757 let exp = &sci[epos + 1..];
758 let (sign, digits) = match exp.strip_prefix('-') {
759 Some(d) => ('-', d),
760 None => ('+', exp),
761 };
762 alloc::format!("{mant}e{sign}{digits:0>2}")
763}
764
765pub fn format_money(cents: i64) -> String {
768 let neg = cents < 0;
769 let abs = cents.unsigned_abs();
770 let dollars = abs / 100;
771 let cc = abs % 100;
772 let dollar_str = dollars.to_string();
774 let bytes = dollar_str.as_bytes();
775 let mut int_part = String::with_capacity(dollar_str.len() + dollar_str.len() / 3);
776 for (i, b) in bytes.iter().enumerate() {
777 let from_right = bytes.len() - i;
780 if i > 0 && from_right % 3 == 0 {
781 int_part.push(',');
782 }
783 int_part.push(*b as char);
784 }
785 let sign = if neg { "-" } else { "" };
786 format!("{sign}${int_part}.{cc:02}")
787}
788
789pub fn format_timetz(us: i64, offset_secs: i32) -> String {
794 let time = format_time(us);
795 let sign = if offset_secs < 0 { '-' } else { '+' };
796 let abs = offset_secs.unsigned_abs();
797 let oh = abs / 3600;
798 let om = (abs % 3600) / 60;
799 if om == 0 {
800 format!("{time}{sign}{oh:02}")
801 } else {
802 format!("{time}{sign}{oh:02}:{om:02}")
803 }
804}
805
806pub fn format_time(us: i64) -> String {
811 let total_secs = us.div_euclid(1_000_000);
812 let frac = us.rem_euclid(1_000_000);
813 let hh = total_secs / 3600;
814 let mm = (total_secs / 60) % 60;
815 let ss = total_secs % 60;
816 if frac == 0 {
817 format!("{hh:02}:{mm:02}:{ss:02}")
818 } else {
819 let raw = format!("{frac:06}");
820 let trimmed = raw.trim_end_matches('0');
821 format!("{hh:02}:{mm:02}:{ss:02}.{trimmed}")
822 }
823}
824
825pub fn format_timestamp(micros: i64) -> String {
826 if micros == i64::MAX {
828 return "infinity".into();
829 }
830 if micros == i64::MIN {
831 return "-infinity".into();
832 }
833 const MICROS_PER_DAY: i64 = 86_400_000_000;
834 let days = micros.div_euclid(MICROS_PER_DAY);
837 let day_micros = micros.rem_euclid(MICROS_PER_DAY);
838 let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
839 let (y, m, d) = civil_from_days(day_i32);
840 let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
842 let secs = day_micros / 1_000_000;
843 let frac = day_micros % 1_000_000;
844 let hh = secs / 3600;
845 let mm = (secs / 60) % 60;
846 let ss = secs % 60;
847 if frac == 0 {
848 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}{bc}")
849 } else {
850 let raw = format!("{frac:06}");
852 let trimmed = raw.trim_end_matches('0');
853 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}.{trimmed}{bc}")
854 }
855}
856
857#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
860pub fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
861 let y_adj = if m <= 2 {
862 i64::from(y) - 1
863 } else {
864 i64::from(y)
865 };
866 let era = y_adj.div_euclid(400);
867 let yoe = (y_adj - era * 400) as u32;
868 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.saturating_sub(1);
869 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
870 let total = era * 146_097 + i64::from(doe) - 719_468;
871 i32::try_from(total).unwrap_or(i32::MAX)
872}
873
874pub fn parse_date_literal(s: &str) -> Option<i32> {
878 parse_date_literal_ordered(s, DateOrder::Mdy)
879}
880
881pub fn parse_date_literal_ordered(s: &str, order: DateOrder) -> Option<i32> {
891 let s = s.trim();
892 if let Some(base) = s
896 .strip_suffix(" BC")
897 .or_else(|| s.strip_suffix(" bc"))
898 .or_else(|| s.strip_suffix(" Bc"))
899 {
900 let days = parse_date_literal_ordered(base, order)?;
901 let (y, m, d) = civil_from_days(days);
902 if y < 1 {
903 return None;
904 }
905 return Some(days_from_civil(1 - y, m, d));
906 }
907 if let Some(base) = s.strip_suffix(" AD").or_else(|| s.strip_suffix(" ad")) {
908 return parse_date_literal_ordered(base, order);
909 }
910 if s.eq_ignore_ascii_case("epoch") {
912 return Some(days_from_civil(1970, 1, 1));
913 }
914 if s.eq_ignore_ascii_case("infinity") || s.eq_ignore_ascii_case("+infinity") {
915 return Some(i32::MAX);
916 }
917 if s.eq_ignore_ascii_case("-infinity") {
918 return Some(i32::MIN);
919 }
920 let bytes = s.as_bytes();
921 if bytes.len() == 8 && bytes.iter().all(u8::is_ascii_digit) {
923 let y: i32 = s[0..4].parse().ok()?;
924 let m: u32 = s[4..6].parse().ok()?;
925 let d: u32 = s[6..8].parse().ok()?;
926 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
927 return None;
928 }
929 return Some(days_from_civil(y, m, d));
930 }
931 if s.bytes().any(|b| b.is_ascii_alphabetic()) {
936 if let Some(jd) = s.strip_prefix(['J', 'j'])
939 && !jd.is_empty()
940 && jd.bytes().all(|b| b.is_ascii_digit())
941 {
942 let jd: i64 = jd.parse().ok()?;
943 return i32::try_from(jd - 2_440_588).ok();
944 }
945 return parse_month_name_date(s, order);
946 }
947 {
954 let mut two = s.splitn(2, ['-', '/', '.']);
955 if let (Some(ya), Some(dd)) = (two.next(), two.next())
956 && ya.len() >= 3
957 && dd.len() == 3
958 && !dd.contains(['-', '/', '.', ' '])
959 && ya.bytes().all(|b| b.is_ascii_digit())
960 && dd.bytes().all(|b| b.is_ascii_digit())
961 {
962 let y: i32 = ya.parse().ok()?;
963 let doy: i64 = dd.parse().ok()?;
964 if y != 0 && (1..=366).contains(&doy) {
965 let jan1 = days_from_civil(y, 1, 1);
966 let days = jan1 + i32::try_from(doy).ok()? - 1;
967 let (yy, _, _) = civil_from_days(days);
968 if yy == y {
969 return Some(days);
970 }
971 return None; }
973 }
974 }
975 let mut parts = s.splitn(3, ['-', '/', '.']);
976 let (fa, fb, fc) = (parts.next()?, parts.next()?, parts.next()?);
977 if fc.contains(['-', '/', '.', ' ']) {
978 return None; }
980 if [fa, fb, fc]
981 .iter()
982 .any(|p| p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()))
983 {
984 return None;
985 }
986 if fa.len() >= 3 && fb.len() <= 2 && fc.len() <= 2 {
990 let y: i32 = fa.parse().ok()?;
991 if y == 0 {
993 return None;
994 }
995 let m: u32 = fb.parse().ok()?;
999 let d: u32 = fc.parse().ok()?;
1000 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1004 return None;
1005 }
1006 return Some(days_from_civil(y, m, d));
1007 }
1008 let expand_year = |t: &str| -> Option<i32> {
1010 match t.len() {
1011 4 => t.parse().ok(),
1012 1 | 2 => {
1014 let n: i32 = t.parse().ok()?;
1015 Some(if n < 70 { 2000 + n } else { 1900 + n })
1016 }
1017 _ => None,
1018 }
1019 };
1020 let (ys, ms, ds) = match order {
1021 DateOrder::Mdy => (fc, fa, fb),
1022 DateOrder::Dmy => (fc, fb, fa),
1023 DateOrder::Ymd => (fa, fb, fc),
1024 };
1025 if ms.len() > 2 || ds.len() > 2 {
1026 return None;
1027 }
1028 let y = expand_year(ys)?;
1029 let m: u32 = ms.parse().ok()?;
1030 let d: u32 = ds.parse().ok()?;
1031 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1032 return None;
1033 }
1034 Some(days_from_civil(y, m, d))
1035}
1036
1037fn parse_month_name_date(s: &str, order: DateOrder) -> Option<i32> {
1044 let tokens: alloc::vec::Vec<&str> =
1045 s.split([' ', ',', '-']).filter(|t| !t.is_empty()).collect();
1046 if tokens.len() != 3 {
1047 return None;
1048 }
1049 let month_of = |t: &str| -> Option<u32> {
1050 let up = t.to_ascii_uppercase();
1051 MONTH_ABBR
1052 .iter()
1053 .position(|a| a.eq_ignore_ascii_case(&up))
1054 .or_else(|| MONTH_FULL.iter().position(|f| f.eq_ignore_ascii_case(&up)))
1055 .map(|i| i as u32 + 1)
1056 };
1057 let mut month: Option<u32> = None;
1058 let mut nums: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
1059 for t in tokens {
1060 if let Some(m) = month_of(t) {
1061 if month.replace(m).is_some() {
1062 return None; }
1064 } else if t.bytes().all(|b| b.is_ascii_digit()) {
1065 nums.push(t);
1066 } else {
1067 return None; }
1069 }
1070 let m = month?;
1071 if nums.len() != 2 {
1072 return None;
1073 }
1074 let (ys, ds) = match (nums[0].len() >= 3, nums[1].len() >= 3) {
1080 (true, true) => return None,
1081 (true, false) => (nums[0], nums[1]),
1082 (false, true) => (nums[1], nums[0]),
1083 (false, false) => {
1084 if order == DateOrder::Ymd {
1085 (nums[0], nums[1])
1086 } else {
1087 (nums[1], nums[0])
1088 }
1089 }
1090 };
1091 let mut y: i32 = ys.parse().ok()?;
1092 if ys.len() <= 2 {
1093 y += if y < 70 { 2000 } else { 1900 };
1094 }
1095 let d: u32 = ds.parse().ok()?;
1096 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1097 return None;
1098 }
1099 Some(days_from_civil(y, m, d))
1100}
1101
1102pub fn parse_timestamp_literal(s: &str) -> Option<i64> {
1107 parse_timestamp_literal_ordered(s, DateOrder::Mdy)
1108}
1109
1110pub fn date_text_is_field_shaped(s: &str) -> bool {
1116 let s = s.trim();
1117 let date_part = match s.find([' ', 'T']) {
1118 Some(i) => &s[..i],
1119 None => s,
1120 };
1121 let b = date_part.as_bytes();
1122 if b.len() == 8 && b.iter().all(u8::is_ascii_digit) {
1123 return true;
1124 }
1125 let fields: alloc::vec::Vec<&str> = date_part.split(['-', '/', '.']).collect();
1126 fields.len() == 3
1127 && fields
1128 .iter()
1129 .all(|f| !f.is_empty() && f.len() <= 4 && f.bytes().all(|c| c.is_ascii_digit()))
1130}
1131
1132pub fn parse_timestamp_literal_ordered(s: &str, order: DateOrder) -> Option<i64> {
1135 parse_timestamp_literal_tz_ordered(s, order).map(|(us, _)| us)
1136}
1137
1138pub fn parse_timestamp_literal_tz_ordered(s: &str, order: DateOrder) -> Option<(i64, bool)> {
1144 if let Some(v) = timestamp_sentinel(s) {
1145 return Some((v, true));
1146 }
1147 let (days, day_micros, tz) = parse_timestamp_parts(s, order)?;
1148 let t = i64::from(days)
1149 .checked_mul(86_400_000_000)?
1150 .checked_add(day_micros)?
1151 .checked_sub(tz.unwrap_or(0))?;
1152 Some((t, tz.is_some()))
1153}
1154
1155fn parse_timestamp_parts(s: &str, order: DateOrder) -> Option<(i32, i64, Option<i64>)> {
1161 let trimmed = s.trim();
1162 if trimmed.eq_ignore_ascii_case("epoch") {
1165 return Some((0, 0, Some(0)));
1166 }
1167 if trimmed.eq_ignore_ascii_case("infinity")
1171 || trimmed.eq_ignore_ascii_case("+infinity")
1172 || trimmed.eq_ignore_ascii_case("-infinity")
1173 {
1174 return None;
1175 }
1176 let (trimmed, era_bc) = match trimmed
1179 .strip_suffix(" BC")
1180 .or_else(|| trimmed.strip_suffix(" bc"))
1181 {
1182 Some(b) => (b.trim_end(), true),
1183 None => (
1184 trimmed
1185 .strip_suffix(" AD")
1186 .or_else(|| trimmed.strip_suffix(" ad"))
1187 .map_or(trimmed, str::trim_end),
1188 false,
1189 ),
1190 };
1191 let (date_part, time_part) = match trimmed.find([' ', 'T']) {
1192 Some(i) => (&trimmed[..i], Some(&trimmed[i + 1..])),
1193 None => (trimmed, None),
1194 };
1195 if time_part.is_none() && parse_date_literal_ordered(date_part, order).is_none() {
1203 if let Some(rest) = date_part.strip_suffix(['Z', 'z']) {
1204 if let Some(d) = parse_date_literal_ordered(rest, order) {
1205 return Some((d, 0, Some(0)));
1206 }
1207 }
1208 for (i, c) in date_part.char_indices().rev() {
1214 if c != '+' {
1215 continue;
1216 }
1217 let (head, tail) = date_part.split_at(i);
1218 let (Some(d), Some(off)) = (
1219 parse_date_literal_ordered(head, order),
1220 parse_tz_offset_suffix(tail, c == '+'),
1221 ) else {
1222 continue;
1223 };
1224 return Some((d, 0, Some(off)));
1225 }
1226 }
1227 let mut days = parse_date_literal_ordered(date_part, order)?;
1228 if era_bc {
1229 let (y, m, d) = civil_from_days(days);
1230 if y < 1 {
1231 return None;
1232 }
1233 days = days_from_civil(1 - y, m, d);
1234 }
1235 let (day_micros, tz_offset) = match time_part {
1236 None => (0, None),
1237 Some(t) => parse_time_of_day_micros_tz(t)?,
1238 };
1239 Some((days, day_micros, tz_offset))
1240}
1241
1242pub fn parse_timestamp_literal_wall_ordered(s: &str, order: DateOrder) -> Option<i64> {
1261 if let Some(v) = timestamp_sentinel(s) {
1262 return Some(v);
1263 }
1264 let (days, day_micros, _tz) = parse_timestamp_parts(s, order)?;
1267 i64::from(days)
1268 .checked_mul(86_400_000_000)?
1269 .checked_add(day_micros)
1270}
1271
1272fn timestamp_sentinel(s: &str) -> Option<i64> {
1275 let t = s.trim();
1276 if t.eq_ignore_ascii_case("epoch") {
1277 return Some(0);
1278 }
1279 if t.eq_ignore_ascii_case("infinity") || t.eq_ignore_ascii_case("+infinity") {
1280 return Some(i64::MAX);
1281 }
1282 if t.eq_ignore_ascii_case("-infinity") {
1283 return Some(i64::MIN);
1284 }
1285 None
1286}
1287
1288#[must_use]
1315pub(crate) fn datetime_input_error_text(text: &str, type_name: &str) -> alloc::string::String {
1316 let (kind, hint) = classify_datetime_input(text);
1317 match kind {
1318 DatetimeInputProblem::Syntax => {
1319 alloc::format!("invalid input syntax for type {type_name}: \"{text}\"")
1320 }
1321 DatetimeInputProblem::OutOfRange => {
1322 let mut m = alloc::format!("date/time field value out of range: \"{text}\"");
1323 if hint {
1324 m.push_str("\nHINT: Perhaps you need a different \"DateStyle\" setting.");
1325 }
1326 m
1327 }
1328 }
1329}
1330
1331enum DatetimeInputProblem {
1332 Syntax,
1333 OutOfRange,
1334}
1335
1336fn classify_datetime_input(text: &str) -> (DatetimeInputProblem, bool) {
1338 let t = text.trim();
1339 if t.is_empty()
1342 || !t
1343 .chars()
1344 .all(|c| c.is_ascii_digit() || matches!(c, '-' | ':' | '.' | ' ' | 'T' | '+' | 'Z'))
1345 {
1346 return (DatetimeInputProblem::Syntax, false);
1347 }
1348 let date_part = t.split([' ', 'T']).next().unwrap_or("");
1350 let fields: alloc::vec::Vec<&str> = date_part.split('-').collect();
1351 if fields.len() != 3
1352 || fields
1353 .iter()
1354 .any(|f| f.is_empty() || !f.chars().all(|c| c.is_ascii_digit()))
1355 {
1356 return (DatetimeInputProblem::Syntax, false);
1357 }
1358 let month = fields[1].parse::<u32>().unwrap_or(0);
1361 let day = fields[2].parse::<u32>().unwrap_or(0);
1362 let field_out_of_range = !(1..=12).contains(&month) || !(1..=31).contains(&day);
1363 (DatetimeInputProblem::OutOfRange, field_out_of_range)
1364}
1365
1366fn parse_time_of_day_micros(t: &str) -> Option<(i64, i64)> {
1368 parse_time_of_day_micros_tz(t).map(|(us, tz)| (us, tz.unwrap_or(0)))
1369}
1370
1371fn parse_time_of_day_micros_tz(t: &str) -> Option<(i64, Option<i64>)> {
1376 let t = t.trim();
1377 let (core, tz_micros) = if let Some(rest) = t.strip_suffix('Z') {
1383 (rest, Some(0i64))
1384 } else if let Some(rest) = t.strip_suffix(" UTC").or_else(|| t.strip_suffix("UTC")) {
1385 (rest, Some(0i64))
1386 } else if let Some((idx, sign_byte)) = find_offset_sign(t) {
1387 let suffix = &t[idx..];
1388 let micros = parse_tz_offset_suffix(suffix, sign_byte == b'+')?;
1389 (&t[..idx], Some(micros))
1390 } else {
1391 (t, None)
1392 };
1393 let (time, frac_str) = match core.split_once('.') {
1394 Some((a, b)) => (a, Some(b)),
1395 None => (core, None),
1396 };
1397 let bytes = time.as_bytes();
1398 let (hh, mm, ss): (i64, i64, i64) = if bytes.len() == 8 && bytes[2] == b':' && bytes[5] == b':'
1402 {
1403 (
1404 time[0..2].parse().ok()?,
1405 time[3..5].parse().ok()?,
1406 time[6..8].parse().ok()?,
1407 )
1408 } else if bytes.len() == 5 && bytes[2] == b':' {
1409 (time[0..2].parse().ok()?, time[3..5].parse().ok()?, 0)
1410 } else {
1411 return None;
1412 };
1413 if !(0..24).contains(&hh) || !(0..60).contains(&mm) || !(0..60).contains(&ss) {
1414 return None;
1415 }
1416 let frac_micros: i64 = match frac_str {
1417 None => 0,
1418 Some(f) => {
1419 if f.is_empty() || f.len() > 9 {
1421 return None;
1422 }
1423 let mut padded = String::with_capacity(6);
1424 padded.push_str(&f[..f.len().min(6)]);
1425 while padded.len() < 6 {
1426 padded.push('0');
1427 }
1428 padded.parse().ok()?
1429 }
1430 };
1431 Some((
1432 ((hh * 3600 + mm * 60 + ss) * 1_000_000) + frac_micros,
1433 tz_micros,
1434 ))
1435}
1436
1437fn find_offset_sign(t: &str) -> Option<(usize, u8)> {
1443 let bytes = t.as_bytes();
1444 if bytes.len() < 6 {
1448 return None;
1449 }
1450 for i in 5..bytes.len() {
1451 match bytes[i] {
1452 b'+' | b'-' => return Some((i, bytes[i])),
1453 _ => {}
1454 }
1455 }
1456 None
1457}
1458
1459fn parse_tz_offset_suffix(suffix: &str, is_positive: bool) -> Option<i64> {
1463 let body = &suffix[1..];
1465 let (hh, mm): (i64, i64) = if let Some((h, m)) = body.split_once(':') {
1466 (h.parse().ok()?, m.parse().ok()?)
1467 } else {
1468 match body.len() {
1469 2 => (body.parse().ok()?, 0),
1470 3 => {
1471 return None;
1475 }
1476 4 => {
1477 let h: i64 = body[0..2].parse().ok()?;
1478 let m: i64 = body[2..4].parse().ok()?;
1479 (h, m)
1480 }
1481 _ => return None,
1482 }
1483 };
1484 if !(0..=18).contains(&hh) || !(0..60).contains(&mm) {
1485 return None;
1486 }
1487 let abs = (hh * 3600 + mm * 60) * 1_000_000;
1488 Some(if is_positive { abs } else { -abs })
1489}
1490
1491pub fn format_interval(months: i32, days: i32, micros: i64) -> String {
1499 let mut parts: Vec<String> = Vec::new();
1500 let years = months / 12;
1501 let mons = months % 12;
1502 let unit = |n: i64, singular: &'static str, plural: &'static str| -> &'static str {
1505 if n == 1 { singular } else { plural }
1506 };
1507 let mut prev_negative = false;
1510 if years != 0 {
1511 parts.push(format!(
1512 "{years} {}",
1513 unit(i64::from(years), "year", "years")
1514 ));
1515 prev_negative = years < 0;
1516 }
1517 if mons != 0 {
1518 let plus = if prev_negative && mons > 0 { "+" } else { "" };
1519 parts.push(format!(
1520 "{plus}{mons} {}",
1521 unit(i64::from(mons), "mon", "mons")
1522 ));
1523 prev_negative = mons < 0;
1524 }
1525 if days != 0 {
1526 let plus = if prev_negative && days > 0 { "+" } else { "" };
1527 parts.push(format!(
1528 "{plus}{days} {}",
1529 unit(i64::from(days), "day", "days")
1530 ));
1531 }
1532 let mut rem = micros;
1533 if rem != 0 {
1534 let neg = rem < 0;
1535 if neg {
1536 rem = -rem;
1537 }
1538 let secs = rem / 1_000_000;
1539 let frac = rem % 1_000_000;
1540 let hh = secs / 3600;
1541 let mm = (secs / 60) % 60;
1542 let ss = secs % 60;
1543 let is_before = if days != 0 {
1548 days < 0
1549 } else if mons != 0 {
1550 mons < 0
1551 } else {
1552 years < 0
1553 };
1554 let sign = if neg {
1555 "-"
1556 } else if is_before {
1557 "+"
1558 } else {
1559 ""
1560 };
1561 if frac == 0 {
1562 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}"));
1563 } else {
1564 let raw = format!("{frac:06}");
1565 let trimmed = raw.trim_end_matches('0');
1566 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}.{trimmed}"));
1567 }
1568 }
1569 if parts.is_empty() {
1570 "00:00:00".into()
1572 } else {
1573 parts.join(" ")
1574 }
1575}
1576
1577pub fn format_text_array(items: &[Option<String>]) -> String {
1583 let mut out = String::with_capacity(2 + items.len() * 8);
1584 out.push('{');
1585 for (i, item) in items.iter().enumerate() {
1586 if i > 0 {
1587 out.push(',');
1588 }
1589 match item {
1590 None => out.push_str("NULL"),
1591 Some(s) => {
1592 let needs_quote = s.is_empty()
1597 || s.eq_ignore_ascii_case("NULL")
1598 || s.chars().any(|c| {
1599 matches!(
1600 c,
1601 ',' | '{'
1602 | '}'
1603 | '"'
1604 | '\\'
1605 | ' '
1606 | '\t'
1607 | '\n'
1608 | '\r'
1609 | '\x0b'
1610 | '\x0c'
1611 )
1612 });
1613 if needs_quote {
1614 out.push('"');
1615 for c in s.chars() {
1616 if c == '"' || c == '\\' {
1617 out.push('\\');
1618 }
1619 out.push(c);
1620 }
1621 out.push('"');
1622 } else {
1623 out.push_str(s);
1624 }
1625 }
1626 }
1627 }
1628 out.push('}');
1629 out
1630}
1631
1632pub fn format_int_array(items: &[Option<i32>]) -> String {
1636 let mut out = String::with_capacity(2 + items.len() * 4);
1637 out.push('{');
1638 for (i, item) in items.iter().enumerate() {
1639 if i > 0 {
1640 out.push(',');
1641 }
1642 match item {
1643 None => out.push_str("NULL"),
1644 Some(n) => out.push_str(&n.to_string()),
1645 }
1646 }
1647 out.push('}');
1648 out
1649}
1650
1651pub fn format_bigint_array(items: &[Option<i64>]) -> String {
1654 let mut out = String::with_capacity(2 + items.len() * 6);
1655 out.push('{');
1656 for (i, item) in items.iter().enumerate() {
1657 if i > 0 {
1658 out.push(',');
1659 }
1660 match item {
1661 None => out.push_str("NULL"),
1662 Some(n) => out.push_str(&n.to_string()),
1663 }
1664 }
1665 out.push('}');
1666 out
1667}
1668
1669pub fn format_bool_array(items: &[Option<bool>]) -> String {
1673 let mut out = String::with_capacity(2 + items.len() * 2);
1674 out.push('{');
1675 for (i, item) in items.iter().enumerate() {
1676 if i > 0 {
1677 out.push(',');
1678 }
1679 match item {
1680 None => out.push_str("NULL"),
1681 Some(b) => out.push(if *b { 't' } else { 'f' }),
1682 }
1683 }
1684 out.push('}');
1685 out
1686}
1687
1688pub fn format_smallint_array(items: &[Option<i16>]) -> String {
1690 let mut out = String::with_capacity(2 + items.len() * 4);
1691 out.push('{');
1692 for (i, item) in items.iter().enumerate() {
1693 if i > 0 {
1694 out.push(',');
1695 }
1696 match item {
1697 None => out.push_str("NULL"),
1698 Some(n) => out.push_str(&n.to_string()),
1699 }
1700 }
1701 out.push('}');
1702 out
1703}
1704
1705pub fn format_float_array(items: &[Option<f64>]) -> String {
1709 let mut out = String::with_capacity(2 + items.len() * 8);
1710 out.push('{');
1711 for (i, item) in items.iter().enumerate() {
1712 if i > 0 {
1713 out.push(',');
1714 }
1715 match item {
1716 None => out.push_str("NULL"),
1717 Some(x) => out.push_str(&format_float(*x)),
1720 }
1721 }
1722 out.push('}');
1723 out
1724}
1725
1726pub fn format_numeric_array(items: &[Option<(i128, u16)>]) -> String {
1728 let mut out = String::with_capacity(2 + items.len() * 6);
1729 out.push('{');
1730 for (i, item) in items.iter().enumerate() {
1731 if i > 0 {
1732 out.push(',');
1733 }
1734 match item {
1735 None => out.push_str("NULL"),
1736 Some((scaled, scale)) => out.push_str(&format_numeric(*scaled, *scale)),
1737 }
1738 }
1739 out.push('}');
1740 out
1741}
1742
1743pub fn format_date_array(items: &[Option<i32>]) -> String {
1746 let mut out = String::with_capacity(2 + items.len() * 12);
1747 out.push('{');
1748 for (i, item) in items.iter().enumerate() {
1749 if i > 0 {
1750 out.push(',');
1751 }
1752 match item {
1753 None => out.push_str("NULL"),
1754 Some(d) => out.push_str(&format_date(*d)),
1755 }
1756 }
1757 out.push('}');
1758 out
1759}
1760
1761pub fn format_timestamp_array(items: &[Option<i64>], with_tz: bool) -> String {
1767 let mut out = String::with_capacity(2 + items.len() * 22);
1768 out.push('{');
1769 for (i, item) in items.iter().enumerate() {
1770 if i > 0 {
1771 out.push(',');
1772 }
1773 match item {
1774 None => out.push_str("NULL"),
1775 Some(t) => {
1776 out.push('"');
1777 if with_tz {
1778 out.push_str(&format_timestamptz(*t));
1779 } else {
1780 out.push_str(&format_timestamp(*t));
1781 }
1782 out.push('"');
1783 }
1784 }
1785 }
1786 out.push('}');
1787 out
1788}
1789
1790pub fn format_uuid_array(items: &[Option<[u8; 16]>]) -> String {
1794 let mut out = String::with_capacity(2 + items.len() * 38);
1795 out.push('{');
1796 for (i, item) in items.iter().enumerate() {
1797 if i > 0 {
1798 out.push(',');
1799 }
1800 match item {
1801 None => out.push_str("NULL"),
1802 Some(b) => out.push_str(&spg_storage::format_uuid(b)),
1803 }
1804 }
1805 out.push('}');
1806 out
1807}
1808
1809pub fn format_bytea_array(items: &[Option<Vec<u8>>]) -> String {
1814 let mut out = String::with_capacity(2 + items.len() * 8);
1815 out.push('{');
1816 for (i, item) in items.iter().enumerate() {
1817 if i > 0 {
1818 out.push(',');
1819 }
1820 match item {
1821 None => out.push_str("NULL"),
1822 Some(b) => {
1823 out.push('"');
1824 let hex = format_bytea_hex(b);
1825 for c in hex.chars() {
1828 if c == '\\' {
1829 out.push('\\');
1830 }
1831 out.push(c);
1832 }
1833 out.push('"');
1834 }
1835 }
1836 }
1837 out.push('}');
1838 out
1839}
1840
1841pub fn format_interval_array(items: &[Option<spg_storage::IntervalSpan>]) -> String {
1848 let mut out = String::with_capacity(2 + items.len() * 12);
1849 out.push('{');
1850 for (i, item) in items.iter().enumerate() {
1851 if i > 0 {
1852 out.push(',');
1853 }
1854 match item {
1855 None => out.push_str("NULL"),
1856 Some(span) => {
1857 out.push('"');
1858 out.push_str(&format_interval(span.months, span.days, span.micros));
1859 out.push('"');
1860 }
1861 }
1862 }
1863 out.push('}');
1864 out
1865}
1866
1867#[must_use]
1873pub fn format_bytea_escape(b: &[u8]) -> String {
1874 let mut out = String::with_capacity(b.len());
1875 for &byte in b {
1876 match byte {
1877 b'\\' => out.push_str("\\\\"),
1878 0x20..=0x7e => out.push(byte as char),
1879 _ => out.push_str(&alloc::format!("\\{byte:03o}")),
1880 }
1881 }
1882 out
1883}
1884
1885pub fn format_bytea_hex(b: &[u8]) -> String {
1886 let mut out = String::with_capacity(2 + 2 * b.len());
1887 out.push_str("\\x");
1888 const HEX: &[u8; 16] = b"0123456789abcdef";
1889 for byte in b {
1890 out.push(HEX[(byte >> 4) as usize] as char);
1891 out.push(HEX[(byte & 0x0F) as usize] as char);
1892 }
1893 out
1894}
1895
1896pub fn format_numeric_kind(kind: spg_storage::NumericKind, scaled: i128, scale: u16) -> String {
1903 use spg_storage::NumericKind;
1904 match kind {
1905 NumericKind::Finite => format_numeric(scaled, scale),
1906 NumericKind::NaN => String::from("NaN"),
1907 NumericKind::PosInf => String::from("Infinity"),
1908 NumericKind::NegInf => String::from("-Infinity"),
1909 }
1910}
1911
1912pub fn format_numeric(scaled: i128, scale: u16) -> String {
1913 if scale == 0 {
1914 return format!("{scaled}");
1915 }
1916 let negative = scaled < 0;
1917 let mag_str = scaled.unsigned_abs().to_string();
1918 let mag_bytes = mag_str.as_bytes();
1919 let scale_u = scale as usize;
1920 let mut out = String::with_capacity(mag_str.len() + 3);
1921 if negative {
1922 out.push('-');
1923 }
1924 if mag_bytes.len() <= scale_u {
1925 out.push('0');
1926 out.push('.');
1927 for _ in mag_bytes.len()..scale_u {
1928 out.push('0');
1929 }
1930 out.push_str(&mag_str);
1931 } else {
1932 let split = mag_bytes.len() - scale_u;
1933 out.push_str(&mag_str[..split]);
1934 out.push('.');
1935 out.push_str(&mag_str[split..]);
1936 }
1937 out
1938}
1939
1940fn shortest_float_sci(x: f64) -> String {
1958 let (m, e) = f64_mantissa_exp(x);
1959 let (hi_m, hi_e) = (2 * m + 1, e - 1);
1962 let (lo_m, lo_e) = if m == 1 << 52 && e > f64_min_exp() {
1963 (4 * m - 1, e - 2)
1964 } else {
1965 (2 * m - 1, e - 1)
1966 };
1967 for p in 1..=17u32 {
1968 let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
1969 let Ok(v) = cand.parse::<f64>() else { continue };
1970 if v != x {
1971 continue;
1972 }
1973 let Some((d, k)) = sci_to_digits_exp(&cand) else {
1974 continue;
1975 };
1976 if !decimal_eq_binary(d, k, hi_m, hi_e) && !decimal_eq_binary(d, k, lo_m, lo_e) {
1977 return cand;
1978 }
1979 }
1980 alloc::format!("{x:e}")
1981}
1982
1983fn f64_mantissa_exp(x: f64) -> (u128, i32) {
1986 let bits = x.abs().to_bits();
1987 let biased = ((bits >> 52) & 0x7ff) as i32;
1988 let frac = u128::from(bits & 0x000f_ffff_ffff_ffff);
1989 if biased == 0 {
1990 (frac, -1074) } else {
1992 ((1u128 << 52) | frac, biased - 1075)
1993 }
1994}
1995
1996const fn f64_min_exp() -> i32 {
1999 -1074
2000}
2001
2002fn sci_to_digits_exp(sci: &str) -> Option<(u128, i32)> {
2006 let epos = sci.find('e')?;
2007 let (mant, rest) = sci.split_at(epos);
2008 let exp: i32 = rest[1..].parse().ok()?;
2009 let mant = mant.strip_prefix('-').unwrap_or(mant);
2010 let (int_part, frac_part) = match mant.split_once('.') {
2011 Some((a, b)) => (a, b),
2012 None => (mant, ""),
2013 };
2014 let mut digits: u128 = 0;
2015 for c in int_part.chars().chain(frac_part.chars()) {
2016 digits = digits
2017 .checked_mul(10)?
2018 .checked_add(u128::from(c as u8 - b'0'))?;
2019 }
2020 Some((digits, exp - i32::try_from(frac_part.len()).ok()?))
2021}
2022
2023fn decimal_eq_binary(d: u128, k: i32, m: u128, e: i32) -> bool {
2029 if d == 0 {
2030 return false;
2031 }
2032 let a = i32::try_from(d.trailing_zeros()).unwrap_or(i32::MAX);
2033 let d_odd = d >> d.trailing_zeros();
2034 if k >= 0 {
2035 let mut lhs = d_odd;
2037 for _ in 0..k {
2038 match lhs.checked_mul(5) {
2039 Some(v) if v <= m => lhs = v,
2040 _ => return false,
2041 }
2042 }
2043 lhs == m && a + k == e
2044 } else {
2045 let j = -k;
2047 let mut lhs = d_odd;
2048 for _ in 0..j {
2049 if lhs % 5 != 0 {
2050 return false;
2051 }
2052 lhs /= 5;
2053 }
2054 lhs == m && a - j == e
2055 }
2056}