Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions packages/markitdown/src/markitdown/converters/_xlsx_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
80 changes: 80 additions & 0 deletions packages/markitdown/tests/test_xlsx_number_formatting.py
Original file line number Diff line number Diff line change
@@ -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