From 3da5bc37da33f8778233d6ca0a1b7b5504a6d86a Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Dec 2024 10:10:53 +0800 Subject: [PATCH] Add 4 new functions: move_sheet, new_conditional_style, set_conditional_format and set_default_font - Update unit tests and docs for the function --- .github/workflows/codeql-analysis.yml | 35 --------- excelize.py | 102 +++++++++++++++++++++++++- main.go | 85 +++++++++++++++++++++ test_excelize.py | 33 +++++++++ types_c.h | 37 +++++++++- types_go.py | 29 ++++++++ types_py.py | 28 +++++++ 7 files changed, 309 insertions(+), 40 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 96a29a2..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '0 6 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-24.04 - - strategy: - fail-fast: false - matrix: - language: ['go', 'python'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/excelize.py b/excelize.py index a299b86..3e4e53c 100644 --- a/excelize.py +++ b/excelize.py @@ -319,7 +319,7 @@ def py_value_to_c(py_instance, ctypes_instance): # Pointer of the Go data type, for example: *excelize.Options or *string value = getattr(py_instance, py_field_name) c_type = get_c_field_type(ctypes_instance, c_field_name)._type_ - if value: + if value is not None: if any(is_py_primitive_type(arg) for arg in py_field_args): # Pointer of the Go basic data type, for example: *string setattr( @@ -1379,6 +1379,55 @@ def merge_cell( ).decode(ENCODE) return None if err == "" else Exception(err) + def move_sheet(self, source: str, target: str) -> Optional[Exception]: + """ + Moves a sheet to a specified position in the workbook. The function + moves the source sheet before the target sheet. After moving, other + sheets will be shifted to the left or right. If the sheet is already at + the target position, the function will not perform any action. Not that + this function will be ungroup all sheets after moving. + + Args: + source (str): The source worksheet name + target (str): The target worksheet name + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + + Example: + For example, move Sheet2 before Sheet1: + + .. code-block:: python + + err = f.move_sheet("Sheet2", "Sheet1") + """ + lib.MoveSheet.restype = c_char_p + err = lib.MoveSheet( + self.file_index, + source.encode(ENCODE), + target.encode(ENCODE), + ).decode(ENCODE) + return None if err == "" else Exception(err) + + def new_conditional_style(self, style: Style) -> Tuple[int, Optional[Exception]]: + """ + Create style for conditional format by given style format. The + parameters are the same with the new_style function. + + Args: + style (Style): The style options + + Returns: + Tuple[int, Optional[Exception]]: A tuple containing the style index + and an exception if any error occurs. + """ + lib.NewConditionalStyle.restype = types_go._NewStyleResult + options = py_value_to_c(style, types_go._Style()) + res = lib.NewConditionalStyle(self.file_index, byref(options)) + err = res.err.decode(ENCODE) + return res.style, None if err == "" else Exception(err) + def new_sheet(self, sheet: str) -> Tuple[int, Optional[Exception]]: """ Create a new sheet by given a worksheet name and returns the index of @@ -1949,6 +1998,57 @@ def set_col_width( ).decode(ENCODE) return None if err == "" else Exception(err) + def set_conditional_format( + self, + sheet: str, + range_ref: str, + opts: List[ConditionalFormatOptions], + ) -> Optional[Exception]: + """ + Create conditional formatting rule for cell value. Conditional + formatting is a feature of Excel which allows you to apply a format to a + cell or a range of cells based on certain criteria. + + Args: + sheet (str): The worksheet name + range_ref (str): The top-left and right-bottom cell range reference + opts (List[ConditionalFormatOptions]): The conditional format options + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.SetConditionalFormat.restype = c_char_p + vals = (types_go._ConditionalFormatOptions * len(opts))() + for i, value in enumerate(opts): + vals[i] = py_value_to_c(value, types_go._ConditionalFormatOptions()) + err = lib.SetConditionalFormat( + self.file_index, + sheet.encode(ENCODE), + range_ref.encode(ENCODE), + byref(vals), + len(vals), + ).decode(ENCODE) + return None if err == "" else Exception(err) + + def set_default_font(self, font_name: str) -> Optional[Exception]: + """ + Set the default font name in the workbook. The spreadsheet generated by + excelize default font is Calibri. + + Args: + font_name (str): The font name + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.SetDefaultFont.restype = c_char_p + err = lib.SetDefaultFont(self.file_index, font_name.encode(ENCODE)).decode( + ENCODE + ) + return None if err == "" else Exception(err) + def set_defined_name(self, defined_name: DefinedName) -> Optional[Exception]: """ Set the defined names of the workbook or worksheet. If not specified diff --git a/main.go b/main.go index 06ac6a9..edfa996 100644 --- a/main.go +++ b/main.go @@ -162,6 +162,10 @@ func cToGoArray(cArray reflect.Value, cArrayLen int) reflect.Value { val := cArray.Interface().(*C.struct_ChartSeries) arr := unsafe.Slice(val, cArrayLen) return reflect.ValueOf(arr) + case "main._Ctype_struct_ConditionalFormatOptions": + val := cArray.Interface().(*C.struct_ConditionalFormatOptions) + arr := unsafe.Slice(val, cArrayLen) + return reflect.ValueOf(arr) case "main._Ctype_struct_PivotTableField": val := cArray.Interface().(*C.struct_PivotTableField) arr := unsafe.Slice(val, cArrayLen) @@ -1132,6 +1136,47 @@ func MergeCell(idx int, sheet, topLeftCell, bottomRightCell *C.char) *C.char { return C.CString(errNil) } +// MoveSheet moves a sheet to a specified position in the workbook. The function +// moves the source sheet before the target sheet. After moving, other sheets +// will be shifted to the left or right. If the sheet is already at the target +// position, the function will not perform any action. Not that this function +// will be ungroup all sheets after moving. +// +//export MoveSheet +func MoveSheet(idx int, source, target *C.char) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString("") + } + if err := f.(*excelize.File).MoveSheet(C.GoString(source), C.GoString(target)); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) +} + +// NewConditionalStyle provides a function to create style for conditional +// format by given style format. The parameters are the same with the NewStyle +// function. +// +//export NewConditionalStyle +func NewConditionalStyle(idx int, style *C.struct_Style) C.struct_NewStyleResult { + var s excelize.Style + goVal, err := cValueToGo(reflect.ValueOf(*style), reflect.TypeOf(excelize.Style{})) + if err != nil { + return C.struct_NewStyleResult{style: C.int(0), err: C.CString(err.Error())} + } + s = goVal.Elem().Interface().(excelize.Style) + f, ok := files.Load(idx) + if !ok { + return C.struct_NewStyleResult{style: C.int(0), err: C.CString(errFilePtr)} + } + styleID, err := f.(*excelize.File).NewConditionalStyle(&s) + if err != nil { + return C.struct_NewStyleResult{style: C.int(styleID), err: C.CString(err.Error())} + } + return C.struct_NewStyleResult{style: C.int(styleID), err: C.CString(errNil)} +} + // NewFile provides a function to create new file by default template. // //export NewFile @@ -1544,6 +1589,46 @@ func SetColWidth(idx int, sheet, startCol, endCol *C.char, width float64) *C.cha return C.CString(errNil) } +// SetConditionalFormat provides a function to create conditional formatting +// rule for cell value. Conditional formatting is a feature of Excel which +// allows you to apply a format to a cell or a range of cells based on certain +// criteria. +// +//export SetConditionalFormat +func SetConditionalFormat(idx int, sheet, rangeRef *C.char, opts *C.struct_ConditionalFormatOptions, length int) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString("") + } + options := make([]excelize.ConditionalFormatOptions, length) + for i, val := range unsafe.Slice(opts, length) { + goVal, err := cValueToGo(reflect.ValueOf(val), reflect.TypeOf(excelize.ConditionalFormatOptions{})) + if err != nil { + return C.CString(err.Error()) + } + options[i] = goVal.Elem().Interface().(excelize.ConditionalFormatOptions) + } + if err := f.(*excelize.File).SetConditionalFormat(C.GoString(sheet), C.GoString(rangeRef), options); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) +} + +// SetDefaultFont provides the default font name currently set in the +// workbook. The spreadsheet generated by excelize default font is Calibri. +// +//export SetDefaultFont +func SetDefaultFont(idx int, fontName *C.char) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString(errFilePtr) + } + if err := f.(*excelize.File).SetDefaultFont(C.GoString(fontName)); err != nil { + C.CString(err.Error()) + } + return C.CString(errNil) +} + // SetDefinedName provides a function to set the defined names of the workbook // or worksheet. If not specified scope, the default scope is workbook. // diff --git a/test_excelize.py b/test_excelize.py index bf4a520..e7845bc 100644 --- a/test_excelize.py +++ b/test_excelize.py @@ -68,6 +68,11 @@ def test_app_props(self): self.assertEqual(props.application, "Go Excelize") self.assertIsNone(err) + def test_default_font(self): + f = excelize.new_file() + font_name = "Arial" + self.assertIsNone(f.set_default_font(font_name)) + def test_style(self): f = excelize.new_file() s = excelize.Style( @@ -257,6 +262,7 @@ def test_style(self): ], ) + self.assertIsNone(f.move_sheet("Sheet2", "Sheet1")) self.assertIsNone(f.ungroup_sheets()) self.assertIsNone(f.update_linked_value()) self.assertIsNone(f.save()) @@ -807,6 +813,33 @@ def test_cell_rich_text(self): self.assertIsNone(f.save_as(os.path.join("test", "TestCellRichText.xlsx"))) self.assertIsNone(f.close()) + def test_conditional_format(self): + f = excelize.new_file() + format, err = f.new_conditional_style( + excelize.Style( + font=excelize.Font(color="9A0511"), + fill=excelize.Fill(type="pattern", color=["FEC7CE"], pattern=1), + ) + ) + self.assertIsNone(err) + self.assertIsNone( + f.set_conditional_format( + "Sheet1", + "A1:A10", + [ + excelize.ConditionalFormatOptions( + type="cell", + criteria="between", + format=format, + min_value="6", + max_value="8", + ) + ], + ) + ) + self.assertIsNone(f.save_as(os.path.join("test", "TestConditionalFormat.xlsx"))) + self.assertIsNone(f.close()) + def test_column_name_to_number(self): col, err = excelize.column_name_to_number("Z") self.assertEqual(col, 26) diff --git a/types_c.h b/types_c.h index 9f1788e..1db4ea1 100644 --- a/types_c.h +++ b/types_c.h @@ -194,11 +194,11 @@ struct GraphicOptions // Picture maps the format settings of the picture. struct Picture { - char *Extension; + char *Extension; int FileLen; - unsigned char *File; - struct GraphicOptions *Format; - unsigned char InsertType; + unsigned char *File; + struct GraphicOptions *Format; + unsigned char InsertType; }; // RichTextRun directly maps the settings of the rich text run. @@ -221,6 +221,35 @@ struct Comment struct RichTextRun *Paragraph; }; +// ConditionalFormatOptions directly maps the conditional format settings of the cells. +struct ConditionalFormatOptions +{ + char *Type; + bool AboveAverage; + bool Percent; + int *Format; + char *Criteria; + char *Value; + char *MinType; + char *MidType; + char *MaxType; + char *MinValue; + char *MidValue; + char *MaxValue; + char *MinColor; + char *MidColor; + char *MaxColor; + char *BarColor; + char *BarBorderColor; + char *BarDirection; + bool BarOnly; + bool BarSolid; + char *IconStyle; + bool ReverseIcons; + bool IconsOnly; + bool StopIfTrue; +}; + // FormControl directly maps the form controls information. struct FormControl { diff --git a/types_go.py b/types_go.py index 6c1c8bd..532186a 100644 --- a/types_go.py +++ b/types_go.py @@ -198,6 +198,35 @@ class _Comment(Structure): ] +class _ConditionalFormatOptions(Structure): + _fields_ = [ + ("Type", c_char_p), + ("AboveAverage", c_bool), + ("Percent", c_bool), + ("Format", POINTER(c_int)), + ("Criteria", c_char_p), + ("Value", c_char_p), + ("MinType", c_char_p), + ("MidType", c_char_p), + ("MaxType", c_char_p), + ("MinValue", c_char_p), + ("MidValue", c_char_p), + ("MaxValue", c_char_p), + ("MinColor", c_char_p), + ("MidColor", c_char_p), + ("MaxColor", c_char_p), + ("BarColor", c_char_p), + ("BarBorderColor", c_char_p), + ("BarDirection", c_char_p), + ("BarOnly", c_bool), + ("BarSolid", c_bool), + ("IconStyle", c_char_p), + ("ReverseIcons", c_bool), + ("IconsOnly", c_bool), + ("StopIfTrue", c_bool), + ] + + class _FormControl(Structure): _fields_ = [ ("Cell", c_char_p), diff --git a/types_py.py b/types_py.py index 95e6ff0..6467312 100644 --- a/types_py.py +++ b/types_py.py @@ -336,6 +336,34 @@ class Comment: paragraph: Optional[List[RichTextRun]] = None +@dataclass +class ConditionalFormatOptions: + type: str = "" + above_average: bool = False + percent: bool = False + format: Optional[int] = None + criteria: str = "" + value: str = "" + min_type: str = "" + mid_type: str = "" + max_type: str = "" + min_value: str = "" + mid_value: str = "" + max_value: str = "" + min_color: str = "" + mid_color: str = "" + max_color: str = "" + bar_color: str = "" + bar_border_color: str = "" + bar_direction: str = "" + bar_only: bool = False + bar_solid: bool = False + icon_style: str = "" + reverse_icons: bool = False + icons_only: bool = False + stop_if_true: bool = False + + @dataclass class FormControl: cell: str = ""