Skip to main content

uqa_core/
float_text.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` floating-point text formatting shared by SQL and graph values.
8
9use crate::{
10    memory::{Produced, ProductionControl, ProductionString},
11    ValueRetentionError,
12};
13
14/// `PostgreSQL` `float8out` shortest-round-trip formatting: fixed notation while the decimal exponent is in `[-4, 15)`, scientific (`1e+15`, `1e-05`) otherwise, with `NaN` and `Infinity` spelled out.
15#[must_use]
16pub fn format_float_pg(f: f64) -> String {
17    format_float_pg_with_control(f, &ProductionControl::uncontrolled())
18        .expect("ordinary float formatting")
19        .into_uncontrolled()
20        .expect("ordinary float text")
21}
22
23pub fn format_float_pg_with_control(
24    f: f64,
25    control: &ProductionControl<'_>,
26) -> Result<Produced<String>, ValueRetentionError> {
27    if f.is_nan() {
28        return control.copy_text("NaN");
29    }
30    if f.is_infinite() {
31        return control.copy_text(if f > 0.0 { "Infinity" } else { "-Infinity" });
32    }
33    let sci = control.format(format_args!("{f:e}"))?;
34    let Some((mantissa, exp)) = sci.split_once('e') else {
35        return Ok(sci);
36    };
37    let Ok(exp) = exp.parse::<i32>() else {
38        return Ok(sci);
39    };
40    let mut digits = ProductionString::new(*control);
41    for character in mantissa.chars().filter(char::is_ascii_digit) {
42        digits.push(character)?;
43    }
44    let sign = if mantissa.starts_with('-') { "-" } else { "" };
45    let mut output = ProductionString::new(*control);
46    output.push_str(sign)?;
47    if (-4..15).contains(&exp) {
48        if exp >= 0 {
49            let Ok(int_len) = usize::try_from(exp + 1) else {
50                return Ok(sci);
51            };
52            if digits.len() > int_len {
53                output.push_str(&digits[..int_len])?;
54                output.push('.')?;
55                output.push_str(&digits[int_len..])?;
56            } else {
57                output.push_str(&digits)?;
58                for _ in digits.len()..int_len {
59                    output.push('0')?;
60                }
61            }
62        } else {
63            let Ok(zero_count) = usize::try_from(-exp - 1) else {
64                return Ok(sci);
65            };
66            output.push_str("0.")?;
67            for _ in 0..zero_count {
68                output.push('0')?;
69            }
70            output.push_str(&digits)?;
71        }
72    } else {
73        output.push_str(&digits[..1])?;
74        if digits.len() > 1 {
75            output.push('.')?;
76            output.push_str(&digits[1..])?;
77        }
78        output.push_str(&control.format(format_args!("e{exp:+03}"))?)?;
79    }
80    output.finish()
81}
82
83#[cfg(test)]
84mod tests;