From 01ce28cd2f8f222582d6f3e7178b0d48bdd8a545 Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:28:03 +0530 Subject: [PATCH] fix(xlsx): render whole numbers as integers when a column holds blanks A single blank cell makes pandas read a numeric column as float64, so every whole number in it rendered as "1.0" even though Excel shows "1". Columns whose present values are all whole numbers are converted back to integers before the HTML render; fractional columns, missing cells and non-float columns are unchanged. Applies to both the .xlsx and .xls converters. --- .../markitdown/converters/_xlsx_converter.py | 30 ++++++- .../tests/test_xlsx_number_formatting.py | 80 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 packages/markitdown/tests/test_xlsx_number_formatting.py diff --git a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py index 355dd8f8d7..b42aff163f 100644 --- a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py +++ b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py @@ -84,6 +84,32 @@ def _repair_sheetview_show_zeroes( return repaired_stream +def _format_integral_columns(sheet: "pd.DataFrame") -> "pd.DataFrame": + """Render whole numbers stored as floats without a trailing ``.0``. + + A single blank cell makes pandas read the column as float64, so every + whole number in it renders as ``1.0`` even though Excel shows ``1``. + Columns whose present values are all whole numbers are converted back to + integers for display; fractional values and missing cells are unchanged. + """ + for column in sheet.columns: + series = sheet[column] + values = series.dropna() + if ( + pd.api.types.is_float_dtype(series.dtype) + and not values.empty + and (values % 1 == 0).all() + ): + # The object dtype stops pandas from coercing the mixed ints and + # NaN values back into a float column on assignment. + sheet[column] = pd.Series( + [int(value) if pd.notna(value) else value for value in series], + dtype=object, + index=series.index, + ) + return sheet + + class XlsxConverter(DocumentConverter): """ Converts XLSX files to Markdown, with each sheet presented as a separate Markdown table. @@ -135,7 +161,7 @@ def convert( md_content = "" for s in sheets: md_content += f"## {s}\n" - html_content = sheets[s].to_html(index=False) + html_content = _format_integral_columns(sheets[s]).to_html(index=False) md_content += ( self._html_converter.convert_string( html_content, **kwargs @@ -197,7 +223,7 @@ def convert( md_content = "" for s in sheets: md_content += f"## {s}\n" - html_content = sheets[s].to_html(index=False) + html_content = _format_integral_columns(sheets[s]).to_html(index=False) md_content += ( self._html_converter.convert_string( html_content, **kwargs diff --git a/packages/markitdown/tests/test_xlsx_number_formatting.py b/packages/markitdown/tests/test_xlsx_number_formatting.py new file mode 100644 index 0000000000..f0be7eaafe --- /dev/null +++ b/packages/markitdown/tests/test_xlsx_number_formatting.py @@ -0,0 +1,80 @@ +from io import BytesIO + +import openpyxl + +from markitdown import MarkItDown, StreamInfo + + +def _convert_workbook(workbook: openpyxl.Workbook) -> str: + stream = BytesIO() + workbook.save(stream) + stream.seek(0) + return MarkItDown().convert( + stream, stream_info=StreamInfo(extension=".xlsx") + ).markdown + + +def test_integer_column_with_blank_cell_renders_without_trailing_point_zero() -> None: + # A blank cell makes pandas read the column as float64, which used to + # render every whole number in it as "1.0" even though Excel shows "1". + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "Sheet" + sheet.append(["Qty", "Note"]) + sheet.append([1, "first"]) + sheet.append([None, "gap"]) + sheet.append([3, "last"]) + + markdown = _convert_workbook(workbook) + + assert "| 1 | first |" in markdown + assert "| 3 | last |" in markdown + assert "1.0" not in markdown + assert "3.0" not in markdown + + +def test_integer_column_with_numeric_header_and_blank_cell() -> None: + # A numeric header row (e.g. years) keeps the column numeric even before + # the header is split off, so the whole column is read as float64 and + # its data cells used to render as "1.0" beside clean "5" cells. + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "Sheet" + sheet.append([2023, 2024]) + sheet.append([1, 5]) + sheet.append([None, 6]) + sheet.append([3, 7]) + + markdown = _convert_workbook(workbook) + + assert "| 1 | 5 |" in markdown + assert "| 3 | 7 |" in markdown + assert "1.0" not in markdown + assert "3.0" not in markdown + + +def test_fractional_numbers_keep_their_decimals() -> None: + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "Sheet" + sheet.append(["Price"]) + sheet.append([2.5]) + sheet.append([None]) + + markdown = _convert_workbook(workbook) + + assert "2.5" in markdown + + +def test_fully_populated_integer_column_is_unchanged() -> None: + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "Sheet" + sheet.append(["Qty"]) + sheet.append([2]) + sheet.append([4]) + + markdown = _convert_workbook(workbook) + + assert "| 2 |" in markdown + assert "| 4 |" in markdown