use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::civil_from_days;
pub fn format_date(days: i32) -> String {
let (y, m, d) = civil_from_days(days);
format!("{y:04}-{m:02}-{d:02}")
}
pub fn format_timestamptz(micros: i64) -> String {
let base = format_timestamp(micros);
let mut s = String::with_capacity(base.len() + 3);
s.push_str(&base);
s.push_str("+00");
s
}
pub fn format_money(cents: i64) -> String {
let neg = cents < 0;
let abs = cents.unsigned_abs();
let dollars = abs / 100;
let cc = abs % 100;
let dollar_str = dollars.to_string();
let bytes = dollar_str.as_bytes();
let mut int_part = String::with_capacity(dollar_str.len() + dollar_str.len() / 3);
for (i, b) in bytes.iter().enumerate() {
let from_right = bytes.len() - i;
if i > 0 && from_right % 3 == 0 {
int_part.push(',');
}
int_part.push(*b as char);
}
let sign = if neg { "-" } else { "" };
format!("{sign}${int_part}.{cc:02}")
}
pub fn format_timetz(us: i64, offset_secs: i32) -> String {
let time = format_time(us);
let sign = if offset_secs < 0 { '-' } else { '+' };
let abs = offset_secs.unsigned_abs();
let oh = abs / 3600;
let om = (abs % 3600) / 60;
if om == 0 {
format!("{time}{sign}{oh:02}")
} else {
format!("{time}{sign}{oh:02}:{om:02}")
}
}
pub fn format_time(us: i64) -> String {
let total_secs = us.div_euclid(1_000_000);
let frac = us.rem_euclid(1_000_000);
let hh = total_secs / 3600;
let mm = (total_secs / 60) % 60;
let ss = total_secs % 60;
if frac == 0 {
format!("{hh:02}:{mm:02}:{ss:02}")
} else {
let raw = format!("{frac:06}");
let trimmed = raw.trim_end_matches('0');
format!("{hh:02}:{mm:02}:{ss:02}.{trimmed}")
}
}
pub fn format_timestamp(micros: i64) -> String {
const MICROS_PER_DAY: i64 = 86_400_000_000;
let days = micros.div_euclid(MICROS_PER_DAY);
let day_micros = micros.rem_euclid(MICROS_PER_DAY);
let day_i32 = i32::try_from(days).unwrap_or(i32::MAX);
let (y, m, d) = civil_from_days(day_i32);
let secs = day_micros / 1_000_000;
let frac = day_micros % 1_000_000;
let hh = secs / 3600;
let mm = (secs / 60) % 60;
let ss = secs % 60;
if frac == 0 {
format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}")
} else {
let raw = format!("{frac:06}");
let trimmed = raw.trim_end_matches('0');
format!("{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02}.{trimmed}")
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn days_from_civil(y: i32, m: u32, d: u32) -> i32 {
let y_adj = if m <= 2 {
i64::from(y) - 1
} else {
i64::from(y)
};
let era = y_adj.div_euclid(400);
let yoe = (y_adj - era * 400) as u32;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.saturating_sub(1);
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let total = era * 146_097 + i64::from(doe) - 719_468;
i32::try_from(total).unwrap_or(i32::MAX)
}
pub fn parse_date_literal(s: &str) -> Option<i32> {
let bytes = s.as_bytes();
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
return None;
}
let y: i32 = s[0..4].parse().ok()?;
let m: u32 = s[5..7].parse().ok()?;
let d: u32 = s[8..10].parse().ok()?;
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
Some(days_from_civil(y, m, d))
}
pub fn parse_timestamp_literal(s: &str) -> Option<i64> {
let trimmed = s.trim();
let (date_part, time_part) = match trimmed.find([' ', 'T']) {
Some(i) => (&trimmed[..i], Some(&trimmed[i + 1..])),
None => (trimmed, None),
};
let days = parse_date_literal(date_part)?;
let (day_micros, tz_offset_micros) = match time_part {
None => (0, 0),
Some(t) => parse_time_of_day_micros(t)?,
};
Some(i64::from(days) * 86_400_000_000 + day_micros - tz_offset_micros)
}
fn parse_time_of_day_micros(t: &str) -> Option<(i64, i64)> {
let t = t.trim();
let (core, tz_micros) = if let Some(rest) = t.strip_suffix('Z') {
(rest, 0i64)
} else if let Some(rest) = t.strip_suffix(" UTC").or_else(|| t.strip_suffix("UTC")) {
(rest, 0i64)
} else if let Some((idx, sign_byte)) = find_offset_sign(t) {
let suffix = &t[idx..];
let micros = parse_tz_offset_suffix(suffix, sign_byte == b'+')?;
(&t[..idx], micros)
} else {
(t, 0i64)
};
let (time, frac_str) = match core.split_once('.') {
Some((a, b)) => (a, Some(b)),
None => (core, None),
};
let bytes = time.as_bytes();
if bytes.len() != 8 || bytes[2] != b':' || bytes[5] != b':' {
return None;
}
let hh: i64 = time[0..2].parse().ok()?;
let mm: i64 = time[3..5].parse().ok()?;
let ss: i64 = time[6..8].parse().ok()?;
if !(0..24).contains(&hh) || !(0..60).contains(&mm) || !(0..60).contains(&ss) {
return None;
}
let frac_micros: i64 = match frac_str {
None => 0,
Some(f) => {
if f.is_empty() || f.len() > 9 {
return None;
}
let mut padded = String::with_capacity(6);
padded.push_str(&f[..f.len().min(6)]);
while padded.len() < 6 {
padded.push('0');
}
padded.parse().ok()?
}
};
Some((
((hh * 3600 + mm * 60 + ss) * 1_000_000) + frac_micros,
tz_micros,
))
}
fn find_offset_sign(t: &str) -> Option<(usize, u8)> {
let bytes = t.as_bytes();
if bytes.len() < 9 {
return None;
}
for i in 8..bytes.len() {
match bytes[i] {
b'+' | b'-' => return Some((i, bytes[i])),
_ => {}
}
}
None
}
fn parse_tz_offset_suffix(suffix: &str, is_positive: bool) -> Option<i64> {
let body = &suffix[1..];
let (hh, mm): (i64, i64) = if let Some((h, m)) = body.split_once(':') {
(h.parse().ok()?, m.parse().ok()?)
} else {
match body.len() {
2 => (body.parse().ok()?, 0),
3 => {
return None;
}
4 => {
let h: i64 = body[0..2].parse().ok()?;
let m: i64 = body[2..4].parse().ok()?;
(h, m)
}
_ => return None,
}
};
if !(0..=18).contains(&hh) || !(0..60).contains(&mm) {
return None;
}
let abs = (hh * 3600 + mm * 60) * 1_000_000;
Some(if is_positive { abs } else { -abs })
}
pub fn format_interval(months: i32, micros: i64) -> String {
const MICROS_PER_DAY: i64 = 86_400_000_000;
let mut parts: Vec<String> = Vec::new();
let years = months / 12;
let mons = months % 12;
let unit = |n: i64, singular: &'static str, plural: &'static str| -> &'static str {
if n == 1 { singular } else { plural }
};
if years != 0 {
parts.push(format!(
"{years} {}",
unit(i64::from(years), "year", "years")
));
}
if mons != 0 {
parts.push(format!("{mons} {}", unit(i64::from(mons), "mon", "mons")));
}
let days = micros / MICROS_PER_DAY;
let mut rem = micros % MICROS_PER_DAY;
if days != 0 {
parts.push(format!("{days} {}", unit(days, "day", "days")));
}
if rem != 0 {
let neg = rem < 0;
if neg {
rem = -rem;
}
let secs = rem / 1_000_000;
let frac = rem % 1_000_000;
let hh = secs / 3600;
let mm = (secs / 60) % 60;
let ss = secs % 60;
let sign = if neg { "-" } else { "" };
if frac == 0 {
parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}"));
} else {
let raw = format!("{frac:06}");
let trimmed = raw.trim_end_matches('0');
parts.push(format!("{sign}{hh:02}:{mm:02}:{ss:02}.{trimmed}"));
}
}
if parts.is_empty() {
"0".into()
} else {
parts.join(" ")
}
}
pub fn format_text_array(items: &[Option<String>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 8);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(s) => {
let needs_quote = s.is_empty()
|| s.eq_ignore_ascii_case("NULL")
|| s.chars()
.any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\' | ' ' | '\t'));
if needs_quote {
out.push('"');
for c in s.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
out.push('"');
} else {
out.push_str(s);
}
}
}
}
out.push('}');
out
}
pub fn format_int_array(items: &[Option<i32>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 4);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(n) => out.push_str(&n.to_string()),
}
}
out.push('}');
out
}
pub fn format_bigint_array(items: &[Option<i64>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 6);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(n) => out.push_str(&n.to_string()),
}
}
out.push('}');
out
}
pub fn format_bytea_hex(b: &[u8]) -> String {
let mut out = String::with_capacity(2 + 2 * b.len());
out.push_str("\\x");
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in b {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0F) as usize] as char);
}
out
}
pub fn format_numeric(scaled: i128, scale: u8) -> String {
if scale == 0 {
return format!("{scaled}");
}
let negative = scaled < 0;
let mag_str = scaled.unsigned_abs().to_string();
let mag_bytes = mag_str.as_bytes();
let scale_u = scale as usize;
let mut out = String::with_capacity(mag_str.len() + 3);
if negative {
out.push('-');
}
if mag_bytes.len() <= scale_u {
out.push('0');
out.push('.');
for _ in mag_bytes.len()..scale_u {
out.push('0');
}
out.push_str(&mag_str);
} else {
let split = mag_bytes.len() - scale_u;
out.push_str(&mag_str[..split]);
out.push('.');
out.push_str(&mag_str[split..]);
}
out
}