cell_utils.py•18.2 kB
import re
from typing import Tuple, Optional, Any
import datetime
from spire.xls import *
def letter_to_column(column_letter: str) -> int:
    """
    Convert Excel column letter to column number.
    Example: A -> 1, Z -> 26, AA -> 27, etc.
    
    Args:
        column_letter: Excel column letter (A-Z, AA-ZZ, etc.)
    
    Returns:
        Column number (1-based index)
    """
    if not column_letter or not column_letter.isalpha():
        try:
            # If it's already a number, just return it
            return int(column_letter)
        except ValueError:
            raise ValueError(f"Invalid column letter: {column_letter}")
    column_letter = column_letter.upper()
    result = 0
    for char in column_letter:
        result = result * 26 + (ord(char) - ord('A') + 1)
    return result
def column_to_letter(column: int) -> str:
    """
    Convert column number to Excel column letter.
    Example: 1 -> A, 26 -> Z, 27 -> AA, etc.
    
    Args:
        column: Column number (1-based index)
    
    Returns:
        Excel column letter (A-Z, AA-ZZ, etc.)
    """
    if not isinstance(column, int) or column < 1:
        # If it's already a string, just return it
        if isinstance(column, str) and column.isalpha():
            return column
        raise ValueError(f"Invalid column number: {column}")
    result = ""
    while column > 0:
        remainder = (column - 1) % 26
        result = chr(ord('A') + remainder) + result
        column = (column - 1) // 26
    return result
