Skip to content

Commit

Permalink
feat: mimic excel behavior to convert float to string (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood authored Oct 7, 2024
1 parent 5145063 commit 5383632
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 0 deletions.
Binary file added python/tests/fixtures/decimal-numbers.xlsx
Binary file not shown.
16 changes: 16 additions & 0 deletions python/tests/test_fastexcel.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,3 +536,19 @@ def test_null_values_in_cells() -> None:
def test_null_column_is_nullable() -> None:
sheet = fastexcel.read_excel(path_for_fixture("null-column.xlsx")).load_sheet(0)
assert sheet.to_arrow().schema.field("nullonly").nullable is True


def test_sheet_with_decimal_numbers() -> None:
sheet = fastexcel.read_excel(path_for_fixture("decimal-numbers.xlsx")).load_sheet(0)
pl_assert_frame_equal(
sheet.to_polars(),
pl.DataFrame({"Decimals": [28.14, 29.02]}),
)

sheet2 = fastexcel.read_excel(path_for_fixture("decimal-numbers.xlsx")).load_sheet(
0, dtypes={0: "string"}
)
pl_assert_frame_equal(
sheet2.to_polars(),
pl.DataFrame({"Decimals": ["28.14", "29.02"]}),
)
4 changes: 4 additions & 0 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ mod array_impls {
use calamine::{CellType, DataType, Range};
use chrono::NaiveDate;

use crate::types::dtype::excel_float_to_string;

pub(crate) fn create_boolean_array<DT: CellType + DataType>(
data: &Range<DT>,
col: usize,
Expand Down Expand Up @@ -142,6 +144,8 @@ mod array_impls {
cell.get_datetime_iso().map(str::to_string)
} else if cell.is_bool() {
cell.get_bool().map(|v| v.to_string())
} else if cell.is_float() {
cell.get_float().map(excel_float_to_string)
} else {
cell.as_string()
}
Expand Down
33 changes: 33 additions & 0 deletions src/types/dtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,31 @@ pub(crate) fn get_dtype_for_column<DT: CellType + Debug + DataType>(
}
}

/// Convert a float to a nice string to mimic Excel behaviour.
///
/// Excel can store a float like 29.02 set by the user as "29.020000000000003" in the XML.
/// But in fact, the user will see "29.02" in the cell.
/// Excel indeed displays decimal numbers with 8 digits in a standard cell width
/// and 10 digits in a wide cell. Like this:
///
/// Format = 0.000000000 | Unformatted, wide cell | Unformatted, standard width
/// ---------------------|--------------------------|----------------------------
/// 1.123456789 | 1.123456789 | 1.123457
/// 12.123456789 | 12.12345679 | 12.12346
/// ... | ... | ...
/// 123456.123456789 | 123456.1235 | 123456.1
///
/// Excel also trims trailing zeros and the decimal point if there is no fractional part.
///
/// We do not distinguish between wide cells and standard cells here, so we retain at most
/// nine digits after the decimal point and trim any trailing zeros.
pub(crate) fn excel_float_to_string(x: f64) -> String {
format!("{x:.9}")
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}

#[cfg(test)]
mod tests {
use calamine::{Cell, Data as CalData};
Expand Down Expand Up @@ -394,4 +419,12 @@ mod tests {
FastExcelErrorKind::UnsupportedColumnTypeCombination(_)
));
}

#[rstest]
#[case(29.020000000000003, "29.02")]
#[case(10000_f64, "10000")]
#[case(23.0, "23")]
fn test_excel_float_to_string(#[case] x: f64, #[case] expected: &str) {
assert_eq!(excel_float_to_string(x), expected.to_string());
}
}

0 comments on commit 5383632

Please sign in to comment.