use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::{MONTH_ABBR, MONTH_FULL, civil_from_days};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DateOrder {
Mdy,
Dmy,
Ymd,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DateStyleKind {
Iso,
German,
Sql,
Postgres,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum IntervalStyleKind {
Postgres,
SqlStandard,
Iso8601,
PostgresVerbose,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct RenderStyle {
pub date_style: DateStyleKind,
pub date_order: DateOrder,
pub interval_style: IntervalStyleKind,
pub extra_float_digits: i32,
pub bytea_escape: bool,
pub mysql: bool,
}
impl Default for RenderStyle {
fn default() -> Self {
Self {
date_style: DateStyleKind::Iso,
date_order: DateOrder::Mdy,
interval_style: IntervalStyleKind::Postgres,
extra_float_digits: 1,
bytea_escape: false,
mysql: false,
}
}
}
fn dow_abbr(days: i32) -> &'static str {
const DOW: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
DOW[((days.rem_euclid(7)) as usize + 3) % 7]
}
fn hms_from_day_micros(day_micros: i64) -> String {
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!("{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_date_styled(days: i32, style: &RenderStyle) -> String {
if days == i32::MAX {
return "infinity".into();
}
if days == i32::MIN {
return "-infinity".into();
}
let (y, m, d) = civil_from_days(days);
let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
let dmy = style.date_order == DateOrder::Dmy;
match style.date_style {
DateStyleKind::Iso => format!("{y:04}-{m:02}-{d:02}{bc}"),
DateStyleKind::German => format!("{d:02}.{m:02}.{y:04}{bc}"),
DateStyleKind::Sql => {
if dmy {
format!("{d:02}/{m:02}/{y:04}{bc}")
} else {
format!("{m:02}/{d:02}/{y:04}{bc}")
}
}
DateStyleKind::Postgres => {
if dmy {
format!("{d:02}-{m:02}-{y:04}{bc}")
} else {
format!("{m:02}-{d:02}-{y:04}{bc}")
}
}
}
}
pub fn format_timestamp_styled(micros: i64, style: &RenderStyle) -> String {
if micros == i64::MAX {
return "infinity".into();
}
if micros == i64::MIN {
return "-infinity".into();
}
if style.date_style == DateStyleKind::Iso {
return format_timestamp(micros);
}
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 hms = hms_from_day_micros(day_micros);
match style.date_style {
DateStyleKind::Iso => unreachable!("handled above"),
DateStyleKind::German | DateStyleKind::Sql => {
let d = format_date_styled(day_i32, style);
match d.strip_suffix(" BC") {
Some(base) => format!("{base} {hms} BC"),
None => format!("{d} {hms}"),
}
}
DateStyleKind::Postgres => {
let (y, m, d) = civil_from_days(day_i32);
let (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
let mon = MONTH_ABBR[(m as usize).saturating_sub(1).min(11)];
let dow = dow_abbr(day_i32);
if style.date_order == DateOrder::Dmy {
format!("{dow} {d} {mon} {hms} {y:04}{bc}")
} else {
format!("{dow} {mon} {d} {hms} {y:04}{bc}")
}
}
}
}
pub fn format_timestamptz_styled(micros: i64, style: &RenderStyle) -> String {
format_timestamptz_tz(micros, style, 0, None)
}
pub fn format_timestamptz_tz(
micros: i64,
style: &RenderStyle,
offset_micros: i64,
abbr: Option<&str>,
) -> String {
if style.date_style == DateStyleKind::Iso {
return format_timestamptz_at(micros, offset_micros);
}
if micros == i64::MAX || micros == i64::MIN {
return format_timestamp(micros);
}
let body = format_timestamp_styled(micros + offset_micros, style);
match abbr {
Some(a) => format!("{body} {a}"),
None if offset_micros == 0 => format!("{body} UTC"),
None => {
let total_min = (offset_micros / 60_000_000).abs();
let (h, m) = (total_min / 60, total_min % 60);
let sign = if offset_micros < 0 { '-' } else { '+' };
if m == 0 {
format!("{body} {sign}{h:02}")
} else {
format!("{body} {sign}{h:02}:{m:02}")
}
}
}
}
fn sql_std_time(abs_us: i64) -> String {
let secs = abs_us / 1_000_000;
let frac = abs_us % 1_000_000;
let h = secs / 3600;
let mm = (secs / 60) % 60;
let ss = secs % 60;
if frac == 0 {
format!("{h}:{mm:02}:{ss:02}")
} else {
let raw = format!("{frac:06}");
let trimmed = raw.trim_end_matches('0');
format!("{h}:{mm:02}:{ss:02}.{trimmed}")
}
}
fn secs_body(abs_us: i64) -> String {
let ss = abs_us / 1_000_000;
let frac = abs_us % 1_000_000;
if frac == 0 {
format!("{ss}")
} else {
let raw = format!("{frac:06}");
let trimmed = raw.trim_end_matches('0');
format!("{ss}.{trimmed}")
}
}
pub fn format_interval_styled(months: i32, days: i32, micros: i64, style: &RenderStyle) -> String {
match style.interval_style {
IntervalStyleKind::Postgres => format_interval(months, days, micros),
IntervalStyleKind::SqlStandard => {
let has_ym = months != 0;
let has_dt = days != 0 || micros != 0;
if !has_ym && !has_dt {
return "0".into();
}
let y = months / 12;
let mo = (months % 12).abs();
let signs: Vec<i8> = [i64::from(months), i64::from(days), micros]
.iter()
.filter(|v| **v != 0)
.map(|v| if *v < 0 { -1i8 } else { 1 })
.collect();
let coherent = signs.windows(2).all(|w| w[0] == w[1]);
if has_ym && !has_dt && coherent {
return format!("{y}-{mo}");
}
if !has_ym && coherent {
let neg = days < 0 || micros < 0;
let time = sql_std_time(micros.abs());
if days == 0 {
return format!("{}{time}", if neg { "-" } else { "" });
}
return format!("{days} {time}");
}
let sgn = |neg: bool| if neg { '-' } else { '+' };
format!(
"{}{}-{} {}{} {}{}",
sgn(months < 0),
y.abs(),
mo,
sgn(days < 0),
days.abs(),
sgn(micros < 0),
sql_std_time(micros.abs())
)
}
IntervalStyleKind::Iso8601 => {
if months == 0 && days == 0 && micros == 0 {
return "PT0S".into();
}
let y = months / 12;
let mo = months % 12;
let mut out = String::from("P");
if y != 0 {
out.push_str(&format!("{y}Y"));
}
if mo != 0 {
out.push_str(&format!("{mo}M"));
}
if days != 0 {
out.push_str(&format!("{days}D"));
}
if micros != 0 {
out.push('T');
let neg = micros < 0;
let abs = micros.abs();
let h = abs / 3_600_000_000;
let m = (abs / 60_000_000) % 60;
let s_us = abs % 60_000_000;
let sgn = if neg { "-" } else { "" };
if h != 0 {
out.push_str(&format!("{sgn}{h}H"));
}
if m != 0 {
out.push_str(&format!("{sgn}{m}M"));
}
if s_us != 0 {
out.push_str(&format!("{sgn}{}S", secs_body(s_us)));
}
}
out
}
IntervalStyleKind::PostgresVerbose => {
if months == 0 && days == 0 && micros == 0 {
return "@ 0".into();
}
let total = i128::from(months) * 30 * 86_400_000_000
+ i128::from(days) * 86_400_000_000
+ i128::from(micros);
let ago = total < 0;
let (months, days, micros) = if ago {
(-months, -days, -micros)
} else {
(months, days, micros)
};
let y = months / 12;
let mo = months % 12;
let neg_t = micros < 0;
let abs = micros.abs();
let h = abs / 3_600_000_000;
let m = (abs / 60_000_000) % 60;
let s_us = abs % 60_000_000;
let mut parts: Vec<String> = Vec::new();
let unit = |n: i64, singular: &'static str| -> String {
if n == 1 {
singular.into()
} else {
format!("{singular}s")
}
};
if y != 0 {
parts.push(format!("{y} {}", unit(i64::from(y), "year")));
}
if mo != 0 {
parts.push(format!("{mo} {}", unit(i64::from(mo), "mon")));
}
if days != 0 {
parts.push(format!("{days} {}", unit(i64::from(days), "day")));
}
let tsgn = if neg_t { "-" } else { "" };
if h != 0 {
parts.push(format!("{tsgn}{h} {}", unit(h, "hour")));
}
if m != 0 {
parts.push(format!("{tsgn}{m} {}", unit(m, "min")));
}
if s_us != 0 {
let body = secs_body(s_us);
let plural = body != "1";
parts.push(format!(
"{tsgn}{body} {}",
if plural { "secs" } else { "sec" }
));
}
let mut out = String::from("@ ");
out.push_str(&parts.join(" "));
if ago {
out.push_str(" ago");
}
out
}
}
}
pub fn format_date_array_styled(items: &[Option<i32>], style: &RenderStyle) -> String {
array_styled(items, |d| format_date_styled(*d, style))
}
pub fn format_timestamp_array_styled(
items: &[Option<i64>],
with_tz: bool,
style: &RenderStyle,
) -> String {
if with_tz {
array_styled(items, |t| format_timestamptz_styled(*t, style))
} else {
array_styled(items, |t| format_timestamp_styled(*t, style))
}
}
pub fn format_interval_array_styled(
items: &[Option<spg_storage::IntervalSpan>],
style: &RenderStyle,
) -> String {
array_styled(items, |iv| {
format_interval_styled(iv.months, iv.days, iv.micros, style)
})
}
pub fn format_float_array_styled(items: &[Option<f64>], style: &RenderStyle) -> String {
array_styled(items, |f| format_float_styled(*f, style))
}
fn array_styled<T>(items: &[Option<T>], mut f: impl FnMut(&T) -> String) -> String {
let mut out = String::with_capacity(2 + items.len() * 12);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(v) => push_array_element(&mut out, &f(v)),
}
}
out.push('}');
out
}
fn push_array_element(out: &mut String, s: &str) {
let needs_quote = s.is_empty()
|| s.eq_ignore_ascii_case("null")
|| s.chars()
.any(|c| matches!(c, ',' | '{' | '}' | '"' | '\\') || c.is_whitespace());
if !needs_quote {
out.push_str(s);
return;
}
out.push('"');
for c in s.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
out.push('"');
}
fn format_g(x: f64, prec: usize) -> String {
let prec = prec.max(1);
let sci = format!("{:.*e}", prec - 1, x);
let epos = sci.find('e').expect("{:e} always has an 'e'");
let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
let mant = &sci[..epos];
if exp_val >= -4 && (exp_val as i64) < prec as i64 {
let decimals =
usize::try_from(i64::try_from(prec).unwrap_or(1) - 1 - i64::from(exp_val)).unwrap_or(0);
let rounded: f64 = sci.parse().unwrap_or(x);
let fixed = format!("{rounded:.decimals$}");
if fixed.contains('.') {
let t = fixed.trim_end_matches('0').trim_end_matches('.');
t.into()
} else {
fixed
}
} else {
let mant = if mant.contains('.') {
mant.trim_end_matches('0').trim_end_matches('.')
} else {
mant
};
let (sign, digits) = if exp_val < 0 {
('-', format!("{}", -exp_val))
} else {
('+', format!("{exp_val}"))
};
format!("{mant}e{sign}{digits:0>2}")
}
}
pub fn format_float_styled(x: f64, style: &RenderStyle) -> String {
if style.extra_float_digits >= 1 {
return format_float(x);
}
if x.is_nan() {
return "NaN".into();
}
if x.is_infinite() {
return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if x == 0.0 {
return if x.is_sign_negative() { "-0" } else { "0" }.into();
}
let prec = (15 + style.extra_float_digits).clamp(1, 17) as usize;
format_g(x, prec)
}
pub fn format_real_styled(x: f32, style: &RenderStyle) -> String {
if style.extra_float_digits >= 1 {
return format_real(x);
}
if x.is_nan() {
return "NaN".into();
}
if x.is_infinite() {
return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if x == 0.0 {
return if x.is_sign_negative() { "-0" } else { "0" }.into();
}
let prec = (6 + style.extra_float_digits).clamp(1, 9) as usize;
format_g(f64::from(x), prec)
}
pub fn format_date(days: i32) -> String {
if days == i32::MAX {
return "infinity".into();
}
if days == i32::MIN {
return "-infinity".into();
}
let (y, m, d) = civil_from_days(days);
if y <= 0 {
return format!("{:04}-{m:02}-{d:02} BC", 1 - y);
}
format!("{y:04}-{m:02}-{d:02}")
}
pub fn format_timestamptz(micros: i64) -> String {
format_timestamptz_at(micros, 0)
}
pub fn format_timestamptz_at(micros: i64, offset_micros: i64) -> String {
if micros == i64::MAX || micros == i64::MIN {
return format_timestamp(micros);
}
let base = format_timestamp(micros + offset_micros);
let (base, bc) = match base.strip_suffix(" BC") {
Some(b) => (String::from(b), " BC"),
None => (base, ""),
};
let mut s = String::with_capacity(base.len() + 9);
s.push_str(&base);
let total_min = (offset_micros / 60_000_000).abs();
let (h, m) = (total_min / 60, total_min % 60);
s.push(if offset_micros < 0 { '-' } else { '+' });
s.push_str(&alloc::format!("{h:02}"));
if m != 0 {
s.push(':');
s.push_str(&alloc::format!("{m:02}"));
}
s.push_str(bc);
s
}
pub fn format_float(x: f64) -> String {
if x.is_nan() {
return "NaN".into();
}
if x.is_infinite() {
return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if x == 0.0 {
return if x.is_sign_negative() { "-0" } else { "0" }.into();
}
let sci = shortest_float_sci(x); let epos = sci.find('e').expect("{:e} always has an 'e'");
let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
if (-4..=14).contains(&exp_val) {
return fixed_from_sci(&sci, exp_val);
}
let mant = &sci[..epos];
let exp = &sci[epos + 1..];
let (sign, digits) = match exp.strip_prefix('-') {
Some(d) => ('-', d),
None => ('+', exp),
};
alloc::format!("{mant}e{sign}{digits:0>2}")
}
fn shortest_real_sci(x: f32) -> String {
let wide = f64::from(x);
let below = f64::from(next_f32(x, false));
let above = f64::from(next_f32(x, true));
let lo = (wide + below) / 2.0;
let hi = (wide + above) / 2.0;
for p in 1..=9u32 {
let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
let Ok(v) = cand.parse::<f64>() else { continue };
#[allow(clippy::cast_possible_truncation)]
if v as f32 == x && v != lo && v != hi {
return cand;
}
}
alloc::format!("{x:e}")
}
fn next_f32(x: f32, up: bool) -> f32 {
let bits = x.to_bits();
let stepped = if (x > 0.0) == up { bits + 1 } else { bits - 1 };
f32::from_bits(stepped)
}
fn fixed_from_sci(sci: &str, exp: i32) -> String {
let epos = sci.find('e').expect("{:e} always has an 'e'");
let (mant, _) = sci.split_at(epos);
let (sign, mant) = match mant.strip_prefix('-') {
Some(m) => ("-", m),
None => ("", mant),
};
let digits: String = mant.chars().filter(char::is_ascii_digit).collect();
let point = exp + 1; let mut out = String::from(sign);
if point <= 0 {
out.push_str("0.");
for _ in 0..-point {
out.push('0');
}
out.push_str(&digits);
} else if (point as usize) >= digits.len() {
out.push_str(&digits);
for _ in 0..(point as usize - digits.len()) {
out.push('0');
}
} else {
out.push_str(&digits[..point as usize]);
out.push('.');
out.push_str(&digits[point as usize..]);
}
out
}
pub fn format_real(x: f32) -> String {
if x.is_nan() {
return "NaN".into();
}
if x.is_infinite() {
return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
}
if x == 0.0 {
return if x.is_sign_negative() { "-0" } else { "0" }.into();
}
let sci = shortest_real_sci(x);
let epos = sci.find('e').expect("{:e} always has an 'e'");
let exp_val: i32 = sci[epos + 1..].parse().unwrap_or(0);
if (-4..=5).contains(&exp_val) {
return fixed_from_sci(&sci, exp_val);
}
let mant = &sci[..epos];
let exp = &sci[epos + 1..];
let (sign, digits) = match exp.strip_prefix('-') {
Some(d) => ('-', d),
None => ('+', exp),
};
alloc::format!("{mant}e{sign}{digits:0>2}")
}
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 {
if micros == i64::MAX {
return "infinity".into();
}
if micros == i64::MIN {
return "-infinity".into();
}
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 (y, bc) = if y <= 0 { (1 - y, " BC") } else { (y, "") };
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}{bc}")
} 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}{bc}")
}
}
#[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> {
parse_date_literal_ordered(s, DateOrder::Mdy)
}
pub fn parse_date_literal_ordered(s: &str, order: DateOrder) -> Option<i32> {
let s = s.trim();
if let Some(base) = s
.strip_suffix(" BC")
.or_else(|| s.strip_suffix(" bc"))
.or_else(|| s.strip_suffix(" Bc"))
{
let days = parse_date_literal_ordered(base, order)?;
let (y, m, d) = civil_from_days(days);
if y < 1 {
return None;
}
return Some(days_from_civil(1 - y, m, d));
}
if let Some(base) = s.strip_suffix(" AD").or_else(|| s.strip_suffix(" ad")) {
return parse_date_literal_ordered(base, order);
}
if s.eq_ignore_ascii_case("epoch") {
return Some(days_from_civil(1970, 1, 1));
}
if s.eq_ignore_ascii_case("infinity") || s.eq_ignore_ascii_case("+infinity") {
return Some(i32::MAX);
}
if s.eq_ignore_ascii_case("-infinity") {
return Some(i32::MIN);
}
let bytes = s.as_bytes();
if bytes.len() == 8 && bytes.iter().all(u8::is_ascii_digit) {
let y: i32 = s[0..4].parse().ok()?;
let m: u32 = s[4..6].parse().ok()?;
let d: u32 = s[6..8].parse().ok()?;
if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
return None;
}
return Some(days_from_civil(y, m, d));
}
if s.bytes().any(|b| b.is_ascii_alphabetic()) {
if let Some(jd) = s.strip_prefix(['J', 'j'])
&& !jd.is_empty()
&& jd.bytes().all(|b| b.is_ascii_digit())
{
let jd: i64 = jd.parse().ok()?;
return i32::try_from(jd - 2_440_588).ok();
}
return parse_month_name_date(s, order);
}
{
let mut two = s.splitn(2, ['-', '/', '.']);
if let (Some(ya), Some(dd)) = (two.next(), two.next())
&& ya.len() >= 3
&& dd.len() == 3
&& !dd.contains(['-', '/', '.', ' '])
&& ya.bytes().all(|b| b.is_ascii_digit())
&& dd.bytes().all(|b| b.is_ascii_digit())
{
let y: i32 = ya.parse().ok()?;
let doy: i64 = dd.parse().ok()?;
if y != 0 && (1..=366).contains(&doy) {
let jan1 = days_from_civil(y, 1, 1);
let days = jan1 + i32::try_from(doy).ok()? - 1;
let (yy, _, _) = civil_from_days(days);
if yy == y {
return Some(days);
}
return None; }
}
}
let mut parts = s.splitn(3, ['-', '/', '.']);
let (fa, fb, fc) = (parts.next()?, parts.next()?, parts.next()?);
if fc.contains(['-', '/', '.', ' ']) {
return None; }
if [fa, fb, fc]
.iter()
.any(|p| p.is_empty() || !p.bytes().all(|b| b.is_ascii_digit()))
{
return None;
}
if fa.len() >= 3 && fb.len() <= 2 && fc.len() <= 2 {
let y: i32 = fa.parse().ok()?;
if y == 0 {
return None;
}
let m: u32 = fb.parse().ok()?;
let d: u32 = fc.parse().ok()?;
if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
return None;
}
return Some(days_from_civil(y, m, d));
}
let expand_year = |t: &str| -> Option<i32> {
match t.len() {
4 => t.parse().ok(),
1 | 2 => {
let n: i32 = t.parse().ok()?;
Some(if n < 70 { 2000 + n } else { 1900 + n })
}
_ => None,
}
};
let (ys, ms, ds) = match order {
DateOrder::Mdy => (fc, fa, fb),
DateOrder::Dmy => (fc, fb, fa),
DateOrder::Ymd => (fa, fb, fc),
};
if ms.len() > 2 || ds.len() > 2 {
return None;
}
let y = expand_year(ys)?;
let m: u32 = ms.parse().ok()?;
let d: u32 = ds.parse().ok()?;
if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
return None;
}
Some(days_from_civil(y, m, d))
}
fn parse_month_name_date(s: &str, order: DateOrder) -> Option<i32> {
let tokens: alloc::vec::Vec<&str> =
s.split([' ', ',', '-']).filter(|t| !t.is_empty()).collect();
if tokens.len() != 3 {
return None;
}
let month_of = |t: &str| -> Option<u32> {
let up = t.to_ascii_uppercase();
MONTH_ABBR
.iter()
.position(|a| a.eq_ignore_ascii_case(&up))
.or_else(|| MONTH_FULL.iter().position(|f| f.eq_ignore_ascii_case(&up)))
.map(|i| i as u32 + 1)
};
let mut month: Option<u32> = None;
let mut nums: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
for t in tokens {
if let Some(m) = month_of(t) {
if month.replace(m).is_some() {
return None; }
} else if t.bytes().all(|b| b.is_ascii_digit()) {
nums.push(t);
} else {
return None; }
}
let m = month?;
if nums.len() != 2 {
return None;
}
let (ys, ds) = match (nums[0].len() >= 3, nums[1].len() >= 3) {
(true, true) => return None,
(true, false) => (nums[0], nums[1]),
(false, true) => (nums[1], nums[0]),
(false, false) => {
if order == DateOrder::Ymd {
(nums[0], nums[1])
} else {
(nums[1], nums[0])
}
}
};
let mut y: i32 = ys.parse().ok()?;
if ys.len() <= 2 {
y += if y < 70 { 2000 } else { 1900 };
}
let d: u32 = ds.parse().ok()?;
if !(1..=12).contains(&m) || d < 1 || d > super::days_in_month(y, m) {
return None;
}
Some(days_from_civil(y, m, d))
}
pub fn parse_timestamp_literal(s: &str) -> Option<i64> {
parse_timestamp_literal_ordered(s, DateOrder::Mdy)
}
pub fn date_text_is_field_shaped(s: &str) -> bool {
let s = s.trim();
let date_part = match s.find([' ', 'T']) {
Some(i) => &s[..i],
None => s,
};
let b = date_part.as_bytes();
if b.len() == 8 && b.iter().all(u8::is_ascii_digit) {
return true;
}
let fields: alloc::vec::Vec<&str> = date_part.split(['-', '/', '.']).collect();
fields.len() == 3
&& fields
.iter()
.all(|f| !f.is_empty() && f.len() <= 4 && f.bytes().all(|c| c.is_ascii_digit()))
}
pub fn parse_timestamp_literal_ordered(s: &str, order: DateOrder) -> Option<i64> {
parse_timestamp_literal_tz_ordered(s, order).map(|(us, _)| us)
}
pub fn parse_timestamp_literal_tz_ordered(s: &str, order: DateOrder) -> Option<(i64, bool)> {
if let Some(v) = timestamp_sentinel(s) {
return Some((v, true));
}
let (days, day_micros, tz) = parse_timestamp_parts(s, order)?;
let t = i64::from(days)
.checked_mul(86_400_000_000)?
.checked_add(day_micros)?
.checked_sub(tz.unwrap_or(0))?;
Some((t, tz.is_some()))
}
fn parse_timestamp_parts(s: &str, order: DateOrder) -> Option<(i32, i64, Option<i64>)> {
let trimmed = s.trim();
if trimmed.eq_ignore_ascii_case("epoch") {
return Some((0, 0, Some(0)));
}
if trimmed.eq_ignore_ascii_case("infinity")
|| trimmed.eq_ignore_ascii_case("+infinity")
|| trimmed.eq_ignore_ascii_case("-infinity")
{
return None;
}
let (trimmed, era_bc) = match trimmed
.strip_suffix(" BC")
.or_else(|| trimmed.strip_suffix(" bc"))
{
Some(b) => (b.trim_end(), true),
None => (
trimmed
.strip_suffix(" AD")
.or_else(|| trimmed.strip_suffix(" ad"))
.map_or(trimmed, str::trim_end),
false,
),
};
let (date_part, time_part) = match trimmed.find([' ', 'T']) {
Some(i) => (&trimmed[..i], Some(&trimmed[i + 1..])),
None => (trimmed, None),
};
if time_part.is_none() && parse_date_literal_ordered(date_part, order).is_none() {
if let Some(rest) = date_part.strip_suffix(['Z', 'z']) {
if let Some(d) = parse_date_literal_ordered(rest, order) {
return Some((d, 0, Some(0)));
}
}
for (i, c) in date_part.char_indices().rev() {
if c != '+' {
continue;
}
let (head, tail) = date_part.split_at(i);
let (Some(d), Some(off)) = (
parse_date_literal_ordered(head, order),
parse_tz_offset_suffix(tail, c == '+'),
) else {
continue;
};
return Some((d, 0, Some(off)));
}
}
let mut days = parse_date_literal_ordered(date_part, order)?;
if era_bc {
let (y, m, d) = civil_from_days(days);
if y < 1 {
return None;
}
days = days_from_civil(1 - y, m, d);
}
let (day_micros, tz_offset) = match time_part {
None => (0, None),
Some(t) => parse_time_of_day_micros_tz(t)?,
};
Some((days, day_micros, tz_offset))
}
pub fn parse_timestamp_literal_wall_ordered(s: &str, order: DateOrder) -> Option<i64> {
if let Some(v) = timestamp_sentinel(s) {
return Some(v);
}
let (days, day_micros, _tz) = parse_timestamp_parts(s, order)?;
i64::from(days)
.checked_mul(86_400_000_000)?
.checked_add(day_micros)
}
fn timestamp_sentinel(s: &str) -> Option<i64> {
let t = s.trim();
if t.eq_ignore_ascii_case("epoch") {
return Some(0);
}
if t.eq_ignore_ascii_case("infinity") || t.eq_ignore_ascii_case("+infinity") {
return Some(i64::MAX);
}
if t.eq_ignore_ascii_case("-infinity") {
return Some(i64::MIN);
}
None
}
#[must_use]
pub(crate) fn datetime_input_error_text(text: &str, type_name: &str) -> alloc::string::String {
let (kind, hint) = classify_datetime_input(text);
match kind {
DatetimeInputProblem::Syntax => {
alloc::format!("invalid input syntax for type {type_name}: \"{text}\"")
}
DatetimeInputProblem::OutOfRange => {
let mut m = alloc::format!("date/time field value out of range: \"{text}\"");
if hint {
m.push_str("\nHINT: Perhaps you need a different \"DateStyle\" setting.");
}
m
}
}
}
enum DatetimeInputProblem {
Syntax,
OutOfRange,
}
fn classify_datetime_input(text: &str) -> (DatetimeInputProblem, bool) {
let t = text.trim();
if t.is_empty()
|| !t
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '-' | ':' | '.' | ' ' | 'T' | '+' | 'Z'))
{
return (DatetimeInputProblem::Syntax, false);
}
let date_part = t.split([' ', 'T']).next().unwrap_or("");
let fields: alloc::vec::Vec<&str> = date_part.split('-').collect();
if fields.len() != 3
|| fields
.iter()
.any(|f| f.is_empty() || !f.chars().all(|c| c.is_ascii_digit()))
{
return (DatetimeInputProblem::Syntax, false);
}
let month = fields[1].parse::<u32>().unwrap_or(0);
let day = fields[2].parse::<u32>().unwrap_or(0);
let field_out_of_range = !(1..=12).contains(&month) || !(1..=31).contains(&day);
(DatetimeInputProblem::OutOfRange, field_out_of_range)
}
fn parse_time_of_day_micros(t: &str) -> Option<(i64, i64)> {
parse_time_of_day_micros_tz(t).map(|(us, tz)| (us, tz.unwrap_or(0)))
}
fn parse_time_of_day_micros_tz(t: &str) -> Option<(i64, Option<i64>)> {
let t = t.trim();
let (core, tz_micros) = if let Some(rest) = t.strip_suffix('Z') {
(rest, Some(0i64))
} else if let Some(rest) = t.strip_suffix(" UTC").or_else(|| t.strip_suffix("UTC")) {
(rest, Some(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], Some(micros))
} else {
(t, None)
};
let (time, frac_str) = match core.split_once('.') {
Some((a, b)) => (a, Some(b)),
None => (core, None),
};
let bytes = time.as_bytes();
let (hh, mm, ss): (i64, i64, i64) = if bytes.len() == 8 && bytes[2] == b':' && bytes[5] == b':'
{
(
time[0..2].parse().ok()?,
time[3..5].parse().ok()?,
time[6..8].parse().ok()?,
)
} else if bytes.len() == 5 && bytes[2] == b':' {
(time[0..2].parse().ok()?, time[3..5].parse().ok()?, 0)
} else {
return None;
};
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() < 6 {
return None;
}
for i in 5..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, days: i32, micros: i64) -> String {
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 }
};
let mut prev_negative = false;
if years != 0 {
parts.push(format!(
"{years} {}",
unit(i64::from(years), "year", "years")
));
prev_negative = years < 0;
}
if mons != 0 {
let plus = if prev_negative && mons > 0 { "+" } else { "" };
parts.push(format!(
"{plus}{mons} {}",
unit(i64::from(mons), "mon", "mons")
));
prev_negative = mons < 0;
}
if days != 0 {
let plus = if prev_negative && days > 0 { "+" } else { "" };
parts.push(format!(
"{plus}{days} {}",
unit(i64::from(days), "day", "days")
));
}
let mut rem = micros;
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 is_before = if days != 0 {
days < 0
} else if mons != 0 {
mons < 0
} else {
years < 0
};
let sign = if neg {
"-"
} else if is_before {
"+"
} 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() {
"00:00:00".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'
| '\n'
| '\r'
| '\x0b'
| '\x0c'
)
});
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_bool_array(items: &[Option<bool>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 2);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(b) => out.push(if *b { 't' } else { 'f' }),
}
}
out.push('}');
out
}
pub fn format_smallint_array(items: &[Option<i16>]) -> 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_float_array(items: &[Option<f64>]) -> 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(x) => out.push_str(&format_float(*x)),
}
}
out.push('}');
out
}
pub fn format_numeric_array(items: &[Option<(i128, u16)>]) -> 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((scaled, scale)) => out.push_str(&format_numeric(*scaled, *scale)),
}
}
out.push('}');
out
}
pub fn format_date_array(items: &[Option<i32>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 12);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(d) => out.push_str(&format_date(*d)),
}
}
out.push('}');
out
}
pub fn format_timestamp_array(items: &[Option<i64>], with_tz: bool) -> String {
let mut out = String::with_capacity(2 + items.len() * 22);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(t) => {
out.push('"');
if with_tz {
out.push_str(&format_timestamptz(*t));
} else {
out.push_str(&format_timestamp(*t));
}
out.push('"');
}
}
}
out.push('}');
out
}
pub fn format_uuid_array(items: &[Option<[u8; 16]>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 38);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(b) => out.push_str(&spg_storage::format_uuid(b)),
}
}
out.push('}');
out
}
pub fn format_bytea_array(items: &[Option<Vec<u8>>]) -> 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(b) => {
out.push('"');
let hex = format_bytea_hex(b);
for c in hex.chars() {
if c == '\\' {
out.push('\\');
}
out.push(c);
}
out.push('"');
}
}
}
out.push('}');
out
}
pub fn format_interval_array(items: &[Option<spg_storage::IntervalSpan>]) -> String {
let mut out = String::with_capacity(2 + items.len() * 12);
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
match item {
None => out.push_str("NULL"),
Some(span) => {
out.push('"');
out.push_str(&format_interval(span.months, span.days, span.micros));
out.push('"');
}
}
}
out.push('}');
out
}
#[must_use]
pub fn format_bytea_escape(b: &[u8]) -> String {
let mut out = String::with_capacity(b.len());
for &byte in b {
match byte {
b'\\' => out.push_str("\\\\"),
0x20..=0x7e => out.push(byte as char),
_ => out.push_str(&alloc::format!("\\{byte:03o}")),
}
}
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_kind(kind: spg_storage::NumericKind, scaled: i128, scale: u16) -> String {
use spg_storage::NumericKind;
match kind {
NumericKind::Finite => format_numeric(scaled, scale),
NumericKind::NaN => String::from("NaN"),
NumericKind::PosInf => String::from("Infinity"),
NumericKind::NegInf => String::from("-Infinity"),
}
}
pub fn format_numeric(scaled: i128, scale: u16) -> 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
}
fn shortest_float_sci(x: f64) -> String {
let (m, e) = f64_mantissa_exp(x);
let (hi_m, hi_e) = (2 * m + 1, e - 1);
let (lo_m, lo_e) = if m == 1 << 52 && e > f64_min_exp() {
(4 * m - 1, e - 2)
} else {
(2 * m - 1, e - 1)
};
for p in 1..=17u32 {
let cand = alloc::format!("{x:.*e}", (p - 1) as usize);
let Ok(v) = cand.parse::<f64>() else { continue };
if v != x {
continue;
}
let Some((d, k)) = sci_to_digits_exp(&cand) else {
continue;
};
if !decimal_eq_binary(d, k, hi_m, hi_e) && !decimal_eq_binary(d, k, lo_m, lo_e) {
return cand;
}
}
alloc::format!("{x:e}")
}
fn f64_mantissa_exp(x: f64) -> (u128, i32) {
let bits = x.abs().to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = u128::from(bits & 0x000f_ffff_ffff_ffff);
if biased == 0 {
(frac, -1074) } else {
((1u128 << 52) | frac, biased - 1075)
}
}
const fn f64_min_exp() -> i32 {
-1074
}
fn sci_to_digits_exp(sci: &str) -> Option<(u128, i32)> {
let epos = sci.find('e')?;
let (mant, rest) = sci.split_at(epos);
let exp: i32 = rest[1..].parse().ok()?;
let mant = mant.strip_prefix('-').unwrap_or(mant);
let (int_part, frac_part) = match mant.split_once('.') {
Some((a, b)) => (a, b),
None => (mant, ""),
};
let mut digits: u128 = 0;
for c in int_part.chars().chain(frac_part.chars()) {
digits = digits
.checked_mul(10)?
.checked_add(u128::from(c as u8 - b'0'))?;
}
Some((digits, exp - i32::try_from(frac_part.len()).ok()?))
}
fn decimal_eq_binary(d: u128, k: i32, m: u128, e: i32) -> bool {
if d == 0 {
return false;
}
let a = i32::try_from(d.trailing_zeros()).unwrap_or(i32::MAX);
let d_odd = d >> d.trailing_zeros();
if k >= 0 {
let mut lhs = d_odd;
for _ in 0..k {
match lhs.checked_mul(5) {
Some(v) if v <= m => lhs = v,
_ => return false,
}
}
lhs == m && a + k == e
} else {
let j = -k;
let mut lhs = d_odd;
for _ in 0..j {
if lhs % 5 != 0 {
return false;
}
lhs /= 5;
}
lhs == m && a - j == e
}
}