Files
oak-gpui/crates/settings/src/serde_helper.rs
T
DongandBen Kunkle 641ae90cd6 settings_ui: Fix IEEE 754 floating point error when serializing value to JSON (#40677)
Closes #40556

Release Notes:

- settings-ui: Fixed an issue where modifying floating point number fields could result in the values written to the settings file containing IEEE 754 floating point error

> [!Note]
> Seems like there's another pull request fixing the centered layout in
#40661 by normalizing the whole `settings.json` file when updating it.
Not sure which is the better way to handle this issue.

## Description

This pull request solves the IEEE 754 floating point error when
serializing values from settings-ui into `settings.json` by creating a
two `serde_helper` to convert the problematic f32 into a formatted two
decimal placed f32.

Fields currently exists the IEEE 754 error:
- Appearance → Unnecessary Code Fade
- Editor → Drop Size Target
- Window & Layout → Centered Layout Left/Right Padding
- Window & Layout → Inactive Opacity

## How to verify

### Unnecessary Code Fade

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/1bf4bad2-63c5-4b03-ac29-8b6b59569e16"
/> | <video
src="https://github.com/user-attachments/assets/dadcd4a1-651b-43dd-913f-edae073ceb68"
/>

### Drop Size Target

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/9d5b4173-fcac-44d0-b7fc-772a2e426ef1"
/> | <video
src="https://github.com/user-attachments/assets/4b5adeaf-e678-494d-bd1b-6c1d55824c43"
/>

### Centered Layout Left/Right Padding

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/33b4e1ff-7ab2-44f7-9e9b-8abad1565d9a"
/> | <video
src="https://github.com/user-attachments/assets/63d8de9e-28d1-4bd7-a6c9-02452e105486"
/>

### Inactive Opacity

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/a7fe2e72-deb6-41dc-82f3-e2649503b8a4"
/> | <video
src="https://github.com/user-attachments/assets/993c314f-b6f6-4dcd-8f74-fa357ab063e9"
/>

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-10-21 16:45:40 +00:00

136 lines
4.0 KiB
Rust

use serde::Serializer;
/// Serializes an f32 value with 2 decimal places of precision.
///
/// This function rounds the value to 2 decimal places and formats it as a string,
/// then parses it back to f64 before serialization. This ensures clean JSON output
/// without IEEE 754 floating-point artifacts.
///
/// # Arguments
///
/// * `value` - The f32 value to serialize
/// * `serializer` - The serde serializer to use
///
/// # Returns
///
/// Result of the serialization operation
///
/// # Usage
///
/// This function can be used with Serde's `serialize_with` attribute:
/// ```
/// use serde::Serialize;
/// use settings::serialize_f32_with_two_decimal_places;
///
/// #[derive(Serialize)]
/// struct ExampleStruct(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] f32);
/// ```
pub fn serialize_f32_with_two_decimal_places<S>(
value: &f32,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let rounded = (value * 100.0).round() / 100.0;
let formatted = format!("{:.2}", rounded);
let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64);
serializer.serialize_f64(clean_value)
}
/// Serializes an optional f32 value with 2 decimal places of precision.
///
/// This function handles `Option<f32>` types, serializing `Some` values with 2 decimal
/// places of precision and `None` values as null. For `Some` values, it rounds to 2 decimal
/// places and formats as a string, then parses back to f64 before serialization. This ensures
/// clean JSON output without IEEE 754 floating-point artifacts.
///
/// # Arguments
///
/// * `value` - The optional f32 value to serialize
/// * `serializer` - The serde serializer to use
///
/// # Returns
///
/// Result of the serialization operation
///
/// # Behavior
///
/// * `Some(v)` - Serializes the value rounded to 2 decimal places
/// * `None` - Serializes as JSON null
///
/// # Usage
///
/// This function can be used with Serde's `serialize_with` attribute:
/// ```
/// use serde::Serialize;
/// use settings::serialize_optional_f32_with_two_decimal_places;
///
/// #[derive(Serialize)]
/// struct ExampleStruct {
/// #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")]
/// optional_value: Option<f32>,
/// }
/// ```
pub fn serialize_optional_f32_with_two_decimal_places<S>(
value: &Option<f32>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(v) => {
let rounded = (v * 100.0).round() / 100.0;
let formatted = format!("{:.2}", rounded);
let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64);
serializer.serialize_some(&clean_value)
}
None => serializer.serialize_none(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct TestOptional {
#[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")]
value: Option<f32>,
}
#[derive(Serialize, Deserialize)]
struct TestNonOptional {
#[serde(serialize_with = "serialize_f32_with_two_decimal_places")]
value: f32,
}
#[test]
fn test_serialize_optional_f32_with_two_decimal_places() {
let cases = [
(Some(123.456789), r#"{"value":123.46}"#),
(Some(1.2), r#"{"value":1.2}"#),
(Some(300.00000), r#"{"value":300.0}"#),
];
for (value, expected) in cases {
let value = TestOptional { value };
assert_eq!(serde_json::to_string(&value).unwrap(), expected);
}
}
#[test]
fn test_serialize_f32_with_two_decimal_places() {
let cases = [
(123.456789, r#"{"value":123.46}"#),
(1.200, r#"{"value":1.2}"#),
(300.00000, r#"{"value":300.0}"#),
];
for (value, expected) in cases {
let value = TestNonOptional { value };
assert_eq!(serde_json::to_string(&value).unwrap(), expected);
}
}
}