use serde::{Deserialize, Serialize};
const MAX_SINGLE_PRECISION_P: u32 = 24;
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
zerompk::ToMessagePack,
zerompk::FromMessagePack,
)]
#[msgpack(c_enum)]
#[repr(u8)]
pub enum FloatWidth {
F32 = 0,
F64 = 1,
}
impl FloatWidth {
pub fn from_declared_type(declared: &str) -> Option<Self> {
let normalized = declared.trim().to_ascii_lowercase();
let is = |name: &str| super::declared_type_matches(&normalized, name);
if is("real") || is("float4") || is("float32") {
Some(Self::F32)
} else if is("double") || is("float8") || is("float64") {
Some(Self::F64)
} else if is("float") {
Some(Self::from_float_precision(&normalized))
} else {
None
}
}
fn from_float_precision(normalized: &str) -> Self {
let Some(rest) = normalized.strip_prefix("float") else {
return Self::F64;
};
let Some(inner) = rest.trim_start().strip_prefix('(') else {
return Self::F64;
};
let Some(end) = inner.find(')') else {
return Self::F64;
};
let Some(digits) = inner.get(..end) else {
return Self::F64;
};
match digits.trim().parse::<u32>() {
Ok(p) if (1..=MAX_SINGLE_PRECISION_P).contains(&p) => Self::F32,
Ok(_) | Err(_) => Self::F64,
}
}
pub const fn pg_oid(self) -> u32 {
match self {
Self::F32 => 700,
Self::F64 => 701,
}
}
pub const fn pg_type_name(self) -> &'static str {
match self {
Self::F32 => "real",
Self::F64 => "double precision",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_every_declared_float_spelling() {
let cases: &[(&str, FloatWidth)] = &[
("REAL", FloatWidth::F32),
("FLOAT4", FloatWidth::F32),
("FLOAT32", FloatWidth::F32),
("FLOAT", FloatWidth::F64),
("FLOAT8", FloatWidth::F64),
("FLOAT64", FloatWidth::F64),
("DOUBLE", FloatWidth::F64),
("DOUBLE PRECISION", FloatWidth::F64),
];
for (declared, expected) in cases {
assert_eq!(
FloatWidth::from_declared_type(declared),
Some(*expected),
"{declared} must resolve to {expected:?}"
);
assert_eq!(
FloatWidth::from_declared_type(&declared.to_lowercase()),
Some(*expected),
"{declared} must resolve case-insensitively"
);
}
}
#[test]
fn bare_float_is_double_precision() {
assert_eq!(
FloatWidth::from_declared_type("FLOAT"),
Some(FloatWidth::F64)
);
assert_eq!(FloatWidth::F64.pg_oid(), 701);
}
#[test]
fn float_does_not_prefix_match_numbered_spellings() {
for (declared, expected) in [
("float4", FloatWidth::F32),
("float32", FloatWidth::F32),
("float8", FloatWidth::F64),
("float64", FloatWidth::F64),
] {
assert_eq!(FloatWidth::from_declared_type(declared), Some(expected));
}
}
#[test]
fn float_precision_follows_the_postgres_rule() {
for p in [1_u32, 10, 24] {
assert_eq!(
FloatWidth::from_declared_type(&format!("FLOAT({p})")),
Some(FloatWidth::F32),
"FLOAT({p}) must be single precision"
);
}
for p in [25_u32, 40, 53] {
assert_eq!(
FloatWidth::from_declared_type(&format!("FLOAT({p})")),
Some(FloatWidth::F64),
"FLOAT({p}) must be double precision"
);
}
}
#[test]
fn malformed_float_precision_falls_back_to_f64() {
for declared in [
"FLOAT(x)",
"FLOAT(",
"FLOAT()",
"FLOAT(24",
"FLOAT(0)",
"FLOAT(54)",
"FLOAT(-3)",
"FLOAT(99999999999999999999)",
"FLOAT (24",
] {
assert_eq!(
FloatWidth::from_declared_type(declared),
Some(FloatWidth::F64),
"{declared} must fall back to double precision"
);
}
}
#[test]
fn tolerates_trailing_modifiers_and_whitespace() {
assert_eq!(
FloatWidth::from_declared_type("REAL NOT NULL"),
Some(FloatWidth::F32)
);
assert_eq!(
FloatWidth::from_declared_type(" double precision "),
Some(FloatWidth::F64)
);
assert_eq!(
FloatWidth::from_declared_type("FLOAT8 NOT NULL"),
Some(FloatWidth::F64)
);
}
#[test]
fn rejects_non_float_types() {
for declared in [
"TEXT",
"VARCHAR(20)",
"INT4",
"BIGINT",
"BOOL",
"TIMESTAMP",
"GEOMETRY",
"DECIMAL(10,2)",
"",
"floaty",
"realistic",
] {
assert_eq!(
FloatWidth::from_declared_type(declared),
None,
"{declared} must not resolve to a float width"
);
}
}
#[test]
fn pg_oids_match_the_postgres_catalog() {
assert_eq!(FloatWidth::F32.pg_oid(), 700);
assert_eq!(FloatWidth::F64.pg_oid(), 701);
}
#[test]
fn pg_type_names_match_the_postgres_catalog() {
assert_eq!(FloatWidth::F32.pg_type_name(), "real");
assert_eq!(FloatWidth::F64.pg_type_name(), "double precision");
}
#[test]
fn orders_narrow_to_wide() {
assert!(FloatWidth::F32 < FloatWidth::F64);
}
}