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
255#[must_use]
275pub fn format_interval_kinded(
276 months: i32,
277 days: i32,
278 micros: i64,
279 kind: spg_storage::IntervalKind,
280) -> String {
281 match kind {
282 spg_storage::IntervalKind::PosInf => alloc::string::String::from("infinity"),
283 spg_storage::IntervalKind::NegInf => alloc::string::String::from("-infinity"),
284 spg_storage::IntervalKind::Finite => format_interval(months, days, micros),
285 }
286}
287
288pub fn format_interval_styled(months: i32, days: i32, micros: i64, style: &RenderStyle) -> String {
289 match style.interval_style {
290 IntervalStyleKind::Postgres => format_interval(months, days, micros),
291 IntervalStyleKind::SqlStandard => {
292 let has_ym = months != 0;
293 let has_dt = days != 0 || micros != 0;
294 if !has_ym && !has_dt {
295 return "0".into();
296 }
297 let y = months / 12;
298 let mo = (months % 12).abs();
299 let signs: Vec<i8> = [i64::from(months), i64::from(days), micros]
303 .iter()
304 .filter(|v| **v != 0)
305 .map(|v| if *v < 0 { -1i8 } else { 1 })
306 .collect();
307 let coherent = signs.windows(2).all(|w| w[0] == w[1]);
308 if has_ym && !has_dt && coherent {
309 return format!("{y}-{mo}");
310 }
311 if !has_ym && coherent {
312 let neg = days < 0 || micros < 0;
313 let time = sql_std_time(micros.abs());
314 if days == 0 {
315 return format!("{}{time}", if neg { "-" } else { "" });
316 }
317 return format!("{days} {time}");
318 }
319 let sgn = |neg: bool| if neg { '-' } else { '+' };
322 format!(
323 "{}{}-{} {}{} {}{}",
324 sgn(months < 0),
325 y.abs(),
326 mo,
327 sgn(days < 0),
328 days.abs(),
329 sgn(micros < 0),
330 sql_std_time(micros.abs())
331 )
332 }
333 IntervalStyleKind::Iso8601 => {
334 if months == 0 && days == 0 && micros == 0 {
335 return "PT0S".into();
336 }
337 let y = months / 12;
338 let mo = months % 12;
339 let mut out = String::from("P");
340 if y != 0 {
341 out.push_str(&format!("{y}Y"));
342 }
343 if mo != 0 {
344 out.push_str(&format!("{mo}M"));
345 }
346 if days != 0 {
347 out.push_str(&format!("{days}D"));
348 }
349 if micros != 0 {
350 out.push('T');
351 let neg = micros < 0;
352 let abs = micros.abs();
353 let h = abs / 3_600_000_000;
354 let m = (abs / 60_000_000) % 60;
355 let s_us = abs % 60_000_000;
356 let sgn = if neg { "-" } else { "" };
357 if h != 0 {
358 out.push_str(&format!("{sgn}{h}H"));
359 }
360 if m != 0 {
361 out.push_str(&format!("{sgn}{m}M"));
362 }
363 if s_us != 0 {
364 out.push_str(&format!("{sgn}{}S", secs_body(s_us)));
365 }
366 }
367 out
368 }
369 IntervalStyleKind::PostgresVerbose => {
370 if months == 0 && days == 0 && micros == 0 {
371 return "@ 0".into();
372 }
373 let total = i128::from(months) * 30 * 86_400_000_000
377 + i128::from(days) * 86_400_000_000
378 + i128::from(micros);
379 let ago = total < 0;
380 let (months, days, micros) = if ago {
381 (-months, -days, -micros)
382 } else {
383 (months, days, micros)
384 };
385 let y = months / 12;
386 let mo = months % 12;
387 let neg_t = micros < 0;
388 let abs = micros.abs();
389 let h = abs / 3_600_000_000;
390 let m = (abs / 60_000_000) % 60;
391 let s_us = abs % 60_000_000;
392 let mut parts: Vec<String> = Vec::new();
393 let unit = |n: i64, singular: &'static str| -> String {
394 if n == 1 {
395 singular.into()
396 } else {
397 format!("{singular}s")
398 }
399 };
400 if y != 0 {
401 parts.push(format!("{y} {}", unit(i64::from(y), "year")));
402 }
403 if mo != 0 {
404 parts.push(format!("{mo} {}", unit(i64::from(mo), "mon")));
405 }
406 if days != 0 {
407 parts.push(format!("{days} {}", unit(i64::from(days), "day")));
408 }
409 let tsgn = if neg_t { "-" } else { "" };
410 if h != 0 {
411 parts.push(format!("{tsgn}{h} {}", unit(h, "hour")));
412 }
413 if m != 0 {
414 parts.push(format!("{tsgn}{m} {}", unit(m, "min")));
415 }
416 if s_us != 0 {
417 let body = secs_body(s_us);
418 let plural = body != "1";
419 parts.push(format!(
420 "{tsgn}{body} {}",
421 if plural { "secs" } else { "sec" }
422 ));
423 }
424 let mut out = String::from("@ ");
425 out.push_str(&parts.join(" "));
426 if ago {
427 out.push_str(" ago");
428 }
429 out
430 }
431 }
432}
433
434pub fn format_date_array_styled(items: &[Option<i32>], style: &RenderStyle) -> String {
436 array_styled(items, |d| format_date_styled(*d, style))
437}
438
439pub fn format_timestamp_array_styled(
440 items: &[Option<i64>],
441 with_tz: bool,
442 style: &RenderStyle,
443) -> String {
444 if with_tz {
445 array_styled(items, |t| format_timestamptz_styled(*t, style))
446 } else {
447 array_styled(items, |t| format_timestamp_styled(*t, style))
448 }
449}
450
451pub fn format_interval_array_styled(
452 items: &[Option<spg_storage::IntervalSpan>],
453 style: &RenderStyle,
454) -> String {
455 array_styled(items, |iv| {
456 if iv.kind.is_finite() {
457 format_interval_styled(iv.months, iv.days, iv.micros, style)
458 } else {
459 format_interval_kinded(0, 0, 0, iv.kind)
460 }
461 })
462}
463
464pub fn format_float_array_styled(items: &[Option<f64>], style: &RenderStyle) -> String {
465 array_styled(items, |f| format_float_styled(*f, style))
466}
467
468fn array_styled<T>(items: &[Option<T>], mut f: impl FnMut(&T) -> String) -> String {
469 let mut out = String::with_capacity(2 + items.len() * 12);
470 out.push('{');
471 for (i, item) in items.iter().enumerate() {
472 if i > 0 {
473 out.push(',');
474 }
475 match item {
476 None => out.push_str("NULL"),
477 Some(v) => push_array_element(&mut out, &f(v)),
478 }
479 }
480 out.push('}');
481 out
482}
483
484fn push_array_element(out: &mut String, s: &str) {
492 let needs_quote = s.is_empty()
493 || s.eq_ignore_ascii_case("null")
494 || s.chars()
495 .any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\') || c.is_whitespace());
496 if !needs_quote {
497 out.push_str(s);
498 return;
499 }
500 out.push('"');
501 for c in s.chars() {
502 if c == '"' || c == '\\' {
503 out.push('\\');
504 }
505 out.push(c);
506 }
507 out.push('"');
508}
509
510fn format_g(x: f64, prec: usize) -> String {
515 let prec = prec.max(1);
516 let sci = format!("{:.*e}", prec - 1, x);
519 let epos = sci.find('e').expect("{:e} always has an 'e'");
520 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
521 let mant = &sci[..epos];
522 if exp_val >= -4 && (exp_val as i64) < prec as i64 {
523 let decimals =
525 usize::try_from(i64::try_from(prec).unwrap_or(1) - 1 - i64::from(exp_val)).unwrap_or(0);
526 let rounded: f64 = sci.parse().unwrap_or(x);
527 let fixed = format!("{rounded:.decimals$}");
528 if fixed.contains('.') {
529 let t = fixed.trim_end_matches('0').trim_end_matches('.');
530 t.into()
531 } else {
532 fixed
533 }
534 } else {
535 let mant = if mant.contains('.') {
536 mant.trim_end_matches('0').trim_end_matches('.')
537 } else {
538 mant
539 };
540 let (sign, digits) = if exp_val < 0 {
541 ('-', format!("{}", -exp_val))
542 } else {
543 ('+', format!("{exp_val}"))
544 };
545 format!("{mant}e{sign}{digits:0>2}")
546 }
547}
548
549pub fn format_float_styled(x: f64, style: &RenderStyle) -> String {
552 if style.mysql {
553 return format_float_mysql(x);
554 }
555 if style.extra_float_digits >= 1 {
556 return format_float(x);
557 }
558 if x.is_nan() {
559 return "NaN".into();
560 }
561 if x.is_infinite() {
562 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
563 }
564 if x == 0.0 {
565 return if x.is_sign_negative() { "-0" } else { "0" }.into();
566 }
567 let prec = (15 + style.extra_float_digits).clamp(1, 17) as usize;
568 format_g(x, prec)
569}
570
571fn format_mysql_float(sci: &str, mant: &str, exp_val: i32) -> String {
587 if (-15..=14).contains(&exp_val) {
588 let fixed = fixed_from_sci(sci, exp_val);
592 if fixed.contains('.') {
593 return alloc::string::String::from(fixed.trim_end_matches('0').trim_end_matches('.'));
594 }
595 return fixed;
596 }
597 let mant = if mant.contains('.') {
598 mant.trim_end_matches('0').trim_end_matches('.')
599 } else {
600 mant
601 };
602 alloc::format!("{mant}e{exp_val}")
603}
604
605fn split_sci(sci: &str) -> (&str, i32) {
607 let epos = sci.find('e').expect("{:e} always has an 'e'");
608 (&sci[..epos], sci[epos + 1..].parse().unwrap_or(0))
609}
610
611pub fn format_real_mysql(x: f32) -> String {
616 if x.is_nan() {
617 return "NaN".into();
618 }
619 if x.is_infinite() {
620 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
621 }
622 if x == 0.0 {
623 return if x.is_sign_negative() { "-0" } else { "0" }.into();
624 }
625 let sci = alloc::format!("{:.*e}", 5, f64::from(x));
627 let (mant, exp_val) = split_sci(&sci);
628 format_mysql_float(&sci, mant, exp_val)
629}
630
631pub fn format_float_mysql(x: f64) -> String {
633 if x.is_nan() {
634 return "NaN".into();
635 }
636 if x.is_infinite() {
637 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
638 }
639 if x == 0.0 {
640 return if x.is_sign_negative() { "-0" } else { "0" }.into();
641 }
642 let sci = shortest_float_sci(x);
645 let (mant, exp_val) = split_sci(&sci);
646 format_mysql_float(&sci, mant, exp_val)
647}
648
649pub fn format_real_styled(x: f32, style: &RenderStyle) -> String {
650 if style.mysql {
651 return format_real_mysql(x);
652 }
653 if style.extra_float_digits >= 1 {
654 return format_real(x);
655 }
656 if x.is_nan() {
657 return "NaN".into();
658 }
659 if x.is_infinite() {
660 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
661 }
662 if x == 0.0 {
663 return if x.is_sign_negative() { "-0" } else { "0" }.into();
664 }
665 let prec = (6 + style.extra_float_digits).clamp(1, 9) as usize;
666 format_g(f64::from(x), prec)
667}
668
669pub fn format_date(days: i32) -> String {
672 if days == i32::MAX {
673 return "infinity".into();
674 }
675 if days == i32::MIN {
676 return "-infinity".into();
677 }
678 let (y, m, d) = civil_from_days(days);
679 if y <= 0 {
682 return format!("{:04}-{m:02}-{d:02} BC", 1 - y);
683 }
684 format!("{y:04}-{m:02}-{d:02}")
685}
686
687pub fn format_timestamptz(micros: i64) -> String {
698 format_timestamptz_at(micros, 0)
699}
700
701pub fn format_timestamptz_at(micros: i64, offset_micros: i64) -> String {
706 if micros == i64::MAX || micros == i64::MIN {
707 return format_timestamp(micros);
708 }
709 let base = format_timestamp(micros + offset_micros);
710 let (base, bc) = match base.strip_suffix(" BC") {
713 Some(b) => (String::from(b), " BC"),
714 None => (base, ""),
715 };
716 let mut s = String::with_capacity(base.len() + 9);
717 s.push_str(&base);
718 let total_min = (offset_micros / 60_000_000).abs();
719 let (h, m) = (total_min / 60, total_min % 60);
720 s.push(if offset_micros < 0 { '-' } else { '+' });
721 s.push_str(&alloc::format!("{h:02}"));
722 if m != 0 {
723 s.push(':');
724 s.push_str(&alloc::format!("{m:02}"));
725 }
726 s.push_str(bc);
727 s
728}
729
730pub fn format_float(x: f64) -> String {
740 if x.is_nan() {
741 return "NaN".into();
742 }
743 if x.is_infinite() {
744 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
745 }
746 if x == 0.0 {
747 return if x.is_sign_negative() { "-0" } else { "0" }.into();
748 }
749 let sci = shortest_float_sci(x); let epos = sci.find('e').expect("{:e} always has an 'e'");
751 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
752 if (-4..=14).contains(&exp_val) {
753 return fixed_from_sci(&sci, exp_val);
756 }
757 let mant = &sci[..epos];
758 let exp = &sci[epos + 1..];
759 let (sign, digits) = match exp.strip_prefix('-') {
760 Some(d) => ('-', d),
761 None => ('+', exp),
762 };
763 alloc::format!("{mant}e{sign}{digits:0>2}")
764}
765
766fn shortest_real_sci(x: f32) -> String {
783 let wide = f64::from(x);
784 let below = f64::from(next_f32(x, false));
785 let above = f64::from(next_f32(x, true));
786 let lo = (wide + below) / 2.0;
788 let hi = (wide + above) / 2.0;
789 for p in 1..=9u32 {
790 let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
791 let Ok(v) = cand.parse::<f64>() else { continue };
792 #[allow(clippy::cast_possible_truncation)]
794 if v as f32 == x && v != lo && v != hi {
795 return cand;
796 }
797 }
798 alloc::format!("{x:e}")
799}
800
801fn next_f32(x: f32, up: bool) -> f32 {
804 let bits = x.to_bits();
805 let stepped = if (x > 0.0) == up { bits + 1 } else { bits - 1 };
806 f32::from_bits(stepped)
807}
808
809fn fixed_from_sci(sci: &str, exp: i32) -> String {
811 let epos = sci.find('e').expect("{:e} always has an 'e'");
812 let (mant, _) = sci.split_at(epos);
813 let (sign, mant) = match mant.strip_prefix('-') {
814 Some(m) => ("-", m),
815 None => ("", mant),
816 };
817 let digits: String = mant.chars().filter(char::is_ascii_digit).collect();
818 let point = exp + 1; let mut out = String::from(sign);
820 if point <= 0 {
821 out.push_str("0.");
822 for _ in 0..-point {
823 out.push('0');
824 }
825 out.push_str(&digits);
826 } else if (point as usize) >= digits.len() {
827 out.push_str(&digits);
828 for _ in 0..(point as usize - digits.len()) {
829 out.push('0');
830 }
831 } else {
832 out.push_str(&digits[..point as usize]);
833 out.push('.');
834 out.push_str(&digits[point as usize..]);
835 }
836 out
837}
838
839pub fn format_real(x: f32) -> String {
844 if x.is_nan() {
845 return "NaN".into();
846 }
847 if x.is_infinite() {
848 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
849 }
850 if x == 0.0 {
851 return if x.is_sign_negative() { "-0" } else { "0" }.into();
852 }
853 let sci = shortest_real_sci(x);
854 let epos = sci.find('e').expect("{:e} always has an 'e'");
855 let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
856 if (-4..=5).contains(&exp_val) {
857 return fixed_from_sci(&sci, exp_val);
861 }
862 let mant = &sci[..epos];
863 let exp = &sci[epos + 1..];
864 let (sign, digits) = match exp.strip_prefix('-') {
865 Some(d) => ('-', d),
866 None => ('+', exp),
867 };
868 alloc::format!("{mant}e{sign}{digits:0>2}")
869}
870
871pub fn format_money(cents: i64) -> String {
874 let neg = cents < 0;
875 let abs = cents.unsigned_abs();
876 let dollars = abs / 100;
877 let cc = abs % 100;
878 let dollar_str = dollars.to_string();
880 let bytes = dollar_str.as_bytes();
881 let mut int_part = String::with_capacity(dollar_str.len() + dollar_str.len() / 3);
882 for (i, b) in bytes.iter().enumerate() {
883 let from_right = bytes.len() - i;
886 if i > 0 && from_right % 3 == 0 {
887 int_part.push(',');
888 }
889 int_part.push(*b as char);
890 }
891 let sign = if neg { "-" } else { "" };
892 format!("{sign}${int_part}.{cc:02}")
893}
894
895pub fn format_timetz(us: i64, offset_secs: i32) -> String {
900 let time = format_time(us);
901 let sign = if offset_secs < 0 { '-' } else { '+' };
902 let abs = offset_secs.unsigned_abs();
903 let oh = abs / 3600;
904 let om = (abs % 3600) / 60;
905 if om == 0 {
906 format!("{time}{sign}{oh:02}")
907 } else {
908 format!("{time}{sign}{oh:02}:{om:02}")
909 }
910}
911
912pub fn format_time(us: i64) -> String {
917 let total_secs = us.div_euclid(1_000_000);
918 let frac = us.rem_euclid(1_000_000);
919 let hh = total_secs / 3600;
920 let mm = (total_secs / 60) % 60;
921 let ss = total_secs % 60;
922 if frac == 0 {
923 format!("{hh:02}:{mm:02}:{ss:02}")
924 } else {
925 let raw = format!("{frac:06}");
926 let trimmed = raw.trim_end_matches('0');
927 format!("{hh:02}:{mm:02}:{ss:02}.{trimmed}")
928 }
929}
930
931pub fn format_timestamp(micros: i64) -> String {
932 if micros == i64::MAX {
934 return "infinity".into();
935 }
936 if micros == i64::MIN {
937 return "-infinity".into();
938 }
939 const MICROS_PER_DAY: i64 = 86_400_000_000;
940 let days = micros.div_euclid(MICROS_PER_DAY);
943 let day_micros = micros.rem_euclid(MICROS_PER_DAY);
944 let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
945 let (y, m, d) = civil_from_days(day_i32);
946 let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
948 let secs = day_micros / 1_000_000;
949 let frac = day_micros % 1_000_000;
950 let hh = secs / 3600;
951 let mm = (secs / 60) % 60;
952 let ss = secs % 60;
953 if frac == 0 {
954 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}{bc}")
955 } else {
956 let raw = format!("{frac:06}");
958 let trimmed = raw.trim_end_matches('0');
959 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}.{trimmed}{bc}")
960 }
961}
962
963#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
966pub fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
967 let y_adj = if m <= 2 {
968 i64::from(y) - 1
969 } else {
970 i64::from(y)
971 };
972 let era = y_adj.div_euclid(400);
973 let yoe = (y_adj - era * 400) as u32;
974 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.saturating_sub(1);
975 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
976 let total = era * 146_097 + i64::from(doe) - 719_468;
977 i32::try_from(total).unwrap_or(i32::MAX)
978}
979
980pub fn parse_date_literal(s: &str) -> Option<i32> {
984 parse_date_literal_ordered(s, DateOrder::Mdy)
985}
986
987pub fn parse_date_literal_ordered(s: &str, order: DateOrder) -> Option<i32> {
997 let s = s.trim();
998 if let Some(base) = s
1002 .strip_suffix(" BC")
1003 .or_else(|| s.strip_suffix(" bc"))
1004 .or_else(|| s.strip_suffix(" Bc"))
1005 {
1006 let days = parse_date_literal_ordered(base, order)?;
1007 let (y, m, d) = civil_from_days(days);
1008 if y < 1 {
1009 return None;
1010 }
1011 return Some(days_from_civil(1 - y, m, d));
1012 }
1013 if let Some(base) = s.strip_suffix(" AD").or_else(|| s.strip_suffix(" ad")) {
1014 return parse_date_literal_ordered(base, order);
1015 }
1016 if s.eq_ignore_ascii_case("epoch") {
1018 return Some(days_from_civil(1970, 1, 1));
1019 }
1020 if s.eq_ignore_ascii_case("infinity") || s.eq_ignore_ascii_case("+infinity") {
1021 return Some(i32::MAX);
1022 }
1023 if s.eq_ignore_ascii_case("-infinity") {
1024 return Some(i32::MIN);
1025 }
1026 let bytes = s.as_bytes();
1027 if bytes.len() == 8 && bytes.iter().all(u8::is_ascii_digit) {
1029 let y: i32 = s[0..4].parse().ok()?;
1030 let m: u32 = s[4..6].parse().ok()?;
1031 let d: u32 = s[6..8].parse().ok()?;
1032 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1033 return None;
1034 }
1035 return Some(days_from_civil(y, m, d));
1036 }
1037 if s.bytes().any(|b| b.is_ascii_alphabetic()) {
1042 if let Some(jd) = s.strip_prefix(['J', 'j'])
1045 && !jd.is_empty()
1046 && jd.bytes().all(|b| b.is_ascii_digit())
1047 {
1048 let jd: i64 = jd.parse().ok()?;
1049 return i32::try_from(jd - 2_440_588).ok();
1050 }
1051 return parse_month_name_date(s, order);
1052 }
1053 {
1060 let mut two = s.splitn(2, ['-', '/', '.']);
1061 if let (Some(ya), Some(dd)) = (two.next(), two.next())
1062 && ya.len() >= 3
1063 && dd.len() == 3
1064 && !dd.contains(['-', '/', '.', ' '])
1065 && ya.bytes().all(|b| b.is_ascii_digit())
1066 && dd.bytes().all(|b| b.is_ascii_digit())
1067 {
1068 let y: i32 = ya.parse().ok()?;
1069 let doy: i64 = dd.parse().ok()?;
1070 if y != 0 && (1..=366).contains(&doy) {
1071 let jan1 = days_from_civil(y, 1, 1);
1072 let days = jan1 + i32::try_from(doy).ok()? - 1;
1073 let (yy, _, _) = civil_from_days(days);
1074 if yy == y {
1075 return Some(days);
1076 }
1077 return None; }
1079 }
1080 }
1081 let mut parts = s.splitn(3, ['-', '/', '.']);
1082 let (fa, fb, fc) = (parts.next()?, parts.next()?, parts.next()?);
1083 if fc.contains(['-', '/', '.', ' ']) {
1084 return None; }
1086 if [fa, fb, fc]
1087 .iter()
1088 .any(|p| p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()))
1089 {
1090 return None;
1091 }
1092 if fa.len() >= 3 && fb.len() <= 2 && fc.len() <= 2 {
1096 let y: i32 = fa.parse().ok()?;
1097 if y == 0 {
1099 return None;
1100 }
1101 let m: u32 = fb.parse().ok()?;
1105 let d: u32 = fc.parse().ok()?;
1106 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1110 return None;
1111 }
1112 return Some(days_from_civil(y, m, d));
1113 }
1114 let expand_year = |t: &str| -> Option<i32> {
1116 match t.len() {
1117 4 => t.parse().ok(),
1118 1 | 2 => {
1120 let n: i32 = t.parse().ok()?;
1121 Some(if n < 70 { 2000 + n } else { 1900 + n })
1122 }
1123 _ => None,
1124 }
1125 };
1126 let (ys, ms, ds) = match order {
1127 DateOrder::Mdy => (fc, fa, fb),
1128 DateOrder::Dmy => (fc, fb, fa),
1129 DateOrder::Ymd => (fa, fb, fc),
1130 };
1131 if ms.len() > 2 || ds.len() > 2 {
1132 return None;
1133 }
1134 let y = expand_year(ys)?;
1135 let m: u32 = ms.parse().ok()?;
1136 let d: u32 = ds.parse().ok()?;
1137 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1138 return None;
1139 }
1140 Some(days_from_civil(y, m, d))
1141}
1142
1143fn parse_month_name_date(s: &str, order: DateOrder) -> Option<i32> {
1150 let tokens: alloc::vec::Vec<&str> =
1151 s.split([' ', ',', '-']).filter(|t| !t.is_empty()).collect();
1152 if tokens.len() != 3 {
1153 return None;
1154 }
1155 let month_of = |t: &str| -> Option<u32> {
1156 let up = t.to_ascii_uppercase();
1157 MONTH_ABBR
1158 .iter()
1159 .position(|a| a.eq_ignore_ascii_case(&up))
1160 .or_else(|| MONTH_FULL.iter().position(|f| f.eq_ignore_ascii_case(&up)))
1161 .map(|i| i as u32 + 1)
1162 };
1163 let mut month: Option<u32> = None;
1164 let mut nums: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
1165 for t in tokens {
1166 if let Some(m) = month_of(t) {
1167 if month.replace(m).is_some() {
1168 return None; }
1170 } else if t.bytes().all(|b| b.is_ascii_digit()) {
1171 nums.push(t);
1172 } else {
1173 return None; }
1175 }
1176 let m = month?;
1177 if nums.len() != 2 {
1178 return None;
1179 }
1180 let (ys, ds) = match (nums[0].len() >= 3, nums[1].len() >= 3) {
1186 (true, true) => return None,
1187 (true, false) => (nums[0], nums[1]),
1188 (false, true) => (nums[1], nums[0]),
1189 (false, false) => {
1190 if order == DateOrder::Ymd {
1191 (nums[0], nums[1])
1192 } else {
1193 (nums[1], nums[0])
1194 }
1195 }
1196 };
1197 let mut y: i32 = ys.parse().ok()?;
1198 if ys.len() <= 2 {
1199 y += if y < 70 { 2000 } else { 1900 };
1200 }
1201 let d: u32 = ds.parse().ok()?;
1202 if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
1203 return None;
1204 }
1205 Some(days_from_civil(y, m, d))
1206}
1207
1208pub fn parse_timestamp_literal(s: &str) -> Option<i64> {
1213 parse_timestamp_literal_ordered(s, DateOrder::Mdy)
1214}
1215
1216pub fn date_text_is_field_shaped(s: &str) -> bool {
1222 let s = s.trim();
1223 let date_part = match s.find([' ', 'T']) {
1224 Some(i) => &s[..i],
1225 None => s,
1226 };
1227 let b = date_part.as_bytes();
1228 if b.len() == 8 && b.iter().all(u8::is_ascii_digit) {
1229 return true;
1230 }
1231 let fields: alloc::vec::Vec<&str> = date_part.split(['-', '/', '.']).collect();
1232 fields.len() == 3
1233 && fields
1234 .iter()
1235 .all(|f| !f.is_empty() && f.len() <= 4 && f.bytes().all(|c| c.is_ascii_digit()))
1236}
1237
1238pub fn parse_timestamp_literal_ordered(s: &str, order: DateOrder) -> Option<i64> {
1241 parse_timestamp_literal_tz_ordered(s, order).map(|(us, _)| us)
1242}
1243
1244pub fn parse_timestamp_literal_tz_ordered(s: &str, order: DateOrder) -> Option<(i64, bool)> {
1250 if let Some(v) = timestamp_sentinel(s) {
1251 return Some((v, true));
1252 }
1253 let (days, day_micros, tz) = parse_timestamp_parts(s, order)?;
1254 let t = i64::from(days)
1255 .checked_mul(86_400_000_000)?
1256 .checked_add(day_micros)?
1257 .checked_sub(tz.unwrap_or(0))?;
1258 Some((t, tz.is_some()))
1259}
1260
1261fn parse_timestamp_parts(s: &str, order: DateOrder) -> Option<(i32, i64, Option<i64>)> {
1267 let trimmed = s.trim();
1268 if trimmed.eq_ignore_ascii_case("epoch") {
1271 return Some((0, 0, Some(0)));
1272 }
1273 if trimmed.eq_ignore_ascii_case("infinity")
1277 || trimmed.eq_ignore_ascii_case("+infinity")
1278 || trimmed.eq_ignore_ascii_case("-infinity")
1279 {
1280 return None;
1281 }
1282 let (trimmed, era_bc) = match trimmed
1285 .strip_suffix(" BC")
1286 .or_else(|| trimmed.strip_suffix(" bc"))
1287 {
1288 Some(b) => (b.trim_end(), true),
1289 None => (
1290 trimmed
1291 .strip_suffix(" AD")
1292 .or_else(|| trimmed.strip_suffix(" ad"))
1293 .map_or(trimmed, str::trim_end),
1294 false,
1295 ),
1296 };
1297 let (date_part, time_part) = match trimmed.find([' ', 'T']) {
1298 Some(i) => (&trimmed[..i], Some(&trimmed[i + 1..])),
1299 None => (trimmed, None),
1300 };
1301 if time_part.is_none() && parse_date_literal_ordered(date_part, order).is_none() {
1309 if let Some(rest) = date_part.strip_suffix(['Z', 'z']) {
1310 if let Some(d) = parse_date_literal_ordered(rest, order) {
1311 return Some((d, 0, Some(0)));
1312 }
1313 }
1314 for (i, c) in date_part.char_indices().rev() {
1320 if c != '+' {
1321 continue;
1322 }
1323 let (head, tail) = date_part.split_at(i);
1324 let (Some(d), Some(off)) = (
1325 parse_date_literal_ordered(head, order),
1326 parse_tz_offset_suffix(tail, c == '+'),
1327 ) else {
1328 continue;
1329 };
1330 return Some((d, 0, Some(off)));
1331 }
1332 }
1333 let mut days = parse_date_literal_ordered(date_part, order)?;
1334 if era_bc {
1335 let (y, m, d) = civil_from_days(days);
1336 if y < 1 {
1337 return None;
1338 }
1339 days = days_from_civil(1 - y, m, d);
1340 }
1341 let (day_micros, tz_offset) = match time_part {
1342 None => (0, None),
1343 Some(t) => parse_time_of_day_micros_tz(t)?,
1344 };
1345 Some((days, day_micros, tz_offset))
1346}
1347
1348pub fn parse_timestamp_literal_wall_ordered(s: &str, order: DateOrder) -> Option<i64> {
1367 if let Some(v) = timestamp_sentinel(s) {
1368 return Some(v);
1369 }
1370 let (days, day_micros, _tz) = parse_timestamp_parts(s, order)?;
1373 i64::from(days)
1374 .checked_mul(86_400_000_000)?
1375 .checked_add(day_micros)
1376}
1377
1378fn timestamp_sentinel(s: &str) -> Option<i64> {
1381 let t = s.trim();
1382 if t.eq_ignore_ascii_case("epoch") {
1383 return Some(0);
1384 }
1385 if t.eq_ignore_ascii_case("infinity") || t.eq_ignore_ascii_case("+infinity") {
1386 return Some(i64::MAX);
1387 }
1388 if t.eq_ignore_ascii_case("-infinity") {
1389 return Some(i64::MIN);
1390 }
1391 None
1392}
1393
1394#[must_use]
1421pub(crate) fn datetime_input_error_text(text: &str, type_name: &str) -> alloc::string::String {
1422 let (kind, hint) = classify_datetime_input(text);
1423 match kind {
1424 DatetimeInputProblem::Syntax => {
1425 alloc::format!("invalid input syntax for type {type_name}: \"{text}\"")
1426 }
1427 DatetimeInputProblem::OutOfRange => {
1428 let mut m = alloc::format!("date/time field value out of range: \"{text}\"");
1429 if hint {
1430 m.push_str("\nHINT: Perhaps you need a different \"DateStyle\" setting.");
1431 }
1432 m
1433 }
1434 }
1435}
1436
1437enum DatetimeInputProblem {
1438 Syntax,
1439 OutOfRange,
1440}
1441
1442fn classify_datetime_input(text: &str) -> (DatetimeInputProblem, bool) {
1444 let t = text.trim();
1445 if t.is_empty()
1448 || !t
1449 .chars()
1450 .all(|c| c.is_ascii_digit() || matches!(c, '-' | ':' | '.' | ' ' | 'T' | '+' | 'Z'))
1451 {
1452 return (DatetimeInputProblem::Syntax, false);
1453 }
1454 let date_part = t.split([' ', 'T']).next().unwrap_or("");
1456 let fields: alloc::vec::Vec<&str> = date_part.split('-').collect();
1457 if fields.len() != 3
1458 || fields
1459 .iter()
1460 .any(|f| f.is_empty() || !f.chars().all(|c| c.is_ascii_digit()))
1461 {
1462 return (DatetimeInputProblem::Syntax, false);
1463 }
1464 let month = fields[1].parse::<u32>().unwrap_or(0);
1467 let day = fields[2].parse::<u32>().unwrap_or(0);
1468 let field_out_of_range = !(1..=12).contains(&month) || !(1..=31).contains(&day);
1469 (DatetimeInputProblem::OutOfRange, field_out_of_range)
1470}
1471
1472fn parse_time_of_day_micros(t: &str) -> Option<(i64, i64)> {
1474 parse_time_of_day_micros_tz(t).map(|(us, tz)| (us, tz.unwrap_or(0)))
1475}
1476
1477fn parse_time_of_day_micros_tz(t: &str) -> Option<(i64, Option<i64>)> {
1482 let t = t.trim();
1483 let (core, tz_micros) = if let Some(rest) = t.strip_suffix('Z') {
1489 (rest, Some(0i64))
1490 } else if let Some(rest) = t.strip_suffix(" UTC").or_else(|| t.strip_suffix("UTC")) {
1491 (rest, Some(0i64))
1492 } else if let Some((idx, sign_byte)) = find_offset_sign(t) {
1493 let suffix = &t[idx..];
1494 let micros = parse_tz_offset_suffix(suffix, sign_byte == b'+')?;
1495 (&t[..idx], Some(micros))
1496 } else {
1497 (t, None)
1498 };
1499 let (time, frac_str) = match core.split_once('.') {
1500 Some((a, b)) => (a, Some(b)),
1501 None => (core, None),
1502 };
1503 let bytes = time.as_bytes();
1504 let (hh, mm, ss): (i64, i64, i64) = if bytes.len() == 8 && bytes[2] == b':' && bytes[5] == b':'
1508 {
1509 (
1510 time[0..2].parse().ok()?,
1511 time[3..5].parse().ok()?,
1512 time[6..8].parse().ok()?,
1513 )
1514 } else if bytes.len() == 5 && bytes[2] == b':' {
1515 (time[0..2].parse().ok()?, time[3..5].parse().ok()?, 0)
1516 } else {
1517 return None;
1518 };
1519 if !(0..24).contains(&hh) || !(0..60).contains(&mm) || !(0..60).contains(&ss) {
1520 return None;
1521 }
1522 let frac_micros: i64 = match frac_str {
1523 None => 0,
1524 Some(f) => {
1525 if f.is_empty() || f.len() > 9 {
1527 return None;
1528 }
1529 let mut padded = String::with_capacity(6);
1530 padded.push_str(&f[..f.len().min(6)]);
1531 while padded.len() < 6 {
1532 padded.push('0');
1533 }
1534 padded.parse().ok()?
1535 }
1536 };
1537 Some((
1538 ((hh * 3600 + mm * 60 + ss) * 1_000_000) + frac_micros,
1539 tz_micros,
1540 ))
1541}
1542
1543fn find_offset_sign(t: &str) -> Option<(usize, u8)> {
1549 let bytes = t.as_bytes();
1550 if bytes.len() < 6 {
1554 return None;
1555 }
1556 for i in 5..bytes.len() {
1557 match bytes[i] {
1558 b'+' | b'-' => return Some((i, bytes[i])),
1559 _ => {}
1560 }
1561 }
1562 None
1563}
1564
1565fn parse_tz_offset_suffix(suffix: &str, is_positive: bool) -> Option<i64> {
1569 let body = &suffix[1..];
1571 let (hh, mm): (i64, i64) = if let Some((h, m)) = body.split_once(':') {
1572 (h.parse().ok()?, m.parse().ok()?)
1573 } else {
1574 match body.len() {
1575 2 => (body.parse().ok()?, 0),
1576 3 => {
1577 return None;
1581 }
1582 4 => {
1583 let h: i64 = body[0..2].parse().ok()?;
1584 let m: i64 = body[2..4].parse().ok()?;
1585 (h, m)
1586 }
1587 _ => return None,
1588 }
1589 };
1590 if !(0..=18).contains(&hh) || !(0..60).contains(&mm) {
1591 return None;
1592 }
1593 let abs = (hh * 3600 + mm * 60) * 1_000_000;
1594 Some(if is_positive { abs } else { -abs })
1595}
1596
1597pub fn format_interval(months: i32, days: i32, micros: i64) -> String {
1605 let mut parts: Vec<String> = Vec::new();
1606 let years = months / 12;
1607 let mons = months % 12;
1608 let unit = |n: i64, singular: &'static str, plural: &'static str| -> &'static str {
1611 if n == 1 { singular } else { plural }
1612 };
1613 let mut prev_negative = false;
1616 if years != 0 {
1617 parts.push(format!(
1618 "{years} {}",
1619 unit(i64::from(years), "year", "years")
1620 ));
1621 prev_negative = years < 0;
1622 }
1623 if mons != 0 {
1624 let plus = if prev_negative && mons > 0 { "+" } else { "" };
1625 parts.push(format!(
1626 "{plus}{mons} {}",
1627 unit(i64::from(mons), "mon", "mons")
1628 ));
1629 prev_negative = mons < 0;
1630 }
1631 if days != 0 {
1632 let plus = if prev_negative && days > 0 { "+" } else { "" };
1633 parts.push(format!(
1634 "{plus}{days} {}",
1635 unit(i64::from(days), "day", "days")
1636 ));
1637 }
1638 let mut rem = micros;
1639 if rem != 0 {
1640 let neg = rem < 0;
1641 if neg {
1642 rem = -rem;
1643 }
1644 let secs = rem / 1_000_000;
1645 let frac = rem % 1_000_000;
1646 let hh = secs / 3600;
1647 let mm = (secs / 60) % 60;
1648 let ss = secs % 60;
1649 let is_before = if days != 0 {
1654 days < 0
1655 } else if mons != 0 {
1656 mons < 0
1657 } else {
1658 years < 0
1659 };
1660 let sign = if neg {
1661 "-"
1662 } else if is_before {
1663 "+"
1664 } else {
1665 ""
1666 };
1667 if frac == 0 {
1668 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}"));
1669 } else {
1670 let raw = format!("{frac:06}");
1671 let trimmed = raw.trim_end_matches('0');
1672 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}.{trimmed}"));
1673 }
1674 }
1675 if parts.is_empty() {
1676 "00:00:00".into()
1678 } else {
1679 parts.join(" ")
1680 }
1681}
1682
1683pub fn format_text_array(items: &[Option<String>]) -> String {
1689 let mut out = String::with_capacity(2 + items.len() * 8);
1690 out.push('{');
1691 for (i, item) in items.iter().enumerate() {
1692 if i > 0 {
1693 out.push(',');
1694 }
1695 match item {
1696 None => out.push_str("NULL"),
1697 Some(s) => {
1698 let needs_quote = s.is_empty()
1703 || s.eq_ignore_ascii_case("NULL")
1704 || s.chars().any(|c| {
1705 matches!(
1706 c,
1707 ',' | '{'
1708 | '}'
1709 | '"'
1710 | '\\'
1711 | ' '
1712 | '\t'
1713 | '\n'
1714 | '\r'
1715 | '\x0b'
1716 | '\x0c'
1717 )
1718 });
1719 if needs_quote {
1720 out.push('"');
1721 for c in s.chars() {
1722 if c == '"' || c == '\\' {
1723 out.push('\\');
1724 }
1725 out.push(c);
1726 }
1727 out.push('"');
1728 } else {
1729 out.push_str(s);
1730 }
1731 }
1732 }
1733 }
1734 out.push('}');
1735 out
1736}
1737
1738pub fn format_int_array(items: &[Option<i32>]) -> String {
1742 let mut out = String::with_capacity(2 + items.len() * 4);
1743 out.push('{');
1744 for (i, item) in items.iter().enumerate() {
1745 if i > 0 {
1746 out.push(',');
1747 }
1748 match item {
1749 None => out.push_str("NULL"),
1750 Some(n) => out.push_str(&n.to_string()),
1751 }
1752 }
1753 out.push('}');
1754 out
1755}
1756
1757pub fn format_bigint_array(items: &[Option<i64>]) -> String {
1760 let mut out = String::with_capacity(2 + items.len() * 6);
1761 out.push('{');
1762 for (i, item) in items.iter().enumerate() {
1763 if i > 0 {
1764 out.push(',');
1765 }
1766 match item {
1767 None => out.push_str("NULL"),
1768 Some(n) => out.push_str(&n.to_string()),
1769 }
1770 }
1771 out.push('}');
1772 out
1773}
1774
1775pub fn format_bool_array(items: &[Option<bool>]) -> String {
1779 let mut out = String::with_capacity(2 + items.len() * 2);
1780 out.push('{');
1781 for (i, item) in items.iter().enumerate() {
1782 if i > 0 {
1783 out.push(',');
1784 }
1785 match item {
1786 None => out.push_str("NULL"),
1787 Some(b) => out.push(if *b { 't' } else { 'f' }),
1788 }
1789 }
1790 out.push('}');
1791 out
1792}
1793
1794pub fn format_smallint_array(items: &[Option<i16>]) -> String {
1796 let mut out = String::with_capacity(2 + items.len() * 4);
1797 out.push('{');
1798 for (i, item) in items.iter().enumerate() {
1799 if i > 0 {
1800 out.push(',');
1801 }
1802 match item {
1803 None => out.push_str("NULL"),
1804 Some(n) => out.push_str(&n.to_string()),
1805 }
1806 }
1807 out.push('}');
1808 out
1809}
1810
1811pub fn format_float_array(items: &[Option<f64>]) -> String {
1815 let mut out = String::with_capacity(2 + items.len() * 8);
1816 out.push('{');
1817 for (i, item) in items.iter().enumerate() {
1818 if i > 0 {
1819 out.push(',');
1820 }
1821 match item {
1822 None => out.push_str("NULL"),
1823 Some(x) => out.push_str(&format_float(*x)),
1826 }
1827 }
1828 out.push('}');
1829 out
1830}
1831
1832pub fn format_numeric_array(items: &[Option<(i128, u16)>]) -> String {
1834 let mut out = String::with_capacity(2 + items.len() * 6);
1835 out.push('{');
1836 for (i, item) in items.iter().enumerate() {
1837 if i > 0 {
1838 out.push(',');
1839 }
1840 match item {
1841 None => out.push_str("NULL"),
1842 Some((scaled, scale)) => out.push_str(&format_numeric(*scaled, *scale)),
1843 }
1844 }
1845 out.push('}');
1846 out
1847}
1848
1849pub fn format_date_array(items: &[Option<i32>]) -> String {
1852 let mut out = String::with_capacity(2 + items.len() * 12);
1853 out.push('{');
1854 for (i, item) in items.iter().enumerate() {
1855 if i > 0 {
1856 out.push(',');
1857 }
1858 match item {
1859 None => out.push_str("NULL"),
1860 Some(d) => out.push_str(&format_date(*d)),
1861 }
1862 }
1863 out.push('}');
1864 out
1865}
1866
1867pub fn format_timestamp_array(items: &[Option<i64>], with_tz: bool) -> String {
1873 let mut out = String::with_capacity(2 + items.len() * 22);
1874 out.push('{');
1875 for (i, item) in items.iter().enumerate() {
1876 if i > 0 {
1877 out.push(',');
1878 }
1879 match item {
1880 None => out.push_str("NULL"),
1881 Some(t) => {
1882 out.push('"');
1883 if with_tz {
1884 out.push_str(&format_timestamptz(*t));
1885 } else {
1886 out.push_str(&format_timestamp(*t));
1887 }
1888 out.push('"');
1889 }
1890 }
1891 }
1892 out.push('}');
1893 out
1894}
1895
1896pub fn format_uuid_array(items: &[Option<[u8; 16]>]) -> String {
1900 let mut out = String::with_capacity(2 + items.len() * 38);
1901 out.push('{');
1902 for (i, item) in items.iter().enumerate() {
1903 if i > 0 {
1904 out.push(',');
1905 }
1906 match item {
1907 None => out.push_str("NULL"),
1908 Some(b) => out.push_str(&spg_storage::format_uuid(b)),
1909 }
1910 }
1911 out.push('}');
1912 out
1913}
1914
1915pub fn format_bytea_array(items: &[Option<Vec<u8>>]) -> String {
1920 let mut out = String::with_capacity(2 + items.len() * 8);
1921 out.push('{');
1922 for (i, item) in items.iter().enumerate() {
1923 if i > 0 {
1924 out.push(',');
1925 }
1926 match item {
1927 None => out.push_str("NULL"),
1928 Some(b) => {
1929 out.push('"');
1930 let hex = format_bytea_hex(b);
1931 for c in hex.chars() {
1934 if c == '\\' {
1935 out.push('\\');
1936 }
1937 out.push(c);
1938 }
1939 out.push('"');
1940 }
1941 }
1942 }
1943 out.push('}');
1944 out
1945}
1946
1947pub fn format_interval_array(items: &[Option<spg_storage::IntervalSpan>]) -> String {
1954 let mut out = String::with_capacity(2 + items.len() * 12);
1955 out.push('{');
1956 for (i, item) in items.iter().enumerate() {
1957 if i > 0 {
1958 out.push(',');
1959 }
1960 match item {
1961 None => out.push_str("NULL"),
1962 Some(span) => {
1963 if span.kind.is_finite() {
1967 out.push('"');
1968 out.push_str(&format_interval(span.months, span.days, span.micros));
1969 out.push('"');
1970 } else {
1971 out.push_str(&format_interval_kinded(0, 0, 0, span.kind));
1972 }
1973 }
1974 }
1975 }
1976 out.push('}');
1977 out
1978}
1979
1980#[must_use]
1986pub fn format_bytea_escape(b: &[u8]) -> String {
1987 let mut out = String::with_capacity(b.len());
1988 for &byte in b {
1989 match byte {
1990 b'\\' => out.push_str("\\\\"),
1991 0x20..=0x7e => out.push(byte as char),
1992 _ => out.push_str(&alloc::format!("\\{byte:03o}")),
1993 }
1994 }
1995 out
1996}
1997
1998pub fn format_bytea_hex(b: &[u8]) -> String {
1999 let mut out = String::with_capacity(2 + 2 * b.len());
2000 out.push_str("\\x");
2001 const HEX: &[u8; 16] = b"0123456789abcdef";
2002 for byte in b {
2003 out.push(HEX[(byte >> 4) as usize] as char);
2004 out.push(HEX[(byte & 0x0F) as usize] as char);
2005 }
2006 out
2007}
2008
2009pub fn format_numeric_kind(kind: spg_storage::NumericKind, scaled: i128, scale: u16) -> String {
2016 use spg_storage::NumericKind;
2017 match kind {
2018 NumericKind::Finite => format_numeric(scaled, scale),
2019 NumericKind::NaN => String::from("NaN"),
2020 NumericKind::PosInf => String::from("Infinity"),
2021 NumericKind::NegInf => String::from("-Infinity"),
2022 }
2023}
2024
2025pub fn format_numeric(scaled: i128, scale: u16) -> String {
2026 if scale == 0 {
2027 return format!("{scaled}");
2028 }
2029 let negative = scaled < 0;
2030 let mag_str = scaled.unsigned_abs().to_string();
2031 let mag_bytes = mag_str.as_bytes();
2032 let scale_u = scale as usize;
2033 let mut out = String::with_capacity(mag_str.len() + 3);
2034 if negative {
2035 out.push('-');
2036 }
2037 if mag_bytes.len() <= scale_u {
2038 out.push('0');
2039 out.push('.');
2040 for _ in mag_bytes.len()..scale_u {
2041 out.push('0');
2042 }
2043 out.push_str(&mag_str);
2044 } else {
2045 let split = mag_bytes.len() - scale_u;
2046 out.push_str(&mag_str[..split]);
2047 out.push('.');
2048 out.push_str(&mag_str[split..]);
2049 }
2050 out
2051}
2052
2053fn shortest_float_sci(x: f64) -> String {
2071 let (m, e) = f64_mantissa_exp(x);
2072 let (hi_m, hi_e) = (2 * m + 1, e - 1);
2075 let (lo_m, lo_e) = if m == 1 << 52 && e > f64_min_exp() {
2076 (4 * m - 1, e - 2)
2077 } else {
2078 (2 * m - 1, e - 1)
2079 };
2080 for p in 1..=17u32 {
2081 let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
2082 let Ok(v) = cand.parse::<f64>() else { continue };
2083 if v != x {
2084 continue;
2085 }
2086 let Some((d, k)) = sci_to_digits_exp(&cand) else {
2087 continue;
2088 };
2089 if !decimal_eq_binary(d, k, hi_m, hi_e) && !decimal_eq_binary(d, k, lo_m, lo_e) {
2090 return cand;
2091 }
2092 }
2093 alloc::format!("{x:e}")
2094}
2095
2096fn f64_mantissa_exp(x: f64) -> (u128, i32) {
2099 let bits = x.abs().to_bits();
2100 let biased = ((bits >> 52) & 0x7ff) as i32;
2101 let frac = u128::from(bits & 0x000f_ffff_ffff_ffff);
2102 if biased == 0 {
2103 (frac, -1074) } else {
2105 ((1u128 << 52) | frac, biased - 1075)
2106 }
2107}
2108
2109const fn f64_min_exp() -> i32 {
2112 -1074
2113}
2114
2115fn sci_to_digits_exp(sci: &str) -> Option<(u128, i32)> {
2119 let epos = sci.find('e')?;
2120 let (mant, rest) = sci.split_at(epos);
2121 let exp: i32 = rest[1..].parse().ok()?;
2122 let mant = mant.strip_prefix('-').unwrap_or(mant);
2123 let (int_part, frac_part) = match mant.split_once('.') {
2124 Some((a, b)) => (a, b),
2125 None => (mant, ""),
2126 };
2127 let mut digits: u128 = 0;
2128 for c in int_part.chars().chain(frac_part.chars()) {
2129 digits = digits
2130 .checked_mul(10)?
2131 .checked_add(u128::from(c as u8 - b'0'))?;
2132 }
2133 Some((digits, exp - i32::try_from(frac_part.len()).ok()?))
2134}
2135
2136fn decimal_eq_binary(d: u128, k: i32, m: u128, e: i32) -> bool {
2142 if d == 0 {
2143 return false;
2144 }
2145 let a = i32::try_from(d.trailing_zeros()).unwrap_or(i32::MAX);
2146 let d_odd = d >> d.trailing_zeros();
2147 if k >= 0 {
2148 let mut lhs = d_odd;
2150 for _ in 0..k {
2151 match lhs.checked_mul(5) {
2152 Some(v) if v <= m => lhs = v,
2153 _ => return false,
2154 }
2155 }
2156 lhs == m && a + k == e
2157 } else {
2158 let j = -k;
2160 let mut lhs = d_odd;
2161 for _ in 0..j {
2162 if lhs % 5 != 0 {
2163 return false;
2164 }
2165 lhs /= 5;
2166 }
2167 lhs == m && a - j == e
2168 }
2169}