use crate::DataType;
use crate::data_type::FloatPrecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CanonicalText {
Date,
Timestamp,
ZonedTimestamp,
Decimal {
scale: u8,
},
Float {
significant: u8,
},
Boolean,
AsCast,
}
pub const DOUBLE_DIGITS: u8 = 15;
pub const SINGLE_DIGITS: u8 = 6;
pub const UTC_OFFSET: &str = "+00:00";
pub const FLOAT_TEXT_FUNCTION: &str = "razeri_float_text";
pub const SINGLE_FLOAT_TEXT_FUNCTION: &str = "razeri_single_float_text";
#[must_use]
pub const fn float_text_function(significant: u8) -> &'static str {
if significant == SINGLE_DIGITS { SINGLE_FLOAT_TEXT_FUNCTION } else { FLOAT_TEXT_FUNCTION }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueTextFunctions {
pub schema: Option<&'static str>,
pub statements: Vec<String>,
}
#[must_use]
pub fn in_short_lines(expression: &str, indent: &str) -> String {
expression.replace(" WHEN ", &format!("\n{indent}WHEN "))
}
impl CanonicalText {
#[must_use]
pub fn of(data_type: &DataType) -> Self {
match data_type {
DataType::Date(_) => Self::Date,
DataType::Timestamp(_) => Self::Timestamp,
DataType::DateTime(moment) if !moment.with_timezone => Self::Timestamp,
DataType::TimestampTz(_) | DataType::DateTime(_) => Self::ZonedTimestamp,
DataType::Decimal(decimal) => Self::Decimal { scale: decimal.scale },
DataType::Float(float) => Self::Float {
significant: match float.precision {
FloatPrecision::Single => SINGLE_DIGITS,
FloatPrecision::Double => DOUBLE_DIGITS,
},
},
DataType::Boolean(_) => Self::Boolean,
_ => Self::AsCast,
}
}
}
#[must_use]
pub fn truth_text(value: &str) -> String {
format!("CASE WHEN {value} THEN 'true' WHEN NOT {value} THEN 'false' END")
}
#[derive(Debug, Clone, Copy)]
pub struct FloatParts<'a> {
pub digits: &'a str,
pub exponent: &'a str,
pub exponent_digits: &'a str,
}
pub struct TextFunctions<'a> {
pub substring: &'a str,
pub trim_fraction: &'a dyn Fn(&str) -> String,
pub concat: &'a dyn Fn(&[&str]) -> String,
}
#[must_use]
pub fn float_text(parts: &FloatParts<'_>, significant: u8, text: &TextFunctions<'_>) -> String {
let FloatParts { digits, exponent, exponent_digits } = *parts;
let TextFunctions { substring, trim_fraction, concat } = *text;
let mantissa = trim_fraction(&concat(&[
&format!("{substring}({digits}, 1, 1)"),
"'.'",
&format!("{substring}({digits}, 2, {significant})"),
]));
let scientific = concat(&[
&mantissa,
"'e'",
&format!("CASE WHEN {exponent} < 0 THEN '-' ELSE '+' END"),
&format!("CASE WHEN ABS({exponent}) < 10 THEN '0' ELSE '' END"),
exponent_digits,
]);
let below_one =
trim_fraction(&concat(&[&format!("{substring}('0.000', 1, 1 - {exponent})"), digits]));
let with_point = trim_fraction(&concat(&[
&format!("{substring}({digits}, 1, {exponent} + 1)"),
"'.'",
&format!("{substring}({digits}, {exponent} + 2, {significant})"),
]));
format!(
"CASE WHEN {exponent} < -4 OR {exponent} >= {significant} THEN {scientific} \
WHEN {exponent} < 0 THEN {below_one} ELSE {with_point} END"
)
}
pub struct FloatArithmetic<'a> {
pub floor_log10: &'a dyn Fn(&str) -> String,
pub power_of_ten: &'a dyn Fn(&str) -> String,
pub whole_text: &'a dyn Fn(&str) -> String,
pub text: TextFunctions<'a>,
}
#[must_use]
pub fn computed_float_text(value: &str, significant: u8, spelling: &FloatArithmetic<'_>) -> String {
let magnitude = format!("NULLIF(ABS({value}), 0)");
let floor = (spelling.floor_log10)(&magnitude);
let power = format!(
"({floor} - CASE WHEN {magnitude} < {} THEN 1 ELSE 0 END)",
(spelling.power_of_ten)(&floor)
);
let scale = 10_u64.pow(u32::from(significant.saturating_sub(1)));
let whole = format!("ROUND({magnitude} / {} * {scale}, 0)", (spelling.power_of_ten)(&power));
let digits = (spelling.whole_text)(&whole);
let exponent = format!("({power} + CASE WHEN {whole} >= {} THEN 1 ELSE 0 END)", scale * 10);
let exponent_digits = (spelling.whole_text)(&format!("ABS({exponent})"));
let magnitude_text = float_text(
&FloatParts { digits: &digits, exponent: &exponent, exponent_digits: &exponent_digits },
significant,
&spelling.text,
);
signed_float_text(value, &magnitude_text, spelling.text.concat)
}
#[must_use]
pub fn signed_float_text(
value: &str,
magnitude: &str,
concat: &dyn Fn(&[&str]) -> String,
) -> String {
format!(
"CASE WHEN {value} IS NULL THEN NULL WHEN {value} = 0 THEN '0' ELSE {} END",
concat(&[&format!("CASE WHEN {value} < 0 THEN '-' ELSE '' END"), magnitude])
)
}
#[must_use]
pub fn rtrim_fraction(written: &str) -> String {
format!("RTRIM(RTRIM({written}, '0'), '.')")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_type::DateTimeDataType;
#[test]
fn a_date_a_moment_and_a_decimal_have_a_canonical_text() {
assert_eq!(CanonicalText::of(&DataType::date()), CanonicalText::Date);
assert_eq!(CanonicalText::of(&DataType::timestamp()), CanonicalText::Timestamp);
assert_eq!(
CanonicalText::of(&DataType::DateTime(DateTimeDataType::new(6, false))),
CanonicalText::Timestamp
);
assert_eq!(
CanonicalText::of(&DataType::decimal(12, 2)),
CanonicalText::Decimal { scale: 2 }
);
}
#[test]
fn a_zoned_moment_a_float_and_a_truth_value_have_a_canonical_text() {
assert_eq!(CanonicalText::of(&DataType::timestamp_tz()), CanonicalText::ZonedTimestamp);
assert_eq!(
CanonicalText::of(&DataType::DateTime(DateTimeDataType::new(6, true))),
CanonicalText::ZonedTimestamp
);
assert_eq!(
CanonicalText::of(&DataType::double()),
CanonicalText::Float { significant: 15 }
);
assert_eq!(CanonicalText::of(&DataType::float()), CanonicalText::Float { significant: 6 });
assert_eq!(CanonicalText::of(&DataType::boolean()), CanonicalText::Boolean);
}
#[test]
fn text_and_whole_numbers_are_written_by_the_cast() {
for data_type in [DataType::varchar(10), DataType::bigint(), DataType::text()] {
assert_eq!(CanonicalText::of(&data_type), CanonicalText::AsCast, "{data_type:?}");
}
}
#[test]
fn a_truth_value_is_named_by_a_condition() {
assert_eq!(truth_text("b"), "CASE WHEN b THEN 'true' WHEN NOT b THEN 'false' END");
}
#[test]
fn a_float_is_laid_out_from_its_digits_and_its_exponent() {
let pipes = |parts: &[&str]| parts.join(" || ");
let text =
TextFunctions { substring: "SUBSTR", trim_fraction: &rtrim_fraction, concat: &pipes };
let laid_out =
float_text(&FloatParts { digits: "d", exponent: "x", exponent_digits: "w" }, 15, &text);
assert_eq!(
laid_out,
"CASE WHEN x < -4 OR x >= 15 THEN RTRIM(RTRIM(SUBSTR(d, 1, 1) || '.' || \
SUBSTR(d, 2, 15), '0'), '.') || 'e' || CASE WHEN x < 0 THEN '-' ELSE '+' END || \
CASE WHEN ABS(x) < 10 THEN '0' ELSE '' END || w \
WHEN x < 0 THEN RTRIM(RTRIM(SUBSTR('0.000', 1, 1 - x) || d, '0'), '.') \
ELSE RTRIM(RTRIM(SUBSTR(d, 1, x + 1) || '.' || SUBSTR(d, x + 2, 15), '0'), '.') END"
);
assert_eq!(
signed_float_text("v", "m", &pipes),
"CASE WHEN v IS NULL THEN NULL WHEN v = 0 THEN '0' \
ELSE CASE WHEN v < 0 THEN '-' ELSE '' END || m END"
);
}
#[test]
fn a_float_is_written_through_the_function_for_its_digits() {
assert_eq!(float_text_function(DOUBLE_DIGITS), FLOAT_TEXT_FUNCTION);
assert_eq!(float_text_function(SINGLE_DIGITS), SINGLE_FLOAT_TEXT_FUNCTION);
}
#[test]
fn a_computed_float_breaks_before_each_when() {
let arithmetic = FloatArithmetic {
floor_log10: &|x| format!("FLOOR(LOG10({x}))"),
power_of_ten: &|x| format!("POW(10, {x})"),
whole_text: &|x| format!("CAST(CAST({x} AS DECIMAL(19, 0)) AS CHAR)"),
text: TextFunctions {
substring: "SUBSTRING",
trim_fraction: &rtrim_fraction,
concat: &|parts| format!("CONCAT({})", parts.join(", ")),
},
};
let whole = computed_float_text("x", DOUBLE_DIGITS, &arithmetic);
let lines = in_short_lines(&whole, " ");
assert_eq!(lines.replace("\n WHEN ", " WHEN "), whole, "only the line breaks differ");
let longest = lines.lines().map(str::len).max().unwrap_or_default();
assert!(
whole.len() > 3000 && longest < 400,
"{} characters in one line become lines of at most {longest}",
whole.len()
);
}
#[test]
fn the_fraction_is_trimmed_after_its_zeros() {
assert_eq!(rtrim_fraction("x"), "RTRIM(RTRIM(x, '0'), '.')");
}
}