Skip to main content

reifydb_value/util/
float_format.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4pub fn format_f64(v: f64) -> String {
5	if !v.is_finite() {
6		return v.to_string();
7	}
8	if v == 0.0 {
9		return "0".to_string();
10	}
11	let s = v.to_string();
12
13	if !s.contains('.') || count_significant_digits(&s) <= 15 {
14		return s;
15	}
16	let magnitude = v.abs().log10().floor() as i32;
17	let decimal_places = (14 - magnitude).max(0) as usize;
18	let s = format!("{:.prec$}", v, prec = decimal_places);
19	if s.contains('.') {
20		s.trim_end_matches('0').trim_end_matches('.').to_string()
21	} else {
22		s
23	}
24}
25
26pub fn format_f32(v: f32) -> String {
27	if !v.is_finite() {
28		return v.to_string();
29	}
30	if v == 0.0 {
31		return "0".to_string();
32	}
33	let s = v.to_string();
34
35	if !s.contains('.') || count_significant_digits(&s) <= 7 {
36		return s;
37	}
38	let magnitude = (v.abs() as f64).log10().floor() as i32;
39	let decimal_places = (6 - magnitude).max(0) as usize;
40	let s = format!("{:.prec$}", v, prec = decimal_places);
41	if s.contains('.') {
42		s.trim_end_matches('0').trim_end_matches('.').to_string()
43	} else {
44		s
45	}
46}
47
48fn count_significant_digits(s: &str) -> usize {
49	let s = s.strip_prefix('-').unwrap_or(s);
50
51	let s = if let Some(pos) = s.find(['e', 'E']) {
52		&s[..pos]
53	} else {
54		s
55	};
56	let s = s.trim_start_matches('0');
57	let s = s.strip_prefix('.').map(|rest| rest.trim_start_matches('0')).unwrap_or(s);
58	s.chars().filter(|c| c.is_ascii_digit()).count()
59}
60
61#[cfg(test)]
62mod tests {
63	use std::{f32, f64};
64
65	use super::*;
66
67	#[test]
68	fn test_format_f64_special_values() {
69		assert_eq!(format_f64(f64::INFINITY), "inf");
70		assert_eq!(format_f64(f64::NEG_INFINITY), "-inf");
71		assert_eq!(format_f64(f64::NAN), "NaN");
72		assert_eq!(format_f64(0.0), "0");
73		assert_eq!(format_f64(-0.0), "0");
74	}
75
76	#[test]
77	fn test_format_f64_small_values() {
78		assert_eq!(format_f64(1.0), "1");
79		assert_eq!(format_f64(-1.0), "-1");
80		assert_eq!(format_f64(3.14), "3.14");
81		assert_eq!(format_f64(0.1), "0.1");
82		assert_eq!(format_f64(42.0), "42");
83	}
84
85	#[test]
86	fn test_format_f64_15_sig_digits() {
87		// Digits 16 and 17 of a f64 are where platform formatters disagree, so the rendering is
88		// capped at 15 to keep output identical everywhere.
89		let e = f64::consts::E;
90		let s = format_f64(e);
91		assert!(count_significant_digits(&s) <= 15, "got: {}", s);
92	}
93
94	#[test]
95	fn test_format_f64_preserves_short_values() {
96		// The cap must not perturb values already inside it.
97		assert_eq!(format_f64(1.5), "1.5");
98		assert_eq!(format_f64(100.0), "100");
99		assert_eq!(format_f64(0.001), "0.001");
100		assert_eq!(format_f64(123456789012345.0), "123456789012345");
101	}
102
103	#[test]
104	fn test_format_f64_large_values() {
105		// An exact integer keeps all its digits; only fractional values hit the 15-digit cap.
106		assert_eq!(format_f64(1e15), "1000000000000000");
107		let v = 1.234567890123456e10;
108		let s = format_f64(v);
109		assert!(count_significant_digits(&s) <= 15, "got: {}", s);
110	}
111
112	#[test]
113	fn test_format_f64_very_small_values() {
114		let v = 1e-10;
115		let s = format_f64(v);
116		assert!(count_significant_digits(&s) <= 15, "got: {}", s);
117	}
118
119	#[test]
120	fn test_format_f64_negative() {
121		let v = -f64::consts::PI;
122		let s = format_f64(v);
123		assert!(s.starts_with('-'));
124		assert!(count_significant_digits(&s) <= 15, "got: {}", s);
125	}
126
127	#[test]
128	fn test_format_f32_special_values() {
129		assert_eq!(format_f32(f32::INFINITY), "inf");
130		assert_eq!(format_f32(f32::NEG_INFINITY), "-inf");
131		assert_eq!(format_f32(f32::NAN), "NaN");
132		assert_eq!(format_f32(0.0f32), "0");
133		assert_eq!(format_f32(-0.0f32), "0");
134	}
135
136	#[test]
137	fn test_format_f32_small_values() {
138		assert_eq!(format_f32(1.0f32), "1");
139		assert_eq!(format_f32(3.14f32), "3.14");
140		assert_eq!(format_f32(0.1f32), "0.1");
141	}
142
143	#[test]
144	fn test_format_f32_7_sig_digits() {
145		let v = f32::consts::E;
146		let s = format_f32(v);
147		assert!(count_significant_digits(&s) <= 7, "got: {}", s);
148	}
149
150	#[test]
151	fn test_count_significant_digits() {
152		assert_eq!(count_significant_digits("0"), 0);
153		assert_eq!(count_significant_digits("1"), 1);
154		assert_eq!(count_significant_digits("3.14"), 3);
155		assert_eq!(count_significant_digits("0.001"), 1);
156		assert_eq!(count_significant_digits("100"), 3);
157		assert_eq!(count_significant_digits("-3.14"), 3);
158		assert_eq!(count_significant_digits("2.718281828459045"), 16);
159		assert_eq!(count_significant_digits("2.7182818284590455"), 17);
160	}
161
162	#[test]
163	fn test_format_f64_cross_platform_equivalence() {
164		// Irrational constants are exactly where platform formatters diverge past digit 15.
165		let values = [
166			f64::consts::E,
167			f64::consts::PI,
168			f64::consts::LN_2,
169			f64::consts::SQRT_2,
170			1.0f64 / 3.0,
171			2.0f64.sqrt(),
172		];
173		for v in values {
174			let s = format_f64(v);
175			assert!(
176				count_significant_digits(&s) <= 15,
177				"value {} formatted as {} has {} sig digits",
178				v,
179				s,
180				count_significant_digits(&s)
181			);
182		}
183	}
184
185	#[test]
186	fn test_no_trailing_zeros() {
187		// Truncating to 15 digits must trim the padding it exposes, or output stops being stable.
188		let v = 1.2f64;
189		let s = format_f64(v);
190		assert!(!s.ends_with('0') || s == "0", "got: {}", s);
191	}
192}