def serialize_cell(cell, preview_only: bool):
    """Serialize a cell to a JSON-serializable dictionary object with null checks"""
    try:
        # Basic properties
        result = {
            "address": cell.RangeAddressLocal if hasattr(cell, "RangeAddressLocal") else None,
            "row": cell.Row if hasattr(cell, "Row") else None,
            "column": cell.Column if hasattr(cell, "Column") else None,
            "column_letter": column_to_letter(cell.Column) if hasattr(cell, "Column") else None,
            "value": None,
            "text": None,
            "formula": None,
            "has_formula": False
        }
        # Value handling (handle different types of values)
        try:
            result["value"] = cell.Value
        except:
            pass
        try:
            result["formula_value"] = cell.FormulaValue
        except:
            pass
        try:
            result["text"] = cell.Text
        except:
            pass
        # Formula handling
        try:
            result["has_formula"] = cell.HasFormula
            if cell.HasFormula:
                result["formula"] = cell.Formula
        except:
            pass
        if preview_only:
            return result
        # Style handling
        style_dict = {}
        # Font handling
        try:
            font_dict = {}
            if hasattr(cell, "Style") and hasattr(cell.Style, "Font"):
                font = cell.Style.Font
                if hasattr(font, "IsBold"):
                    font_dict["bold"] = font.IsBold
                if hasattr(font, "IsItalic"):
                    font_dict["italic"] = font.IsItalic
                if hasattr(font, "FontName"):
                    font_dict["name"] = font.FontName
                if hasattr(font, "Size"):
                    font_dict["size"] = font.Size
                if hasattr(font, "Color"):
                    try:
                        font_dict["color"] = {
                            "r": font.Color.R,
                            "g": font.Color.G,
                            "b": font.Color.B
                        }
                    except:
                        pass
                if hasattr(font, "Underline"):
                    font_dict["underline"] = str(font.Underline) != "None"
            if font_dict:
                style_dict["font"] = font_dict
        except:
            pass
        # Alignment handling
        try:
            if hasattr(cell, "Style"):
                if hasattr(cell.Style, "HorizontalAlignment"):
                    style_dict["horizontal_alignment"] = str(cell.Style.HorizontalAlignment)
                if hasattr(cell.Style, "VerticalAlignment"):
                    style_dict["vertical_alignment"] = str(cell.Style.VerticalAlignment)
                if hasattr(cell.Style, "WrapText"):
                    style_dict["wrap_text"] = cell.Style.WrapText
                if hasattr(cell.Style, "Rotation"):
                    style_dict["rotation"] = cell.Style.Rotation
                if hasattr(cell.Style, "IndentLevel"):
                    style_dict["indent_level"] = cell.Style.IndentLevel
        except:
            pass
        # Fill handling
        try:
            if hasattr(cell, "Style") and hasattr(cell.Style, "Interior"):
                interior_dict = {}
                interior = cell.Style.Interior
                if hasattr(interior, "Color"):
                    try:
                        interior_dict["color"] = {
                            "r": interior.Color.R,
                            "g": interior.Color.G,
                            "b": interior.Color.B
                        }
                    except:
                        pass
                if hasattr(interior, "FillPattern"):
                    interior_dict["pattern"] = str(interior.FillPattern)
                if interior_dict:
                    style_dict["fill"] = interior_dict
        except:
            pass
        # Borders handling
        try:
            if hasattr(cell, "Style") and hasattr(cell.Style, "Borders"):
                borders_dict = {}
                borders = cell.Style.Borders
                if hasattr(borders, "LineStyle"):
                    borders_dict["line_style"] = str(borders.LineStyle)
                if hasattr(borders, "Color"):
                    try:
                        borders_dict["color"] = {
                            "r": borders.Color.R,
                            "g": borders.Color.G,
                            "b": borders.Color.B
                        }
                    except:
                        pass
                # Individual borders
                border_positions = ["Top", "Bottom", "Left", "Right"]
                for pos in border_positions:
                    try:
                        if hasattr(borders, pos):
                            border = getattr(borders, pos)
                            border_info = {}
                            if hasattr(border, "LineStyle"):
                                border_info["line_style"] = str(border.LineStyle)
                            if hasattr(border, "Color"):
                                try:
                                    border_info["color"] = {
                                        "r": border.Color.R,
                                        "g": border.Color.G,
                                        "b": border.Color.B
                                    }
                                except:
                                    pass
                            if border_info:
                                borders_dict[pos.lower()] = border_info
                    except:
                        pass
                if borders_dict:
                    style_dict["borders"] = borders_dict
        except:
            pass
        # Number format
        try:
            if hasattr(cell, "Style") and hasattr(cell.Style, "NumberFormat"):
                style_dict["number_format"] = cell.Style.NumberFormat
        except:
            pass
        # Cell protection
        try:
            if hasattr(cell, "Style"):
                protection_dict = {}
                if hasattr(cell.Style, "HideFormula"):
                    protection_dict["hide_formula"] = cell.Style.HideFormula
                if protection_dict:
                    style_dict["protection"] = protection_dict
        except:
            pass
        # Add style to result
        if style_dict:
            result["style"] = style_dict
        # Cell type
        try:
            if hasattr(cell, "Type"):
                result["cell_type"] = str(cell.Type)
        except:
            pass
        # Merged cells information
        try:
            if hasattr(cell, "IsMerged"):
                result["is_merged"] = cell.IsMerged
                if cell.IsMerged and hasattr(cell, "MergeArea"):
                    try:
                        area = cell.MergeArea
                        result["merge_area"] = {
                            "first_row": area.FirstRow,
                            "first_column": area.FirstColumn,
                            "last_row": area.LastRow,
                            "last_column": area.LastColumn
                        }
                    except:
                        pass
        except:
            pass
        return result
    except Exception as e:
        # Return basic information when any exception occurs
        return {
            "error": f"Failed to serialize cell: {str(e)}",
            "row": getattr(cell, "Row", None),
            "column": getattr(cell, "Column", None),
            "value": getattr(cell, "Value", None)
        }
def parse_cell_range(
        start_cell: str,
        end_cell: Optional[str] = None,
        workbook: Workbook = Workbook()
) -> Tuple[int, int, Optional[int], Optional[int]]:
    """Parse Excel cell references into row and column numbers."""
    try:
        start_cell = workbook.Worksheets[0].Range[start_cell]
    except:
        raise ValueError(f"Invalid start cell reference: {start_cell}")
    start_col = start_cell.Column
    start_row = start_cell.Row
    # Parse end cell if provided
    end_col = None
    end_row = None
    if end_cell:
        try:
            end_cell = workbook.Worksheets[0].Range[end_cell]
        except:
            raise ValueError(f"Invalid end cell reference: {end_cell}")
        end_col = end_cell.Column
        end_row = end_cell.Row
    return start_row, start_col, end_row, end_col
def validate_cell_reference_regex(cell_ref: str) -> bool:
    """Validate Excel cell reference format."""
    if not cell_ref:
        return False
    # Basic format validation
    pattern = r'^[A-Za-z]{1,3}[1-9][0-9]*$'
    if not re.match(pattern, cell_ref):
        return False
    return True
