1use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15
16use super::civil_from_days;
17
18pub fn format_date(days: i32) -> String {
21 let (y, m, d) = civil_from_days(days);
22 format!("{y:04}-{m:02}-{d:02}")
23}
24
25pub fn format_timestamptz(micros: i64) -> String {
36 let base = format_timestamp(micros);
37 let mut s = String::with_capacity(base.len() + 3);
38 s.push_str(&base);
39 s.push_str("+00");
40 s
41}
42
43pub fn format_money(cents: i64) -> String {
47 let neg = cents < 0;
48 let abs = cents.unsigned_abs();
49 let dollars = abs / 100;
50 let cc = abs % 100;
51 let dollar_str = dollars.to_string();
53 let bytes = dollar_str.as_bytes();
54 let mut int_part = String::with_capacity(dollar_str.len() + dollar_str.len() / 3);
55 for (i, b) in bytes.iter().enumerate() {
56 let from_right = bytes.len() - i;
59 if i > 0 && from_right % 3 == 0 {
60 int_part.push(',');
61 }
62 int_part.push(*b as char);
63 }
64 let sign = if neg { "-" } else { "" };
65 format!("{sign}${int_part}.{cc:02}")
66}
67
68pub fn format_timetz(us: i64, offset_secs: i32) -> String {
73 let time = format_time(us);
74 let sign = if offset_secs < 0 { '-' } else { '+' };
75 let abs = offset_secs.unsigned_abs();
76 let oh = abs / 3600;
77 let om = (abs % 3600) / 60;
78 if om == 0 {
79 format!("{time}{sign}{oh:02}")
80 } else {
81 format!("{time}{sign}{oh:02}:{om:02}")
82 }
83}
84
85pub fn format_time(us: i64) -> String {
90 let total_secs = us.div_euclid(1_000_000);
91 let frac = us.rem_euclid(1_000_000);
92 let hh = total_secs / 3600;
93 let mm = (total_secs / 60) % 60;
94 let ss = total_secs % 60;
95 if frac == 0 {
96 format!("{hh:02}:{mm:02}:{ss:02}")
97 } else {
98 let raw = format!("{frac:06}");
99 let trimmed = raw.trim_end_matches('0');
100 format!("{hh:02}:{mm:02}:{ss:02}.{trimmed}")
101 }
102}
103
104pub fn format_timestamp(micros: i64) -> String {
105 const MICROS_PER_DAY: i64 = 86_400_000_000;
106 let days = micros.div_euclid(MICROS_PER_DAY);
109 let day_micros = micros.rem_euclid(MICROS_PER_DAY);
110 let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
111 let (y, m, d) = civil_from_days(day_i32);
112 let secs = day_micros / 1_000_000;
113 let frac = day_micros % 1_000_000;
114 let hh = secs / 3600;
115 let mm = (secs / 60) % 60;
116 let ss = secs % 60;
117 if frac == 0 {
118 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}")
119 } else {
120 let raw = format!("{frac:06}");
122 let trimmed = raw.trim_end_matches('0');
123 format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}.{trimmed}")
124 }
125}
126
127#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
130pub fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
131 let y_adj = if m <= 2 {
132 i64::from(y) - 1
133 } else {
134 i64::from(y)
135 };
136 let era = y_adj.div_euclid(400);
137 let yoe = (y_adj - era * 400) as u32;
138 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.saturating_sub(1);
139 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
140 let total = era * 146_097 + i64::from(doe) - 719_468;
141 i32::try_from(total).unwrap_or(i32::MAX)
142}
143
144pub fn parse_date_literal(s: &str) -> Option<i32> {
148 let bytes = s.as_bytes();
149 if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
150 return None;
151 }
152 let y: i32 = s[0..4].parse().ok()?;
153 let m: u32 = s[5..7].parse().ok()?;
154 let d: u32 = s[8..10].parse().ok()?;
155 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
156 return None;
157 }
158 Some(days_from_civil(y, m, d))
159}
160
161pub fn parse_timestamp_literal(s: &str) -> Option<i64> {
166 let trimmed = s.trim();
167 let (date_part, time_part) = match trimmed.find([' ', 'T']) {
168 Some(i) => (&trimmed[..i], Some(&trimmed[i + 1..])),
169 None => (trimmed, None),
170 };
171 let days = parse_date_literal(date_part)?;
172 let (day_micros, tz_offset_micros) = match time_part {
173 None => (0, 0),
174 Some(t) => parse_time_of_day_micros(t)?,
175 };
176 Some(i64::from(days) * 86_400_000_000 + day_micros - tz_offset_micros)
186}
187
188fn parse_time_of_day_micros(t: &str) -> Option<(i64, i64)> {
201 let t = t.trim();
202 let (core, tz_micros) = if let Some(rest) = t.strip_suffix('Z') {
208 (rest, 0i64)
209 } else if let Some(rest) = t.strip_suffix(" UTC").or_else(|| t.strip_suffix("UTC")) {
210 (rest, 0i64)
211 } else if let Some((idx, sign_byte)) = find_offset_sign(t) {
212 let suffix = &t[idx..];
213 let micros = parse_tz_offset_suffix(suffix, sign_byte == b'+')?;
214 (&t[..idx], micros)
215 } else {
216 (t, 0i64)
217 };
218 let (time, frac_str) = match core.split_once('.') {
219 Some((a, b)) => (a, Some(b)),
220 None => (core, None),
221 };
222 let bytes = time.as_bytes();
223 if bytes.len() != 8 || bytes[2] != b':' || bytes[5] != b':' {
224 return None;
225 }
226 let hh: i64 = time[0..2].parse().ok()?;
227 let mm: i64 = time[3..5].parse().ok()?;
228 let ss: i64 = time[6..8].parse().ok()?;
229 if !(0..24).contains(&hh) || !(0..60).contains(&mm) || !(0..60).contains(&ss) {
230 return None;
231 }
232 let frac_micros: i64 = match frac_str {
233 None => 0,
234 Some(f) => {
235 if f.is_empty() || f.len() > 9 {
237 return None;
238 }
239 let mut padded = String::with_capacity(6);
240 padded.push_str(&f[..f.len().min(6)]);
241 while padded.len() < 6 {
242 padded.push('0');
243 }
244 padded.parse().ok()?
245 }
246 };
247 Some((
248 ((hh * 3600 + mm * 60 + ss) * 1_000_000) + frac_micros,
249 tz_micros,
250 ))
251}
252
253fn find_offset_sign(t: &str) -> Option<(usize, u8)> {
259 let bytes = t.as_bytes();
260 if bytes.len() < 9 {
262 return None;
263 }
264 for i in 8..bytes.len() {
265 match bytes[i] {
266 b'+' | b'-' => return Some((i, bytes[i])),
267 _ => {}
268 }
269 }
270 None
271}
272
273fn parse_tz_offset_suffix(suffix: &str, is_positive: bool) -> Option<i64> {
277 let body = &suffix[1..];
279 let (hh, mm): (i64, i64) = if let Some((h, m)) = body.split_once(':') {
280 (h.parse().ok()?, m.parse().ok()?)
281 } else {
282 match body.len() {
283 2 => (body.parse().ok()?, 0),
284 3 => {
285 return None;
289 }
290 4 => {
291 let h: i64 = body[0..2].parse().ok()?;
292 let m: i64 = body[2..4].parse().ok()?;
293 (h, m)
294 }
295 _ => return None,
296 }
297 };
298 if !(0..=18).contains(&hh) || !(0..60).contains(&mm) {
299 return None;
300 }
301 let abs = (hh * 3600 + mm * 60) * 1_000_000;
302 Some(if is_positive { abs } else { -abs })
303}
304
305pub fn format_interval(months: i32, days: i32, micros: i64) -> String {
313 let mut parts: Vec<String> = Vec::new();
314 let years = months / 12;
315 let mons = months % 12;
316 let unit = |n: i64, singular: &'static str, plural: &'static str| -> &'static str {
319 if n == 1 { singular } else { plural }
320 };
321 if years != 0 {
322 parts.push(format!(
323 "{years} {}",
324 unit(i64::from(years), "year", "years")
325 ));
326 }
327 if mons != 0 {
328 parts.push(format!("{mons} {}", unit(i64::from(mons), "mon", "mons")));
329 }
330 if days != 0 {
331 parts.push(format!("{days} {}", unit(i64::from(days), "day", "days")));
332 }
333 let mut rem = micros;
334 if rem != 0 {
335 let neg = rem < 0;
336 if neg {
337 rem = -rem;
338 }
339 let secs = rem / 1_000_000;
340 let frac = rem % 1_000_000;
341 let hh = secs / 3600;
342 let mm = (secs / 60) % 60;
343 let ss = secs % 60;
344 let sign = if neg { "-" } else { "" };
345 if frac == 0 {
346 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}"));
347 } else {
348 let raw = format!("{frac:06}");
349 let trimmed = raw.trim_end_matches('0');
350 parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}.{trimmed}"));
351 }
352 }
353 if parts.is_empty() {
354 "0".into()
355 } else {
356 parts.join(" ")
357 }
358}
359
360pub fn format_text_array(items: &[Option<String>]) -> String {
366 let mut out = String::with_capacity(2 + items.len() * 8);
367 out.push('{');
368 for (i, item) in items.iter().enumerate() {
369 if i > 0 {
370 out.push(',');
371 }
372 match item {
373 None => out.push_str("NULL"),
374 Some(s) => {
375 let needs_quote = s.is_empty()
376 || s.eq_ignore_ascii_case("NULL")
377 || s.chars()
378 .any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\' | ' ' | '\t'));
379 if needs_quote {
380 out.push('"');
381 for c in s.chars() {
382 if c == '"' || c == '\\' {
383 out.push('\\');
384 }
385 out.push(c);
386 }
387 out.push('"');
388 } else {
389 out.push_str(s);
390 }
391 }
392 }
393 }
394 out.push('}');
395 out
396}
397
398pub fn format_int_array(items: &[Option<i32>]) -> String {
402 let mut out = String::with_capacity(2 + items.len() * 4);
403 out.push('{');
404 for (i, item) in items.iter().enumerate() {
405 if i > 0 {
406 out.push(',');
407 }
408 match item {
409 None => out.push_str("NULL"),
410 Some(n) => out.push_str(&n.to_string()),
411 }
412 }
413 out.push('}');
414 out
415}
416
417pub fn format_bigint_array(items: &[Option<i64>]) -> String {
420 let mut out = String::with_capacity(2 + items.len() * 6);
421 out.push('{');
422 for (i, item) in items.iter().enumerate() {
423 if i > 0 {
424 out.push(',');
425 }
426 match item {
427 None => out.push_str("NULL"),
428 Some(n) => out.push_str(&n.to_string()),
429 }
430 }
431 out.push('}');
432 out
433}
434
435pub fn format_bool_array(items: &[Option<bool>]) -> String {
439 let mut out = String::with_capacity(2 + items.len() * 2);
440 out.push('{');
441 for (i, item) in items.iter().enumerate() {
442 if i > 0 {
443 out.push(',');
444 }
445 match item {
446 None => out.push_str("NULL"),
447 Some(b) => out.push(if *b { 't' } else { 'f' }),
448 }
449 }
450 out.push('}');
451 out
452}
453
454pub fn format_smallint_array(items: &[Option<i16>]) -> String {
456 let mut out = String::with_capacity(2 + items.len() * 4);
457 out.push('{');
458 for (i, item) in items.iter().enumerate() {
459 if i > 0 {
460 out.push(',');
461 }
462 match item {
463 None => out.push_str("NULL"),
464 Some(n) => out.push_str(&n.to_string()),
465 }
466 }
467 out.push('}');
468 out
469}
470
471pub fn format_float_array(items: &[Option<f64>]) -> String {
475 let mut out = String::with_capacity(2 + items.len() * 8);
476 out.push('{');
477 for (i, item) in items.iter().enumerate() {
478 if i > 0 {
479 out.push(',');
480 }
481 match item {
482 None => out.push_str("NULL"),
483 Some(x) => out.push_str(&x.to_string()),
484 }
485 }
486 out.push('}');
487 out
488}
489
490pub fn format_numeric_array(items: &[Option<(i128, u8)>]) -> String {
492 let mut out = String::with_capacity(2 + items.len() * 6);
493 out.push('{');
494 for (i, item) in items.iter().enumerate() {
495 if i > 0 {
496 out.push(',');
497 }
498 match item {
499 None => out.push_str("NULL"),
500 Some((scaled, scale)) => out.push_str(&format_numeric(*scaled, *scale)),
501 }
502 }
503 out.push('}');
504 out
505}
506
507pub fn format_date_array(items: &[Option<i32>]) -> String {
510 let mut out = String::with_capacity(2 + items.len() * 12);
511 out.push('{');
512 for (i, item) in items.iter().enumerate() {
513 if i > 0 {
514 out.push(',');
515 }
516 match item {
517 None => out.push_str("NULL"),
518 Some(d) => out.push_str(&format_date(*d)),
519 }
520 }
521 out.push('}');
522 out
523}
524
525pub fn format_timestamp_array(items: &[Option<i64>], with_tz: bool) -> String {
531 let mut out = String::with_capacity(2 + items.len() * 22);
532 out.push('{');
533 for (i, item) in items.iter().enumerate() {
534 if i > 0 {
535 out.push(',');
536 }
537 match item {
538 None => out.push_str("NULL"),
539 Some(t) => {
540 out.push('"');
541 if with_tz {
542 out.push_str(&format_timestamptz(*t));
543 } else {
544 out.push_str(&format_timestamp(*t));
545 }
546 out.push('"');
547 }
548 }
549 }
550 out.push('}');
551 out
552}
553
554pub fn format_uuid_array(items: &[Option<[u8; 16]>]) -> String {
558 let mut out = String::with_capacity(2 + items.len() * 38);
559 out.push('{');
560 for (i, item) in items.iter().enumerate() {
561 if i > 0 {
562 out.push(',');
563 }
564 match item {
565 None => out.push_str("NULL"),
566 Some(b) => out.push_str(&spg_storage::format_uuid(b)),
567 }
568 }
569 out.push('}');
570 out
571}
572
573pub fn format_bytea_array(items: &[Option<Vec<u8>>]) -> String {
578 let mut out = String::with_capacity(2 + items.len() * 8);
579 out.push('{');
580 for (i, item) in items.iter().enumerate() {
581 if i > 0 {
582 out.push(',');
583 }
584 match item {
585 None => out.push_str("NULL"),
586 Some(b) => {
587 out.push('"');
588 let hex = format_bytea_hex(b);
589 for c in hex.chars() {
592 if c == '\\' {
593 out.push('\\');
594 }
595 out.push(c);
596 }
597 out.push('"');
598 }
599 }
600 }
601 out.push('}');
602 out
603}
604
605pub fn format_interval_array(items: &[Option<spg_storage::IntervalSpan>]) -> String {
612 let mut out = String::with_capacity(2 + items.len() * 12);
613 out.push('{');
614 for (i, item) in items.iter().enumerate() {
615 if i > 0 {
616 out.push(',');
617 }
618 match item {
619 None => out.push_str("NULL"),
620 Some(span) => {
621 out.push('"');
622 out.push_str(&format_interval(span.months, span.days, span.micros));
623 out.push('"');
624 }
625 }
626 }
627 out.push('}');
628 out
629}
630
631pub fn format_bytea_hex(b: &[u8]) -> String {
635 let mut out = String::with_capacity(2 + 2 * b.len());
636 out.push_str("\\x");
637 const HEX: &[u8; 16] = b"0123456789abcdef";
638 for byte in b {
639 out.push(HEX[(byte >> 4) as usize] as char);
640 out.push(HEX[(byte & 0x0F) as usize] as char);
641 }
642 out
643}
644
645pub fn format_numeric(scaled: i128, scale: u8) -> String {
650 if scale == 0 {
651 return format!("{scaled}");
652 }
653 let negative = scaled < 0;
654 let mag_str = scaled.unsigned_abs().to_string();
655 let mag_bytes = mag_str.as_bytes();
656 let scale_u = scale as usize;
657 let mut out = String::with_capacity(mag_str.len() + 3);
658 if negative {
659 out.push('-');
660 }
661 if mag_bytes.len() <= scale_u {
662 out.push('0');
663 out.push('.');
664 for _ in mag_bytes.len()..scale_u {
665 out.push('0');
666 }
667 out.push_str(&mag_str);
668 } else {
669 let split = mag_bytes.len() - scale_u;
670 out.push_str(&mag_str[..split]);
671 out.push('.');
672 out.push_str(&mag_str[split..]);
673 }
674 out
675}