use sqlx::types::time::{Date, OffsetDateTime, PrimitiveDateTime, Time};
use std::fmt::Write;
pub(crate) fn f32_shortest(f: f32) -> f64 {
f.to_string().parse().unwrap_or_else(|_| f64::from(f))
}
pub(crate) fn uuid_text(b: &[u8]) -> Result<String, String> {
let b: &[u8; 16] = b
.try_into()
.map_err(|_| format!("a uuid is 16 bytes, got {}", b.len()))?;
let mut s = String::with_capacity(36);
for (i, byte) in b.iter().enumerate() {
if matches!(i, 4 | 6 | 8 | 10) {
s.push('-');
}
let _ = write!(s, "{byte:02x}");
}
Ok(s)
}
pub(crate) fn numeric_text(b: &[u8]) -> Result<String, String> {
let bad = || format!("malformed numeric ({} bytes)", b.len());
if b.len() < 8 {
return Err(bad());
}
let i16at = |i: usize| i16::from_be_bytes([b[i], b[i + 1]]);
let ndigits = usize::try_from(i16at(0)).map_err(|_| bad())?;
let weight = i32::from(i16at(2));
let sign = u16::from_be_bytes([b[4], b[5]]);
let dscale = usize::try_from(i16at(6)).map_err(|_| bad())?;
match sign {
0xC000 => return Ok("NaN".to_string()),
0xD000 => return Ok("Infinity".to_string()),
0xF000 => return Ok("-Infinity".to_string()),
0x0000 | 0x4000 => {}
_ => return Err(bad()),
}
if b.len() != 8 + 2 * ndigits {
return Err(bad());
}
let digit = |i: i32| -> u16 {
usize::try_from(i)
.ok()
.filter(|&i| i < ndigits)
.map_or(0, |i| u16::from_be_bytes([b[8 + 2 * i], b[9 + 2 * i]]))
};
let mut s = String::new();
if sign == 0x4000 {
s.push('-');
}
if weight < 0 {
s.push('0');
} else {
for w in 0..=weight {
let d = digit(w);
if w == 0 {
let _ = write!(s, "{d}");
} else {
let _ = write!(s, "{d:04}");
}
}
}
if dscale > 0 {
s.push('.');
let mut frac = String::new();
let mut w = weight + 1;
while frac.len() < dscale {
let _ = write!(frac, "{:04}", digit(w));
w += 1;
}
frac.truncate(dscale);
s.push_str(&frac);
}
Ok(s)
}
pub(crate) fn inet_text(b: &[u8]) -> Result<String, String> {
use std::net::{Ipv4Addr, Ipv6Addr};
let bad = || format!("malformed inet ({} bytes)", b.len());
if b.len() < 4 {
return Err(bad());
}
let (family, bits, is_cidr, n) = (b[0], b[1], b[2] != 0, usize::from(b[3]));
let addr = b.get(4..4 + n).ok_or_else(bad)?;
let (text, full) = match (family, n) {
(2, 4) => (
Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]).to_string(),
32,
),
(3, 16) => {
let a: [u8; 16] = addr.try_into().map_err(|_| bad())?;
(Ipv6Addr::from(a).to_string(), 128)
}
_ => return Err(bad()),
};
Ok(if is_cidr || bits != full {
format!("{text}/{bits}")
} else {
text
})
}
fn fraction(s: &mut String, micros: u32) {
if micros != 0 {
let f = format!("{micros:06}");
s.push('.');
s.push_str(f.trim_end_matches('0'));
}
}
pub(crate) fn date_text(d: Date) -> String {
format!("{:04}-{:02}-{:02}", d.year(), u8::from(d.month()), d.day())
}
pub(crate) fn time_text(t: Time) -> String {
let mut s = format!("{:02}:{:02}:{:02}", t.hour(), t.minute(), t.second());
fraction(&mut s, t.microsecond());
s
}
pub(crate) fn naive_text(t: PrimitiveDateTime) -> String {
format!("{}T{}", date_text(t.date()), time_text(t.time()))
}
pub(crate) fn utc_text(t: OffsetDateTime) -> String {
let u = t.to_offset(sqlx::types::time::UtcOffset::UTC);
format!(
"{}Z",
naive_text(PrimitiveDateTime::new(u.date(), u.time()))
)
}
#[cfg(feature = "mysql")]
pub(crate) fn mysql_time_text(t: &sqlx::mysql::types::MySqlTime) -> String {
let mut s = format!(
"{}{:02}:{:02}:{:02}",
if t.sign().is_negative() { "-" } else { "" },
t.hours(),
t.minutes(),
t.seconds()
);
fraction(&mut s, t.microseconds());
s
}
#[cfg(test)]
mod tests {
use super::*;
fn numeric(ndigits: i16, weight: i16, sign: u16, dscale: i16, digits: &[u16]) -> Vec<u8> {
let mut b = Vec::new();
for v in [ndigits, weight] {
b.extend(v.to_be_bytes());
}
b.extend(sign.to_be_bytes());
b.extend(dscale.to_be_bytes());
for d in digits {
b.extend(d.to_be_bytes());
}
b
}
#[test]
fn numerics() {
assert_eq!(
numeric_text(&numeric(3, 1, 0, 2, &[1, 2345, 6700])).unwrap(),
"12345.67"
);
assert_eq!(
numeric_text(&numeric(2, 0, 0x4000, 2, &[0, 5000])).unwrap(),
"-0.50"
);
assert_eq!(numeric_text(&numeric(0, 0, 0, 2, &[])).unwrap(), "0.00");
assert_eq!(numeric_text(&numeric(0, 0, 0, 0, &[])).unwrap(), "0");
assert_eq!(numeric_text(&numeric(1, 1, 0, 0, &[1])).unwrap(), "10000");
assert_eq!(
numeric_text(&numeric(2, -2, 0, 9, &[123, 4000])).unwrap(),
"0.000001234"
);
assert_eq!(numeric_text(&numeric(0, 0, 0xC000, 0, &[])).unwrap(), "NaN");
assert!(numeric_text(&[0; 4]).is_err());
assert!(numeric_text(&numeric(2, 0, 0, 0, &[1])).is_err());
}
#[test]
fn uuids_and_floats() {
let b = [
0xa0, 0xee, 0xbc, 0x99, 0x9c, 0x0b, 0x4e, 0xf8, 0xbb, 0x6d, 0x6b, 0xb9, 0xbd, 0x38,
0x0a, 0x11,
];
assert_eq!(
uuid_text(&b).unwrap(),
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
);
assert!(uuid_text(&b[..15]).is_err());
assert_eq!(f32_shortest(0.1), 0.1);
assert_eq!(f32_shortest(f32::MAX), 3.4028235e38);
assert!(f32_shortest(f32::NAN).is_nan());
}
#[test]
fn inets() {
assert_eq!(inet_text(&[2, 32, 0, 4, 10, 0, 0, 1]).unwrap(), "10.0.0.1");
assert_eq!(
inet_text(&[2, 24, 0, 4, 10, 0, 0, 1]).unwrap(),
"10.0.0.1/24"
);
assert_eq!(
inet_text(&[2, 24, 1, 4, 10, 0, 0, 0]).unwrap(),
"10.0.0.0/24"
);
let mut v6 = vec![3, 128, 0, 16];
v6.extend([0u8; 15]);
v6.push(1);
assert_eq!(inet_text(&v6).unwrap(), "::1");
assert!(inet_text(&[2, 32, 0, 4, 10]).is_err());
}
#[test]
fn dates_and_times() {
let d = Date::from_ordinal_date(2026, 290).unwrap();
assert_eq!(date_text(d), "2026-10-17");
let t = Time::from_hms_micro(12, 34, 56, 789_000).unwrap();
assert_eq!(time_text(t), "12:34:56.789");
assert_eq!(time_text(Time::from_hms(12, 34, 56).unwrap()), "12:34:56");
assert_eq!(
naive_text(PrimitiveDateTime::new(d, t)),
"2026-10-17T12:34:56.789"
);
let at = PrimitiveDateTime::new(d, t)
.assume_offset(sqlx::types::time::UtcOffset::from_hms(2, 0, 0).unwrap());
assert_eq!(utc_text(at), "2026-10-17T10:34:56.789Z");
}
}