You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/10/14 02:41:43 UTC

(iceberg-rust) branch gh-readonly-queue/main/pr-3358-b154603a710d730d2f43f8af4db257cc137237ef created (now 375fb1211)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-3358-b154603a710d730d2f43f8af4db257cc137237ef
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


      at 375fb1211 fix(spec): format float and double partition values like iceberg-java (#3358)

This branch includes the following new commits:

     new 375fb1211 fix(spec): format float and double partition values like iceberg-java (#3358)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(iceberg-rust) 01/01: fix(spec): format float and double partition values like iceberg-java (#3358)

Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-3358-b154603a710d730d2f43f8af4db257cc137237ef
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git

commit 375fb1211cfa425507a3b49b5409cb7804079a1e
Author: JeonDaehong <90...@users.noreply.github.com>
AuthorDate: Tue Oct 13 23:56:06 2026 +0000

    fix(spec): format float and double partition values like iceberg-java (#3358)
    
    * fix(spec): format float and double partition values like iceberg-java
    
    * test(spec): move float to_human_string test to datum tests
---
 crates/iceberg/src/spec/partition.rs    | 26 +++++++++++++++
 crates/iceberg/src/spec/values/datum.rs | 57 ++++++++++++++++++++++++++++++++-
 crates/iceberg/src/spec/values/tests.rs | 35 ++++++++++++++++++++
 3 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/crates/iceberg/src/spec/partition.rs b/crates/iceberg/src/spec/partition.rs
index 17aa3de96..f3c9f9e54 100644
--- a/crates/iceberg/src/spec/partition.rs
+++ b/crates/iceberg/src/spec/partition.rs
@@ -1951,6 +1951,32 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_partition_to_path_float_matches_java() {
+        let schema = Schema::builder()
+            .with_fields(vec![
+                NestedField::required(1, "d", Type::Primitive(PrimitiveType::Double)).into(),
+                NestedField::required(2, "f", Type::Primitive(PrimitiveType::Float)).into(),
+            ])
+            .build()
+            .unwrap();
+
+        let spec = PartitionSpec::builder(schema.clone())
+            .add_partition_field("d", "d", Transform::Identity)
+            .unwrap()
+            .add_partition_field("f", "f", Transform::Identity)
+            .unwrap()
+            .build()
+            .unwrap();
+
+        let data = Struct::from_iter([Some(Literal::double(1.0)), Some(Literal::float(1.0e7_f32))]);
+
+        assert_eq!(
+            spec.partition_to_path(&data, schema.into()),
+            "d=1.0/f=1.0E7"
+        );
+    }
+
     #[test]
     fn test_partition_to_path_escaped_strings() {
         let schema = Schema::builder()
diff --git a/crates/iceberg/src/spec/values/datum.rs b/crates/iceberg/src/spec/values/datum.rs
index c4c835f61..a501466b3 100644
--- a/crates/iceberg/src/spec/values/datum.rs
+++ b/crates/iceberg/src/spec/values/datum.rs
@@ -18,7 +18,7 @@
 //! Typed literals with validation
 
 use std::cmp::Ordering;
-use std::fmt::{Display, Formatter};
+use std::fmt::{Display, Formatter, LowerExp};
 use std::str::FromStr;
 
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
@@ -1226,11 +1226,66 @@ impl Datum {
     /// Returns a human-readable string representation of this literal.
     ///
     /// For string literals, this returns the raw string value without quotes.
+    /// Float and double literals are formatted like Java's `Float.toString` /
+    /// `Double.toString` (JDK 19+).
     /// For all other literals, it falls back to [`to_string()`](ToString::to_string).
     pub fn to_human_string(&self) -> String {
         match self.literal() {
             PrimitiveLiteral::String(s) => s.to_string(),
+            PrimitiveLiteral::Float(val) => human_float(val.0),
+            PrimitiveLiteral::Double(val) => human_float(val.0),
             _ => self.to_string(),
         }
     }
 }
+
+/// Formats a float like Java's `Float.toString` / `Double.toString` (JDK 19+):
+/// plain decimal when `1e-3 <= |value| < 1e7`, otherwise `<d>.<ddd>E<exp>`,
+/// always with at least one digit after the point.
+fn human_float<T: Float + LowerExp + FromStr>(value: T) -> String {
+    if value.is_nan() {
+        return "NaN".to_string();
+    }
+    let sign = if value.is_sign_negative() { "-" } else { "" };
+    if value.is_infinite() {
+        return format!("{sign}Infinity");
+    }
+    if value.is_zero() {
+        return format!("{sign}0.0");
+    }
+
+    let abs = value.abs();
+    // `{:e}` gives the fewest digits that round-trip. Java uses at least two
+    // digits and, among decimals of that length that round-trip, the closest
+    // one, preferring an even last digit on ties (2097152.2 for 2097152.25f32,
+    // 4.9E-324 for f64::from_bits(1)). Exact formatting picks exactly that.
+    let shortest = format!("{abs:e}");
+    let precision = shortest.find('e').unwrap().saturating_sub(2).max(1);
+    let closest = format!("{abs:.precision$e}");
+    let scientific = if closest.parse::<T>().is_ok_and(|v| v == abs) {
+        closest
+    } else {
+        shortest
+    };
+    let (mantissa, exponent) = scientific.split_once('e').unwrap();
+    let exponent: i32 = exponent.parse().unwrap();
+    let digits = mantissa.replace('.', "");
+    let digits = digits.trim_end_matches('0');
+
+    if (-3..7).contains(&exponent) {
+        if exponent < 0 {
+            let zeros = "0".repeat((-exponent - 1) as usize);
+            format!("{sign}0.{zeros}{digits}")
+        } else {
+            let int_len = exponent as usize + 1;
+            if digits.len() > int_len {
+                format!("{sign}{}.{}", &digits[..int_len], &digits[int_len..])
+            } else {
+                format!("{sign}{digits:0<int_len$}.0")
+            }
+        }
+    } else {
+        let fraction = if digits.len() > 1 { &digits[1..] } else { "0" };
+        format!("{sign}{}.{fraction}E{exponent}", &digits[..1])
+    }
+}
diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs
index 63ff9f636..f21e78ef3 100644
--- a/crates/iceberg/src/spec/values/tests.rs
+++ b/crates/iceberg/src/spec/values/tests.rs
@@ -1986,3 +1986,38 @@ fn test_raw_literal_project_by_name_rejects_missing_required_field() {
     let err = raw.project_by_name(&expected).unwrap_err();
     assert_eq!(err.kind(), ErrorKind::DataInvalid);
 }
+
+/// Expected values are the output of Java's `Double.toString` / `Float.toString` (JDK 21).
+#[test]
+fn test_datum_to_human_string_float_matches_java() {
+    let double = |value: f64| Datum::double(value).to_human_string();
+    assert_eq!(double(1.0), "1.0");
+    assert_eq!(double(100.0), "100.0");
+    assert_eq!(double(0.0), "0.0");
+    assert_eq!(double(-0.0), "-0.0");
+    assert_eq!(double(1.5), "1.5");
+    assert_eq!(double(0.001), "0.001");
+    assert_eq!(double(9999999.0), "9999999.0");
+    assert_eq!(double(1.0e7), "1.0E7");
+    assert_eq!(double(1.0e-5), "1.0E-5");
+    assert_eq!(double(1.23456789e20), "1.23456789E20");
+    assert_eq!(double(1.0e23), "1.0E23");
+    assert_eq!(double(f64::INFINITY), "Infinity");
+    assert_eq!(double(f64::NEG_INFINITY), "-Infinity");
+    assert_eq!(double(f64::NAN), "NaN");
+    assert_eq!(double(f64::MAX), "1.7976931348623157E308");
+    // Java uses at least two significant digits.
+    assert_eq!(double(f64::from_bits(1)), "4.9E-324");
+    // Halfway between two 17-digit decimals: Java picks the even one.
+    assert_eq!(double(2f64.powi(-25)), "2.9802322387695312E-8");
+
+    let float = |value: f32| Datum::float(value).to_human_string();
+    assert_eq!(float(1.0), "1.0");
+    assert_eq!(float(-0.0), "-0.0");
+    assert_eq!(float(0.1), "0.1");
+    assert_eq!(float(1.0e7), "1.0E7");
+    assert_eq!(float(f32::NEG_INFINITY), "-Infinity");
+    assert_eq!(float(f32::from_bits(1)), "1.4E-45");
+    // 2097152.25, halfway between 2097152.2 and 2097152.3.
+    assert_eq!(float(f32::from_bits(0x4a00_0001)), "2097152.2");
+}