diff --git a/pandas/io/formats/css.py b/pandas/io/formats/css.py index 0f1a7302aa808..64795296f30f0 100644 --- a/pandas/io/formats/css.py +++ b/pandas/io/formats/css.py @@ -20,6 +20,25 @@ ) +def _normalize_number_format_value(value: str) -> str: + out = [] + in_single = False + in_double = False + + for ch in value.strip(): + if ch == "'" and not in_double: + in_single = not in_single + out.append(ch) + elif ch == '"' and not in_single: + in_double = not in_double + out.append(ch) + elif in_single or in_double: + out.append(ch) + else: + out.append(ch.lower()) + return "".join(out) + + def _side_expander(prop_fmt: str) -> Callable: """ Wrapper to expand shorthand property into top, right, bottom, left properties @@ -391,7 +410,10 @@ def _error() -> str: def atomize(self, declarations: Iterable) -> Generator[tuple[str, str]]: for prop, value in declarations: prop = prop.lower() - value = value.lower() + if prop == "number-format": + value = _normalize_number_format_value(value) + else: + value = value.lower() if prop in self.CSS_EXPANSIONS: expand = self.CSS_EXPANSIONS[prop] yield from expand(self, prop, value) @@ -414,7 +436,10 @@ def parse(self, declarations_str: str) -> Iterator[tuple[str, str]]: prop, sep, val = decl.partition(":") prop = prop.strip().lower() # TODO: don't lowercase case sensitive parts of values (strings) - val = val.strip().lower() + if prop == "number-format": + val = _normalize_number_format_value(val) + else: + val = val.strip().lower() if sep: yield prop, val else: diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 12f14589365ff..cab85b1c0fa1e 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -96,6 +96,7 @@ def test_styler_to_excel_unstyled(engine, tmp_excel): ("font-style: italic;", ["font", "i"], True), ("text-decoration: underline;", ["font", "u"], "single"), ("number-format: $??,???.00;", ["number_format"], "$??,???.00"), + ('number-format: #,,"M";', ["number_format"], '#,,"M"'), ("text-align: left;", ["alignment", "horizontal"], "left"), ( "vertical-align: bottom;", diff --git a/pandas/tests/io/formats/test_css.py b/pandas/tests/io/formats/test_css.py index 642a562704344..47252a7d587e9 100644 --- a/pandas/tests/io/formats/test_css.py +++ b/pandas/tests/io/formats/test_css.py @@ -286,3 +286,23 @@ def test_css_relative_font_size(size, relative_to, resolved): else: inherited = {"font-size": relative_to} assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited) + + +def test_css_atomize_preserves_number_format_case(): + resolver = CSSResolver() + declarations = [("NUMBER-FORMAT", '#,,"M"')] + + props = dict(resolver.atomize(declarations)) + + assert "number-format" in props + assert props["number-format"] == '#,,"M"' + + +def test_css_parse_preserves_number_format_case(): + resolver = CSSResolver() + + css = 'NUMBER-FORMAT: #,,"M"; COLOR: Red' + result = dict(resolver.parse(css)) + + assert result["number-format"] == '#,,"M"' + assert result["color"] == "red"