class EnumMapper:
    SUBTOTAL_MAP = {
        "sum": SubtotalTypes.Sum,
        "average": SubtotalTypes.Average,
        "count": SubtotalTypes.Count,
        "min": SubtotalTypes.Min,
        "max": SubtotalTypes.Max
    }
    # Filter operator type mapping
    FILTER_OPERATOR_MAP = {
        "=": FilterOperatorType.Equal,
        ">": FilterOperatorType.GreaterThan,
        "<": FilterOperatorType.LessThan,
        ">=": FilterOperatorType.GreaterOrEqual,
        "<=": FilterOperatorType.LessOrEqual,
        "<>": FilterOperatorType.NotEqual
    }
    # condition format operator 
    OPERATOR_MAP = {
        "greater": ComparisonOperatorType.Greater,
        "gt": ComparisonOperatorType.Greater,
        ">": ComparisonOperatorType.Greater,
        "大于": ComparisonOperatorType.Greater,
        "greater than": ComparisonOperatorType.Greater,
        "greater_or_equal": ComparisonOperatorType.GreaterOrEqual,
        ">=": ComparisonOperatorType.GreaterOrEqual,
        "ge": ComparisonOperatorType.GreaterOrEqual,
        "大于等于": ComparisonOperatorType.GreaterOrEqual,
        "less": ComparisonOperatorType.Less,
        "lt": ComparisonOperatorType.Less,
        "<": ComparisonOperatorType.Less,
        "小于": ComparisonOperatorType.Less,
        "less than": ComparisonOperatorType.Less,
        "less_or_equal": ComparisonOperatorType.LessOrEqual,
        "le": ComparisonOperatorType.LessOrEqual,
        "<=": ComparisonOperatorType.LessOrEqual,
        "小于等于": ComparisonOperatorType.LessOrEqual,
        "equal": ComparisonOperatorType.Equal,
        "eq": ComparisonOperatorType.Equal,
        "=": ComparisonOperatorType.Equal,
        "等于": ComparisonOperatorType.Equal,
        "not_equal": ComparisonOperatorType.NotEqual,
        "ne": ComparisonOperatorType.NotEqual,
        "!=": ComparisonOperatorType.NotEqual,
        "<>": ComparisonOperatorType.NotEqual,
        "不等于": ComparisonOperatorType.NotEqual,
    }
    # alignment
    ALIGNMENT_MAP = {
        "left": HorizontalAlignType.Left,
        "居左": HorizontalAlignType.Left,
        "center": HorizontalAlignType.Center,
        "居中": HorizontalAlignType.Center,
        "right": HorizontalAlignType.Right,
        "居右": HorizontalAlignType.Right,
        "justify": HorizontalAlignType.Justify,
        "两端对齐": HorizontalAlignType.Justify,
    }
    # border line style
    BORDER_STYLE_MAP = {
        "thin": LineStyleType.Thin,
        "细线": LineStyleType.Thin,
        "medium": LineStyleType.Medium,
        "中线": LineStyleType.Medium,
        "thick": LineStyleType.Thick,
        "粗线": LineStyleType.Thick,
        "double": LineStyleType.Double,
        "双线": LineStyleType.Double,
    }
    # chart type
    CHART_TYPE_MAP = {
        "column": ExcelChartType.ColumnClustered,
        "bar": ExcelChartType.BarClustered,
        "line": ExcelChartType.Line,
        "pie": ExcelChartType.Pie,
        "area": ExcelChartType.Area,
        "scatter": ExcelChartType.ScatterLine,
        "doughnut": ExcelChartType.Doughnut,
        "waterfall": ExcelChartType.WaterFall,
        "column_stacked": ExcelChartType.ColumnStacked,
        "column_100_percent_stacked": ExcelChartType.Column100PercentStacked,
        "bubble": ExcelChartType.Bubble,
        "funnel": ExcelChartType.Funnel,
        "treemap": ExcelChartType.TreeMap,
        "sunburst": ExcelChartType.SunBurst,
        "histogram": ExcelChartType.Histogram,
        "box_and_whisker": ExcelChartType.BoxAndWhisker,
    }
    # conditional format type
    CONDITION_TYPE_MAP = {
        "cell": ConditionalFormatType.CellValue,
        "单元格值": ConditionalFormatType.CellValue,
        "text": ConditionalFormatType.ContainsText,
        "文本": ConditionalFormatType.ContainsText,
        "date": ConditionalFormatType.TimePeriod,
        "日期": ConditionalFormatType.TimePeriod,
        "time_period": ConditionalFormatType.TimePeriod,
        "时间段": ConditionalFormatType.TimePeriod,
        "average": ConditionalFormatType.Average,
        "平均值": ConditionalFormatType.Average,
        "duplicate": ConditionalFormatType.DuplicateValues,
        "重复值": ConditionalFormatType.DuplicateValues,
        "unique": ConditionalFormatType.UniqueValues,
        "唯一值": ConditionalFormatType.UniqueValues,
        "formula": ConditionalFormatType.Formula,
        "公式": ConditionalFormatType.Formula,
        "top10": ConditionalFormatType.TopBottom,
        "前10项": ConditionalFormatType.TopBottom,
        "data_bar": ConditionalFormatType.DataBar,
        "数据条": ConditionalFormatType.DataBar,
        "color_scale": ConditionalFormatType.ColorScale,
        "色阶": ConditionalFormatType.ColorScale,
        "icon_set": ConditionalFormatType.IconSet,
        "图标集": ConditionalFormatType.IconSet
    }
    @staticmethod
    def smart_enum_map(input_str: str, mapping: dict, default):
        if not input_str:
            return default
        key = input_str.strip().lower()
        return mapping.get(key, default)
    @classmethod
    def get_operator_enum(cls, op_str: str) -> ComparisonOperatorType:
        return cls.smart_enum_map(op_str, cls.OPERATOR_MAP, ComparisonOperatorType.Greater)
    @classmethod
    def get_alignment_enum(cls, align_str: str) -> HorizontalAlignType:
        return cls.smart_enum_map(align_str, cls.ALIGNMENT_MAP, HorizontalAlignType.Left)
    @classmethod
    def get_border_style_enum(cls, style_str: str) -> LineStyleType:
        return cls.smart_enum_map(style_str, cls.BORDER_STYLE_MAP, LineStyleType.Thin)
    @classmethod
    def get_chart_type_enum(cls, chart_str: str) -> ExcelChartType:
        return cls.smart_enum_map(chart_str, cls.CHART_TYPE_MAP, ExcelChartType.ColumnClustered)
    @classmethod
    def get_condition_enum(cls, type_str: str) -> ConditionalFormatType:
        return cls.smart_enum_map(type_str, cls.CONDITION_TYPE_MAP, ConditionalFormatType.CellValue)
    @classmethod
    def get_subtotal_enum(cls, func_str: str) -> SubtotalTypes:
        return cls.smart_enum_map(func_str, cls.SUBTOTAL_MAP, SubtotalTypes.Sum)
    @classmethod
    def get_filter_operator_enum(cls, op_str: str) -> FilterOperatorType:
        return cls.smart_enum_map(op_str, cls.FILTER_OPERATOR_MAP, FilterOperatorType.Equal)
def create_spire_object(value: Any) -> Any:
    """
    Create corresponding SpireObject based on the input value's type.
    Args:
        value: The value to be converted to SpireObject
    Returns:
        SpireObject instance of corresponding type
    Examples:
        >>> create_spire_object(123)  # Returns Int32(123)
        >>> create_spire_object(123.45)  # Returns Double(123.45)
        >>> create_spire_object("text")  # Returns String("text")
        >>> create_spire_object(True)  # Returns Boolean(True)
        >>> create_spire_object(datetime.datetime(2023, 1, 1))  # Returns DateTime(2023, 1, 1)
    """
    if value is None:
        return String("")
    if isinstance(value, bool):
        return Boolean(value)
    if isinstance(value, int):
        if -2147483648 <= value <= 2147483647:
            return Int32(value)
        else:
            return Int64(value)
    if isinstance(value, float):
        return Double(value)
    if isinstance(value, str):
        return String(value)
    if isinstance(value, (datetime.datetime, datetime.date)):
        return DateTime(value.year, value.month, value.day,
                        getattr(value, 'hour', 0),
                        getattr(value, 'minute', 0),
                        getattr(value, 'second', 0),
                        getattr(value, 'microsecond', 0))
    return String(str(value))