comphead commented on code in PR #5840:
URL: https://github.com/apache/datafusion-comet/pull/5840#discussion_r4017815957
##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -136,95 +137,159 @@ macro_rules! cast_float_to_timestamp_impl {
}};
}
-macro_rules! cast_float_to_string {
- ($from:expr, $eval_mode:expr, $type:ty, $output_type:ty, $offset_type:ty,
$min_value:expr) => {{
-
- fn cast<OffsetSize>(
- from: &dyn Array,
- _eval_mode: EvalMode,
- ) -> SparkResult<ArrayRef>
- where
- OffsetSize: OffsetSizeTrait, {
- use std::fmt::Write;
-
- let array =
from.as_any().downcast_ref::<$output_type>().unwrap();
-
- // If the absolute number is less than 10,000,000 and greater
or equal than 0.001, the
- // result is expressed without scientific notation with at
least one digit on either side of
- // the decimal point. Otherwise, Spark uses a mantissa
followed by E and an
- // exponent. The mantissa has an optional leading minus sign
followed by one digit to the
- // left of the decimal point, and the minimal number of digits
greater than zero to the
- // right. The exponent has and optional leading minus sign.
- // source:
https://docs.databricks.com/en/sql/language-manual/functions/cast.html
-
- const LOWER_SCIENTIFIC_BOUND: $type = 0.001;
- const UPPER_SCIENTIFIC_BOUND: $type = 10000000.0;
-
- // Values are formatted straight into the builder, so no
intermediate String
- // is allocated per row. Capacity hint matches arrow-rs's own
AVERAGE_STRING_LENGTH
- // (16 bytes / value) so typical fractional and scientific
outputs like
- // "1234.5678" or "-1.4E-45" do not force a mid-loop grow.
- let mut builder =
GenericStringBuilder::<OffsetSize>::with_capacity(
- array.len(),
- array.len() * 16,
- );
- // Reused across rows by the scientific-notation path, which
has to inspect
- // the formatted text before emitting it.
- let mut scratch = String::with_capacity(32);
-
- for value in array.iter() {
- let Some(value) = value else {
- builder.append_null();
- continue;
- };
- let abs = value.abs();
- if
(LOWER_SCIENTIFIC_BOUND..UPPER_SCIENTIFIC_BOUND).contains(&abs)
- || abs == 0.0
- {
- let _ = write!(builder, "{value}");
- if value.fract() == 0.0 {
- // Spark always renders a fractional digit; Rust
omits it.
- let _ = builder.write_str(".0");
- }
- builder.append_value("");
- } else if !value.is_finite() {
- // NaN and the infinities are excluded by the range
check above.
- builder.append_value(if value.is_nan() {
- "NaN"
- } else if value.is_sign_positive() {
- "Infinity"
- } else {
- "-Infinity"
- });
- } else if abs.to_bits() == 1 {
- // Java's Double.toString / Float.toString are not
shortest-roundtrip
- // and render the smallest subnormals with more digits
than Rust does.
- builder.append_value(if value.is_sign_negative() {
- concat!("-", $min_value)
- } else {
- $min_value
- });
- } else {
- scratch.clear();
- let _ = write!(scratch, "{value:E}");
- match scratch.split_once('E') {
- Some((coefficient, exponent)) if
!coefficient.contains('.') => {
- // Spark keeps the fractional digit Rust drops
from a whole
- // coefficient.
- let _ = builder.write_str(coefficient);
- let _ = builder.write_str(".0E");
- builder.append_value(exponent);
- }
- _ => builder.append_value(&scratch),
- }
- }
- }
+/// A float width that Java renders through `Float.toString` /
`Double.toString`.
+///
+/// `num::Float` supplies the arithmetic predicates; the two widths differ
only in the plain-notation
+/// window's endpoints and in the literal text of the smallest subnormal,
which Java's algorithm
+/// spells with more digits than a shortest-round-trip formatter produces.
+pub trait JavaFloatString: Float + fmt::Display + fmt::UpperExp {
+ /// `Float.MIN_VALUE` / `Double.MIN_VALUE` as Java spells it.
+ const MIN_SUBNORMAL: &'static str;
+ /// Plain notation covers `[0.001, 10^7)`; anything outside it is
scientific.
+ const PLAIN_LOWER: Self;
+ const PLAIN_UPPER: Self;
+
+ /// The value one ULP above zero, the one Java does not render shortest.
`Float::min_positive_value`
+ /// is the smallest *normal*, so this has no `num` equivalent.
+ fn is_smallest_subnormal(self) -> bool;
Review Comment:
Both impls spell this identically (`self.abs().to_bits() == 1`), so the
method only exists because `to_bits` is not on `num::Float`. An associated
*value* const removes the method and both bodies:
```rust
/// `Float.MIN_VALUE` / `Double.MIN_VALUE`.
const MIN_SUBNORMAL: Self; // f32::from_bits(1) / f64::from_bits(1)
/// ...as Java spells it.
const MIN_SUBNORMAL_TEXT: &'static str;
```
Call site becomes `abs == T::MIN_SUBNORMAL`. `from_bits` is `const` well
below the 1.94 MSRV. Renaming the string const also stops `MIN_SUBNORMAL`
reading like the value rather than its spelling.
##########
native/spark-expr/src/conversion_funcs/mod.rs:
##########
@@ -23,4 +23,5 @@ mod temporal;
pub(crate) mod trim;
mod utils;
+pub use numeric::{write_java_float_string, JavaFloatString};
Review Comment:
`lib.rs` re-exports with `pub use conversion_funcs::*`, so this makes
`JavaFloatString` part of the published `datafusion-comet-spark-expr` API and
lets a downstream crate implement it for its own type. The only external
consumer needs `f32`/`f64` -> `String`.
Either seal the trait (private supertrait) or export just the owned helper,
e.g. `pub fn java_float_string<T: JavaFloatString>(v: T) -> String`. The second
also lets `iceberg_partition_path.rs` drop its local wrapper and the
`JavaFloatString` import.
##########
native/core/src/execution/operators/iceberg_partition_path.rs:
##########
@@ -170,7 +170,8 @@ fn human_string(transform: &Transform, field_type: &Type,
value: Option<&Literal
// `year`/`month`/`day`/`hour` render the ordinal itself and never see a
timestamp or binary
// field type (their result types are `int` and `date`), so they cannot
collide with the arms
- // below. iceberg-rust already mirrors `TransformUtil` for them.
+ // below. iceberg-rust already mirrors `TransformUtil` for them. `bucket`
and `truncate` reject
Review Comment:
The guarantee that holds here is stronger than the one stated, and does not
depend on what `bucket` accepts: `partition_to_path` passes
`partition_type.fields()[index].field_type`, i.e. the transform's *result*
type, so `bucket` presents `int` and can never reach a `Float`/`Double` arm
even for a float source column (iceberg-java did allow bucketing float/double
before deprecating it in 1.3). `truncate` keeps the source type but has no
float/double arm, so `identity` is indeed the only way in.
Suggest resting the comment on the result-type argument instead.
##########
spark/src/test/scala/org/apache/comet/CometIcebergWriteActionSuite.scala:
##########
@@ -1110,6 +1110,42 @@ class CometIcebergWriteActionSuite
}
}
+ // iceberg-java renders a `float` or `double` partition value with
`Float.toString` /
+ // `Double.toString`. Rust's `Display` spelled `Double.MAX_VALUE` as 309
digits instead, past the
+ // 255-byte limit on one path component (apache/datafusion-comet#5836).
+ test("native acceleration: float and double partition paths match
iceberg-java") {
Review Comment:
Third instance of this shape in the suite: `ts_path_native`/`ts_path_jvm`
and `escaped_native`/`escaped_jvm` also create a table pair, insert identical
`VALUES`, and compare `partitionDirs`. A helper taking (base name, column DDL,
partition spec, `VALUES`, expected dirs) would collapse all three.
Two other things:
1. Is `PARTITIONED BY (f, d)` accepted on every Iceberg version the suite
runs against? Float/double partitioning has been deprecated since 1.3, and the
neighbouring path-spelling test gates its JVM comparison on
`icebergVersionAtLeast(1, 8)`. If an older or newer profile rejects the `CREATE
TABLE`, this fails for an unrelated reason.
2. Unlike the other two path tests, this one does not read the rows back
through both readers -- `assertNativeWriteEngages` only checks `id`. Since the
bug was that the directory could not be created at all, a `Seq("true",
"false").foreach { cometEnabled => ... }` readback of `f`/`d` would confirm a
`d=4.9E-324` directory is openable by both.
##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -136,95 +137,159 @@ macro_rules! cast_float_to_timestamp_impl {
}};
}
-macro_rules! cast_float_to_string {
- ($from:expr, $eval_mode:expr, $type:ty, $output_type:ty, $offset_type:ty,
$min_value:expr) => {{
-
- fn cast<OffsetSize>(
- from: &dyn Array,
- _eval_mode: EvalMode,
- ) -> SparkResult<ArrayRef>
- where
- OffsetSize: OffsetSizeTrait, {
- use std::fmt::Write;
-
- let array =
from.as_any().downcast_ref::<$output_type>().unwrap();
-
- // If the absolute number is less than 10,000,000 and greater
or equal than 0.001, the
- // result is expressed without scientific notation with at
least one digit on either side of
- // the decimal point. Otherwise, Spark uses a mantissa
followed by E and an
- // exponent. The mantissa has an optional leading minus sign
followed by one digit to the
- // left of the decimal point, and the minimal number of digits
greater than zero to the
- // right. The exponent has and optional leading minus sign.
- // source:
https://docs.databricks.com/en/sql/language-manual/functions/cast.html
-
- const LOWER_SCIENTIFIC_BOUND: $type = 0.001;
- const UPPER_SCIENTIFIC_BOUND: $type = 10000000.0;
-
- // Values are formatted straight into the builder, so no
intermediate String
- // is allocated per row. Capacity hint matches arrow-rs's own
AVERAGE_STRING_LENGTH
- // (16 bytes / value) so typical fractional and scientific
outputs like
- // "1234.5678" or "-1.4E-45" do not force a mid-loop grow.
- let mut builder =
GenericStringBuilder::<OffsetSize>::with_capacity(
- array.len(),
- array.len() * 16,
- );
- // Reused across rows by the scientific-notation path, which
has to inspect
- // the formatted text before emitting it.
- let mut scratch = String::with_capacity(32);
-
- for value in array.iter() {
- let Some(value) = value else {
- builder.append_null();
- continue;
- };
- let abs = value.abs();
- if
(LOWER_SCIENTIFIC_BOUND..UPPER_SCIENTIFIC_BOUND).contains(&abs)
- || abs == 0.0
- {
- let _ = write!(builder, "{value}");
- if value.fract() == 0.0 {
- // Spark always renders a fractional digit; Rust
omits it.
- let _ = builder.write_str(".0");
- }
- builder.append_value("");
- } else if !value.is_finite() {
- // NaN and the infinities are excluded by the range
check above.
- builder.append_value(if value.is_nan() {
- "NaN"
- } else if value.is_sign_positive() {
- "Infinity"
- } else {
- "-Infinity"
- });
- } else if abs.to_bits() == 1 {
- // Java's Double.toString / Float.toString are not
shortest-roundtrip
- // and render the smallest subnormals with more digits
than Rust does.
- builder.append_value(if value.is_sign_negative() {
- concat!("-", $min_value)
- } else {
- $min_value
- });
- } else {
- scratch.clear();
- let _ = write!(scratch, "{value:E}");
- match scratch.split_once('E') {
- Some((coefficient, exponent)) if
!coefficient.contains('.') => {
- // Spark keeps the fractional digit Rust drops
from a whole
- // coefficient.
- let _ = builder.write_str(coefficient);
- let _ = builder.write_str(".0E");
- builder.append_value(exponent);
- }
- _ => builder.append_value(&scratch),
- }
- }
- }
+/// A float width that Java renders through `Float.toString` /
`Double.toString`.
+///
+/// `num::Float` supplies the arithmetic predicates; the two widths differ
only in the plain-notation
+/// window's endpoints and in the literal text of the smallest subnormal,
which Java's algorithm
+/// spells with more digits than a shortest-round-trip formatter produces.
+pub trait JavaFloatString: Float + fmt::Display + fmt::UpperExp {
+ /// `Float.MIN_VALUE` / `Double.MIN_VALUE` as Java spells it.
+ const MIN_SUBNORMAL: &'static str;
+ /// Plain notation covers `[0.001, 10^7)`; anything outside it is
scientific.
+ const PLAIN_LOWER: Self;
+ const PLAIN_UPPER: Self;
+
+ /// The value one ULP above zero, the one Java does not render shortest.
`Float::min_positive_value`
+ /// is the smallest *normal*, so this has no `num` equivalent.
+ fn is_smallest_subnormal(self) -> bool;
+}
- Ok(Arc::new(builder.finish()))
+impl JavaFloatString for f32 {
+ const MIN_SUBNORMAL: &'static str = "1.4E-45";
+ const PLAIN_LOWER: Self = 0.001;
+ const PLAIN_UPPER: Self = 10000000.0;
+
+ fn is_smallest_subnormal(self) -> bool {
+ self.abs().to_bits() == 1
+ }
+}
+
+impl JavaFloatString for f64 {
+ const MIN_SUBNORMAL: &'static str = "4.9E-324";
+ const PLAIN_LOWER: Self = 0.001;
+ const PLAIN_UPPER: Self = 10000000.0;
+
+ fn is_smallest_subnormal(self) -> bool {
+ self.abs().to_bits() == 1
+ }
+}
+
+/// Writes `value` as Java's `Float.toString` / `Double.toString` renders it.
+///
+/// If the absolute value is less than 10,000,000 and greater or equal than
0.001, the result is
+/// expressed without scientific notation with at least one digit on either
side of the decimal
+/// point. Otherwise the value is a mantissa followed by `E` and an exponent,
the mantissa having
+/// an optional leading minus sign followed by one digit to the left of the
decimal point and the
+/// minimal number of digits greater than zero to the right.
+/// Source:
<https://docs.databricks.com/en/sql/language-manual/functions/cast.html>
+///
+/// Rust's own `Display` and `UpperExp` give the same digits but drop a whole
coefficient's
+/// fractional zero (`1` for `1.0`) and never switch to an exponent, so
`Double.MAX_VALUE` would
+/// render as 309 digits. Both matter beyond cosmetics: Spark spells a
`cast(double as string)`
+/// this way, and iceberg-java spells a float or double partition directory
this way, where the
+/// unabbreviated form overruns the filesystem's limit on one path component.
+///
+/// This is the pre-JDK-19 `Double.toString`, which is not shortest-round-trip
for every value.
+/// Only the smallest subnormal, by far the most visible case, is corrected
for here.
+///
+/// Errors only if `out` does; writing into a `String` or an arrow string
builder cannot fail.
+pub fn write_java_float_string<T: JavaFloatString, W: fmt::Write>(
+ value: T,
+ out: &mut W,
+) -> fmt::Result {
+ let abs = value.abs();
+ if (T::PLAIN_LOWER..T::PLAIN_UPPER).contains(&abs) || abs.is_zero() {
+ write!(out, "{value}")?;
+ if value.fract().is_zero() {
+ // Java always renders a fractional digit; Rust omits it.
+ out.write_str(".0")?;
+ }
+ Ok(())
+ } else if !value.is_finite() {
+ // NaN and the infinities are excluded by the range check above.
+ out.write_str(if value.is_nan() {
+ "NaN"
+ } else if value.is_sign_negative() {
+ "-Infinity"
+ } else {
+ "Infinity"
+ })
+ } else if value.is_smallest_subnormal() {
+ if value.is_sign_negative() {
+ out.write_str("-")?;
+ }
+ out.write_str(T::MIN_SUBNORMAL)
+ } else {
+ // The coefficient has to be inspected before any of it is emitted, so
it is formatted
+ // into a stack buffer rather than into `out`, which may not be
rewindable.
+ let mut scratch = ExponentBuf::default();
+ write!(scratch, "{value:E}")?;
+ let text = scratch.as_str();
+ match text.split_once('E') {
+ Some((coefficient, exponent)) if !coefficient.contains('.') => {
+ // Java keeps the fractional digit Rust drops from a whole
coefficient.
+ out.write_str(coefficient)?;
+ out.write_str(".0E")?;
+ out.write_str(exponent)
}
+ _ => out.write_str(text),
+ }
+ }
+}
- cast::<$offset_type>($from, $eval_mode)
- }};
+/// Scratch space for one `{:E}` rendering, sized past the longest a float can
produce
Review Comment:
Minor: the size is justified with an example (`-2.2250738585072014E-308`, 23
bytes) rather than with the bound. Rust's `{:E}` emits at most 17 significant
digits for an `f64`, so sign + digit + point + 16 digits + `E` + sign + 3
digits = 24 bytes worst case. Stating that makes 32 obviously safe and makes
the `Err` arm in `write_str` obviously unreachable rather than defensively so.
##########
native/core/src/execution/operators/iceberg_partition_path.rs:
##########
@@ -388,6 +402,60 @@ mod tests {
assert_eq!(civil_from_days(-719_529), (-1, 12, 31));
}
+ fn double(value: f64) -> String {
+ human_string(
+ &Transform::Identity,
+ &Type::Primitive(PrimitiveType::Double),
+ Some(&Literal::Primitive(PrimitiveLiteral::Double(value.into()))),
+ )
+ }
+
+ fn float(value: f32) -> String {
+ human_string(
+ &Transform::Identity,
+ &Type::Primitive(PrimitiveType::Float),
+ Some(&Literal::Primitive(PrimitiveLiteral::Float(value.into()))),
+ )
+ }
+
+ // Expectations taken from `Double.toString` / `Float.toString` output on
the JDK
+ // (apache/datafusion-comet#5836).
+ #[test]
+ fn renders_doubles_like_java_double_to_string() {
Review Comment:
These 27 assertions pin `write_java_float_string`, which lives in
`spark-expr`, from `core` -- and most of them already have Spark-verified
coverage:
- `numeric.rs::test_spark_cast_float_min_value_to_string` pins `1.4E-45` and
`4.9E-324` for both signs.
- `cast_array_to_string.sql:30-31,60-61` pins `3.4028235E38`, `1.4E-45`,
`1.7976931348623157E308`, `4.9E-324`, `NaN`, `+/-Infinity`.
- `cast_double_to_string.sql:23-36` pins `-0.0`, `0.0`, `+/-1.5`, `NaN`,
`+/-Infinity`, `1.0E20`, `0.001`.
What is genuinely new is the plain-notation window (`9.99E-4`, `9999999.0`,
`1.0E7`), `f64::MAX`, `f64::MIN_POSITIVE`, and float coverage in general. Those
would be better as rows in `cast_double_to_string.sql` plus a new
`cast_float_to_string.sql`: `checkSparkAnswerAndOperator` compares against the
Spark running in CI rather than against strings transcribed from a JDK, which
matters here because the renderer deliberately tracks JDK 19+
shortest-round-trip output while the doc calls it pre-JDK-19.
Then two smoke assertions here are enough to prove the two new match arms
are wired.
##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -136,95 +137,159 @@ macro_rules! cast_float_to_timestamp_impl {
}};
}
-macro_rules! cast_float_to_string {
- ($from:expr, $eval_mode:expr, $type:ty, $output_type:ty, $offset_type:ty,
$min_value:expr) => {{
-
- fn cast<OffsetSize>(
- from: &dyn Array,
- _eval_mode: EvalMode,
- ) -> SparkResult<ArrayRef>
- where
- OffsetSize: OffsetSizeTrait, {
- use std::fmt::Write;
-
- let array =
from.as_any().downcast_ref::<$output_type>().unwrap();
-
- // If the absolute number is less than 10,000,000 and greater
or equal than 0.001, the
- // result is expressed without scientific notation with at
least one digit on either side of
- // the decimal point. Otherwise, Spark uses a mantissa
followed by E and an
- // exponent. The mantissa has an optional leading minus sign
followed by one digit to the
- // left of the decimal point, and the minimal number of digits
greater than zero to the
- // right. The exponent has and optional leading minus sign.
- // source:
https://docs.databricks.com/en/sql/language-manual/functions/cast.html
-
- const LOWER_SCIENTIFIC_BOUND: $type = 0.001;
- const UPPER_SCIENTIFIC_BOUND: $type = 10000000.0;
-
- // Values are formatted straight into the builder, so no
intermediate String
- // is allocated per row. Capacity hint matches arrow-rs's own
AVERAGE_STRING_LENGTH
- // (16 bytes / value) so typical fractional and scientific
outputs like
- // "1234.5678" or "-1.4E-45" do not force a mid-loop grow.
- let mut builder =
GenericStringBuilder::<OffsetSize>::with_capacity(
- array.len(),
- array.len() * 16,
- );
- // Reused across rows by the scientific-notation path, which
has to inspect
- // the formatted text before emitting it.
- let mut scratch = String::with_capacity(32);
-
- for value in array.iter() {
- let Some(value) = value else {
- builder.append_null();
- continue;
- };
- let abs = value.abs();
- if
(LOWER_SCIENTIFIC_BOUND..UPPER_SCIENTIFIC_BOUND).contains(&abs)
- || abs == 0.0
- {
- let _ = write!(builder, "{value}");
- if value.fract() == 0.0 {
- // Spark always renders a fractional digit; Rust
omits it.
- let _ = builder.write_str(".0");
- }
- builder.append_value("");
- } else if !value.is_finite() {
- // NaN and the infinities are excluded by the range
check above.
- builder.append_value(if value.is_nan() {
- "NaN"
- } else if value.is_sign_positive() {
- "Infinity"
- } else {
- "-Infinity"
- });
- } else if abs.to_bits() == 1 {
- // Java's Double.toString / Float.toString are not
shortest-roundtrip
- // and render the smallest subnormals with more digits
than Rust does.
- builder.append_value(if value.is_sign_negative() {
- concat!("-", $min_value)
- } else {
- $min_value
- });
- } else {
- scratch.clear();
- let _ = write!(scratch, "{value:E}");
- match scratch.split_once('E') {
- Some((coefficient, exponent)) if
!coefficient.contains('.') => {
- // Spark keeps the fractional digit Rust drops
from a whole
- // coefficient.
- let _ = builder.write_str(coefficient);
- let _ = builder.write_str(".0E");
- builder.append_value(exponent);
- }
- _ => builder.append_value(&scratch),
- }
- }
- }
+/// A float width that Java renders through `Float.toString` /
`Double.toString`.
+///
+/// `num::Float` supplies the arithmetic predicates; the two widths differ
only in the plain-notation
+/// window's endpoints and in the literal text of the smallest subnormal,
which Java's algorithm
+/// spells with more digits than a shortest-round-trip formatter produces.
+pub trait JavaFloatString: Float + fmt::Display + fmt::UpperExp {
+ /// `Float.MIN_VALUE` / `Double.MIN_VALUE` as Java spells it.
+ const MIN_SUBNORMAL: &'static str;
+ /// Plain notation covers `[0.001, 10^7)`; anything outside it is
scientific.
+ const PLAIN_LOWER: Self;
+ const PLAIN_UPPER: Self;
+
+ /// The value one ULP above zero, the one Java does not render shortest.
`Float::min_positive_value`
+ /// is the smallest *normal*, so this has no `num` equivalent.
+ fn is_smallest_subnormal(self) -> bool;
+}
- Ok(Arc::new(builder.finish()))
+impl JavaFloatString for f32 {
+ const MIN_SUBNORMAL: &'static str = "1.4E-45";
+ const PLAIN_LOWER: Self = 0.001;
+ const PLAIN_UPPER: Self = 10000000.0;
+
+ fn is_smallest_subnormal(self) -> bool {
+ self.abs().to_bits() == 1
+ }
+}
+
+impl JavaFloatString for f64 {
+ const MIN_SUBNORMAL: &'static str = "4.9E-324";
+ const PLAIN_LOWER: Self = 0.001;
+ const PLAIN_UPPER: Self = 10000000.0;
+
+ fn is_smallest_subnormal(self) -> bool {
+ self.abs().to_bits() == 1
+ }
+}
+
+/// Writes `value` as Java's `Float.toString` / `Double.toString` renders it.
+///
+/// If the absolute value is less than 10,000,000 and greater or equal than
0.001, the result is
+/// expressed without scientific notation with at least one digit on either
side of the decimal
+/// point. Otherwise the value is a mantissa followed by `E` and an exponent,
the mantissa having
+/// an optional leading minus sign followed by one digit to the left of the
decimal point and the
+/// minimal number of digits greater than zero to the right.
+/// Source:
<https://docs.databricks.com/en/sql/language-manual/functions/cast.html>
+///
+/// Rust's own `Display` and `UpperExp` give the same digits but drop a whole
coefficient's
+/// fractional zero (`1` for `1.0`) and never switch to an exponent, so
`Double.MAX_VALUE` would
+/// render as 309 digits. Both matter beyond cosmetics: Spark spells a
`cast(double as string)`
+/// this way, and iceberg-java spells a float or double partition directory
this way, where the
+/// unabbreviated form overruns the filesystem's limit on one path component.
+///
+/// This is the pre-JDK-19 `Double.toString`, which is not shortest-round-trip
for every value.
+/// Only the smallest subnormal, by far the most visible case, is corrected
for here.
+///
+/// Errors only if `out` does; writing into a `String` or an arrow string
builder cannot fail.
Review Comment:
The same fact -- writing into a `String` or a string builder cannot fail --
is stated three times: here, at `spark_cast_float_to_utf8`'s `let _ =`, and in
`java_float_string` in `iceberg_partition_path.rs`. One statement on the
function that returns the `fmt::Result` is enough; the two `let _ =` sites can
just point at it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]