From aef20e226cba5cab0fc9331c63f34f563cd24ebc Mon Sep 17 00:00:00 2001 From: Roman Shevelev Date: Tue, 4 Mar 2025 18:20:24 +0500 Subject: [PATCH] Introduce 2 new functions SetCalcProps and GetCalcProps (#2098) - Add new CalcPropsOptions data type - Support assign exported data structure fields value to internal data structure fields dynamically by specified fields name list - Simplify code for function SetAppProps, SetDocProps, SetWorkbookProps, GetWorkbookProps and getPivotTable - Update unit tests --- docProps.go | 57 +++++++-------------------------- lib.go | 49 +++++++++++++++++++++++++++++ pivotTable.go | 25 ++++----------- workbook.go | 82 +++++++++++++++++++++++++++++++++++++----------- workbook_test.go | 36 +++++++++++++++++++++ xmlWorkbook.go | 20 +++++++++++- 6 files changed, 185 insertions(+), 84 deletions(-) diff --git a/docProps.go b/docProps.go index 4f0eef9..b4f3cfa 100644 --- a/docProps.go +++ b/docProps.go @@ -65,34 +65,15 @@ import ( // AppVersion: "16.0000", // }) func (f *File) SetAppProps(appProperties *AppProperties) error { - var ( - app *xlsxProperties - err error - field string - fields []string - immutable, mutable reflect.Value - output []byte - ) - app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). + app := new(xlsxProperties) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { return err } - fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} - immutable, mutable = reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem() - for _, field = range fields { - immutableField := immutable.FieldByName(field) - switch immutableField.Kind() { - case reflect.Bool: - mutable.FieldByName(field).SetBool(immutableField.Bool()) - case reflect.Int: - mutable.FieldByName(field).SetInt(immutableField.Int()) - default: - mutable.FieldByName(field).SetString(immutableField.String()) - } - } + setNoPtrFieldsVal([]string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"}, + reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem()) app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value - output, err = xml.Marshal(app) + output, err := xml.Marshal(app) f.saveFileList(defaultXMLPathDocPropsApp, output) return err } @@ -180,22 +161,12 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // Version: "1.0.0", // }) func (f *File) SetDocProps(docProperties *DocProperties) error { - var ( - core *decodeCoreProperties - err error - field, val string - fields []string - immutable, mutable reflect.Value - newProps *xlsxCoreProperties - output []byte - ) - - core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). + core := new(decodeCoreProperties) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { return err } - newProps = &xlsxCoreProperties{ + newProps := &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, Dcmitype: NameSpaceDublinCoreMetadataInitiative, @@ -219,23 +190,17 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { if core.Modified != nil { newProps.Modified = &xlsxDcTerms{Type: core.Modified.Type, Text: core.Modified.Text} } - fields = []string{ + setNoPtrFieldsVal([]string{ "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", - } - immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem() - for _, field = range fields { - if val = immutable.FieldByName(field).String(); val != "" { - mutable.FieldByName(field).SetString(val) - } - } + }, reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem()) if docProperties.Created != "" { newProps.Created = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Created} } if docProperties.Modified != "" { newProps.Modified = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Modified} } - output, err = xml.Marshal(newProps) + output, err := xml.Marshal(newProps) f.saveFileList(defaultXMLPathDocPropsCore, output) return err diff --git a/lib.go b/lib.go index bfb2ae2..207cd83 100644 --- a/lib.go +++ b/lib.go @@ -21,6 +21,7 @@ import ( "math" "math/big" "os" + "reflect" "regexp" "strconv" "strings" @@ -878,6 +879,54 @@ func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat { return res } +// assignFieldValue assigns the value from an immutable reflect.Value to a +// mutable reflect.Value based on the type of the immutable value. +func assignFieldValue(field string, immutable, mutable reflect.Value) { + switch immutable.Kind() { + case reflect.Bool: + mutable.FieldByName(field).SetBool(immutable.Bool()) + case reflect.Int: + mutable.FieldByName(field).SetInt(immutable.Int()) + default: + mutable.FieldByName(field).SetString(immutable.String()) + } +} + +// setNoPtrFieldsVal assigns values from the pointer or no-pointer structs +// fields (immutable) value to no-pointer struct field. +func setNoPtrFieldsVal(fields []string, immutable, mutable reflect.Value) { + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.Ptr { + if immutableField.IsValid() && !immutableField.IsNil() { + assignFieldValue(field, immutableField.Elem(), mutable) + } + continue + } + assignFieldValue(field, immutableField, mutable) + } +} + +// setPtrFieldsVal assigns values from the pointer or no-pointer structs +// fields (immutable) value to pointer struct field. +func setPtrFieldsVal(fields []string, immutable, mutable reflect.Value) { + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.Ptr { + if immutableField.IsValid() && !immutableField.IsNil() { + mutable.FieldByName(field).Set(immutableField.Elem()) + } + continue + } + if immutableField.IsZero() { + continue + } + ptr := reflect.New(immutableField.Type()) + ptr.Elem().Set(immutableField) + mutable.FieldByName(field).Set(ptr) + } +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/pivotTable.go b/pivotTable.go index bfd4d40..c71ca9f 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -883,14 +883,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.DataRange = pc.CacheSource.WorksheetSource.Name _ = f.getPivotTableDataRange(&opts) } - fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"} - immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem() - for _, field := range fields { - immutableField := immutable.FieldByName(field) - if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { - mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) - } - } + setPtrFieldsVal([]string{ + "RowGrandTotals", "ColGrandTotals", "ShowDrill", + "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError", + }, reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem()) if si := pt.PivotTableStyleInfo; si != nil { opts.ShowRowHeaders = si.ShowRowHeaders opts.ShowColHeaders = si.ShowColHeaders @@ -982,17 +978,8 @@ func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { ShowAll: fld.ShowAll, InsertBlankRow: fld.InsertBlankRow, } - fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"} - immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem() - for _, field := range fields { - immutableField := immutable.FieldByName(field) - if immutableField.Kind() == reflect.String { - mutable.FieldByName(field).SetString(immutableField.String()) - } - if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { - mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) - } - } + setPtrFieldsVal([]string{"Compact", "Name", "Outline", "DefaultSubtotal"}, + reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem()) return pivotTableField } diff --git a/workbook.go b/workbook.go index c40e957..c908241 100644 --- a/workbook.go +++ b/workbook.go @@ -16,12 +16,16 @@ import ( "encoding/xml" "io" "path/filepath" + "reflect" "strconv" "strings" ) // SetWorkbookProps provides a function to sets workbook properties. func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { + if opts == nil { + return nil + } wb, err := f.workbookReader() if err != nil { return err @@ -29,19 +33,9 @@ func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { if wb.WorkbookPr == nil { wb.WorkbookPr = new(xlsxWorkbookPr) } - if opts == nil { - return nil - } - if opts.Date1904 != nil { - wb.WorkbookPr.Date1904 = *opts.Date1904 - } - if opts.FilterPrivacy != nil { - wb.WorkbookPr.FilterPrivacy = *opts.FilterPrivacy - } - if opts.CodeName != nil { - wb.WorkbookPr.CodeName = *opts.CodeName - } - return nil + setNoPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"}, + reflect.ValueOf(*opts), reflect.ValueOf(wb.WorkbookPr).Elem()) + return err } // GetWorkbookProps provides a function to gets workbook properties. @@ -51,11 +45,63 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { if err != nil { return opts, err } - if wb.WorkbookPr != nil { - opts.Date1904 = boolPtr(wb.WorkbookPr.Date1904) - opts.FilterPrivacy = boolPtr(wb.WorkbookPr.FilterPrivacy) - opts.CodeName = stringPtr(wb.WorkbookPr.CodeName) + if wb.WorkbookPr == nil { + return opts, err } + setPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"}, + reflect.ValueOf(*wb.WorkbookPr), reflect.ValueOf(&opts).Elem()) + return opts, err +} + +// SetCalcProps provides a function to sets calculation properties. +func (f *File) SetCalcProps(opts *CalcPropsOptions) error { + if opts == nil { + return nil + } + wb, err := f.workbookReader() + if err != nil { + return err + } + if wb.CalcPr == nil { + wb.CalcPr = new(xlsxCalcPr) + } + setNoPtrFieldsVal([]string{ + "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate", + "IterateDelta", + "CalcMode", "RefMode", + }, reflect.ValueOf(*opts), reflect.ValueOf(wb.CalcPr).Elem()) + if opts.CalcID != nil { + wb.CalcPr.CalcID = int(*opts.CalcID) + } + if opts.ConcurrentManualCount != nil { + wb.CalcPr.ConcurrentManualCount = int(*opts.ConcurrentManualCount) + } + if opts.IterateCount != nil { + wb.CalcPr.IterateCount = int(*opts.IterateCount) + } + wb.CalcPr.ConcurrentCalc = opts.ConcurrentCalc + return err +} + +// GetCalcProps provides a function to gets calculation properties. +func (f *File) GetCalcProps() (CalcPropsOptions, error) { + var opts CalcPropsOptions + wb, err := f.workbookReader() + if err != nil { + return opts, err + } + if wb.CalcPr == nil { + return opts, err + } + setPtrFieldsVal([]string{ + "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate", + "IterateDelta", + "CalcMode", "RefMode", + }, reflect.ValueOf(*wb.CalcPr), reflect.ValueOf(&opts).Elem()) + opts.CalcID = uintPtr(uint(wb.CalcPr.CalcID)) + opts.ConcurrentManualCount = uintPtr(uint(wb.CalcPr.ConcurrentManualCount)) + opts.IterateCount = uintPtr(uint(wb.CalcPr.IterateCount)) + opts.ConcurrentCalc = wb.CalcPr.ConcurrentCalc return opts, err } @@ -99,7 +145,7 @@ func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error { wb.WorkbookProtection.WorkbookHashValue = hashValue wb.WorkbookProtection.WorkbookSpinCount = int(workbookProtectionSpinCount) } - return nil + return err } // UnprotectWorkbook provides a function to remove protection for workbook, diff --git a/workbook_test.go b/workbook_test.go index 73dda6c..2e3baac 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -21,6 +21,10 @@ func TestWorkbookProps(t *testing.T) { opts, err := f.GetWorkbookProps() assert.NoError(t, err) assert.Equal(t, expected, opts) + wb.WorkbookPr = nil + opts, err = f.GetWorkbookProps() + assert.NoError(t, err) + assert.Equal(t, WorkbookPropsOptions{}, opts) // Test set workbook properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) @@ -32,6 +36,38 @@ func TestWorkbookProps(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } +func TestCalcProps(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCalcProps(nil)) + wb, err := f.workbookReader() + assert.NoError(t, err) + wb.CalcPr = nil + expected := CalcPropsOptions{ + FullCalcOnLoad: boolPtr(true), + CalcID: uintPtr(122211), + ConcurrentManualCount: uintPtr(5), + IterateCount: uintPtr(10), + ConcurrentCalc: boolPtr(true), + } + assert.NoError(t, f.SetCalcProps(&expected)) + opts, err := f.GetCalcProps() + assert.NoError(t, err) + assert.Equal(t, expected, opts) + wb.CalcPr = nil + opts, err = f.GetCalcProps() + assert.NoError(t, err) + assert.Equal(t, CalcPropsOptions{}, opts) + // Test set workbook properties with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCalcProps(&expected), "XML syntax error on line 1: invalid UTF-8") + // Test get workbook properties with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + _, err = f.GetCalcProps() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} + func TestDeleteWorkbookRels(t *testing.T) { f := NewFile() // Test delete pivot table without worksheet relationships diff --git a/xmlWorkbook.go b/xmlWorkbook.go index c36485a..6a6b501 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -315,7 +315,7 @@ type xlsxDefinedName struct { // displaying the results as values in the cells that contain the formulas. type xlsxCalcPr struct { CalcCompleted bool `xml:"calcCompleted,attr,omitempty"` - CalcID string `xml:"calcId,attr,omitempty"` + CalcID int `xml:"calcId,attr,omitempty"` CalcMode string `xml:"calcMode,attr,omitempty"` CalcOnSave bool `xml:"calcOnSave,attr,omitempty"` ConcurrentCalc *bool `xml:"concurrentCalc,attr"` @@ -384,6 +384,24 @@ type DefinedName struct { Scope string } +// CalcPropsOptions defines the collection of properties the application uses to +// record calculation status and details. +type CalcPropsOptions struct { + CalcID *uint `xml:"calcId,attr"` + CalcMode *string `xml:"calcMode,attr"` + FullCalcOnLoad *bool `xml:"fullCalcOnLoad,attr"` + RefMode *string `xml:"refMode,attr"` + Iterate *bool `xml:"iterate,attr"` + IterateCount *uint `xml:"iterateCount,attr"` + IterateDelta *float64 `xml:"iterateDelta,attr"` + FullPrecision *bool `xml:"fullPrecision,attr"` + CalcCompleted *bool `xml:"calcCompleted,attr"` + CalcOnSave *bool `xml:"calcOnSave,attr"` + ConcurrentCalc *bool `xml:"concurrentCalc,attr"` + ConcurrentManualCount *uint `xml:"concurrentManualCount,attr"` + ForceFullCalc *bool `xml:"forceFullCalc,attr"` +} + // WorkbookPropsOptions directly maps the settings of workbook proprieties. type WorkbookPropsOptions struct { Date1904 *bool