From f70810321d13946bbca0d19fce027386e2a24f85 Mon Sep 17 00:00:00 2001 From: Todd Date: Thu, 7 Sep 2017 14:57:04 -0500 Subject: [PATCH] spreadsheet: support adding/removing an auto filter --- _examples/spreadsheet/sort-filter/main.go | 35 +++++++++++ .../spreadsheet/sort-filter/sort-filter.xlsx | Bin 0 -> 4310 bytes spreadsheet/definedname.go | 16 +++++ spreadsheet/sheet.go | 58 ++++++++++++++++++ spreadsheet/sheet_test.go | 39 ++++++++++++ spreadsheet/workbook.go | 16 +++++ 6 files changed, 164 insertions(+) create mode 100644 _examples/spreadsheet/sort-filter/main.go create mode 100644 _examples/spreadsheet/sort-filter/sort-filter.xlsx diff --git a/_examples/spreadsheet/sort-filter/main.go b/_examples/spreadsheet/sort-filter/main.go new file mode 100644 index 00000000..7c08cb98 --- /dev/null +++ b/_examples/spreadsheet/sort-filter/main.go @@ -0,0 +1,35 @@ +// Copyright 2017 Baliance. All rights reserved. +package main + +import ( + "fmt" + "log" + + "baliance.com/gooxml/spreadsheet" +) + +func main() { + ss := spreadsheet.New() + // add a single sheet + sheet := ss.AddSheet() + hdrRow := sheet.AddRow() + hdrRow.AddCell().SetString("Product Name") + hdrRow.AddCell().SetString("Quantity") + hdrRow.AddCell().SetString("Price") + sheet.SetAutoFilter("A1:C6") + + // rows + for r := 0; r < 5; r++ { + row := sheet.AddRow() + row.AddCell().SetString(fmt.Sprintf("Product %d", r+1)) + row.AddCell().SetNumber(float64(r + 2)) + row.AddCell().SetNumber(float64(3*r + 1)) + + } + + if err := ss.Validate(); err != nil { + log.Fatalf("error validating sheet: %s", err) + } + + ss.SaveToFile("sort-filter.xlsx") +} diff --git a/_examples/spreadsheet/sort-filter/sort-filter.xlsx b/_examples/spreadsheet/sort-filter/sort-filter.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..851b4d4adda871dc41869ce4ccd17d9b7acdbbb7 GIT binary patch literal 4310 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 literal 0 HcmV?d00001 diff --git a/spreadsheet/definedname.go b/spreadsheet/definedname.go index ab6a423e..2523d6ab 100644 --- a/spreadsheet/definedname.go +++ b/spreadsheet/definedname.go @@ -8,6 +8,7 @@ package spreadsheet import sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml" +import "baliance.com/gooxml" // DefinedName is a named range, formula, etc. type DefinedName struct { @@ -28,3 +29,18 @@ func (d DefinedName) Name() string { func (d DefinedName) Content() string { return d.x.Content } + +// SetContent sets the defined name content. +func (d DefinedName) SetContent(s string) { + d.x.Content = s +} + +// SetHidden marks the defined name as hidden. +func (d DefinedName) SetHidden(b bool) { + d.x.HiddenAttr = gooxml.Bool(b) +} + +// SetHidden marks the defined name as hidden. +func (d DefinedName) SetLocalSheetID(id uint32) { + d.x.LocalSheetIdAttr = gooxml.Uint32(id) +} diff --git a/spreadsheet/sheet.go b/spreadsheet/sheet.go index 5b0b0b4a..d2945600 100644 --- a/spreadsheet/sheet.go +++ b/spreadsheet/sheet.go @@ -25,6 +25,11 @@ type Sheet struct { x *sml.Worksheet } +// X returns the inner wrapped XML type. +func (s Sheet) X() *sml.Worksheet { + return s.x +} + // Row will return a row with a given row number, creating a new row if // necessary. func (s Sheet) Row(rowNum uint32) Row { @@ -193,3 +198,56 @@ func (s Sheet) RangeReference(n string) string { to := fmt.Sprintf("$%s$%d", tc, tr) return fmt.Sprintf(`'%s'!%s:%s`, s.Name(), from, to) } + +const autoFilterName = "_xlnm._FilterDatabase" + +// ClearAutoFilter removes the autofilters from the sheet. +func (s Sheet) ClearAutoFilter() { + s.x.AutoFilter = nil + sn := "'" + s.Name() + "'!" + // see if we have a defined auto filter name for the sheet + for _, dn := range s.w.DefinedNames() { + if dn.Name() == autoFilterName { + if strings.HasPrefix(dn.Content(), sn) { + s.w.RemoveDefinedName(dn) + break + } + } + } +} + +// SetAutoFilter creates autofilters on the sheet. These are the automatic +// filters that are common for a header row. The RangeRef should be of the form +// "A1:C5" and cover the entire range of cells to be filtered, not just the +// header. SetAutoFilter replaces any existing auto filter on the sheet. +func (s Sheet) SetAutoFilter(rangeRef string) { + // this should have no $ in it + rangeRef = strings.Replace(rangeRef, "$", "", -1) + + s.x.AutoFilter = sml.NewCT_AutoFilter() + s.x.AutoFilter.RefAttr = gooxml.String(rangeRef) + sn := "'" + s.Name() + "'!" + var sdn DefinedName + + // see if we already have a defined auto filter name for the sheet + for _, dn := range s.w.DefinedNames() { + if dn.Name() == autoFilterName { + if strings.HasPrefix(dn.Content(), sn) { + sdn = dn + // name must match, but make sure rangeRef matches as well + sdn.SetContent(s.RangeReference(rangeRef)) + break + } + } + } + // no existing name found, so add a new one + if sdn.X() == nil { + sdn = s.w.AddDefinedName(autoFilterName, s.RangeReference(rangeRef)) + } + + for i, ws := range s.w.xws { + if ws == s.x { + sdn.SetLocalSheetID(uint32(i)) + } + } +} diff --git a/spreadsheet/sheet_test.go b/spreadsheet/sheet_test.go index 4840897f..fccc90e3 100644 --- a/spreadsheet/sheet_test.go +++ b/spreadsheet/sheet_test.go @@ -82,3 +82,42 @@ func TestRowNumberValidation(t *testing.T) { t.Errorf("expected validation error with identically numbered rows") } } + +func TestAutoFilter(t *testing.T) { + wb := spreadsheet.New() + sheet := wb.AddSheet() + if len(wb.DefinedNames()) != 0 { + t.Errorf("expected no defined names for new workbook") + } + sheet.SetAutoFilter("A1:C10") + if len(wb.DefinedNames()) != 1 { + t.Errorf("expected a new defined names for the autofilter") + } + dn := wb.DefinedNames()[0] + expContent := "'Sheet 1'!$A$1:$C$10" + if dn.Content() != expContent { + t.Errorf("expected defined name content = '%s', got %s", expContent, dn.Content()) + } + + sheet.SetAutoFilter("A1:B10") + expContent = "'Sheet 1'!$A$1:$B$10" + // setting the filter again should re-write the defined name and not create a new one + if len(wb.DefinedNames()) != 1 { + t.Errorf("expected a new defined names for the autofilter") + } + dn = wb.DefinedNames()[0] + // but the content should have changed + if dn.Content() != expContent { + t.Errorf("expected defined name content = '%s', got %s", expContent, dn.Content()) + } + + sheet.ClearAutoFilter() + if len(wb.DefinedNames()) != 0 { + t.Errorf("clearing the filter should have removed the defined name") + } + + if sheet.X().AutoFilter != nil { + t.Errorf("autofilter should have been nil after clear") + } + +} diff --git a/spreadsheet/workbook.go b/spreadsheet/workbook.go index 1110f01c..5f6220cd 100644 --- a/spreadsheet/workbook.go +++ b/spreadsheet/workbook.go @@ -351,6 +351,22 @@ func (wb *Workbook) AddDefinedName(name, ref string) DefinedName { return DefinedName{dn} } +// RemoveDefinedName removes an existing defined name. +func (wb *Workbook) RemoveDefinedName(dn DefinedName) error { + if dn.X() == nil { + return errors.New("attempt to remove nil DefinedName") + } + for i, sdn := range wb.x.DefinedNames.DefinedName { + if sdn == dn.X() { + copy(wb.x.DefinedNames.DefinedName[i:], wb.x.DefinedNames.DefinedName[i+1:]) + wb.x.DefinedNames.DefinedName[len(wb.x.DefinedNames.DefinedName)-1] = nil + wb.x.DefinedNames.DefinedName = wb.x.DefinedNames.DefinedName[:len(wb.x.DefinedNames.DefinedName)-1] + return nil + } + } + return errors.New("defined name not found") +} + // DefinedNames returns a slice of all defined names in the workbook. func (wb *Workbook) DefinedNames() []DefinedName { if wb.x.DefinedNames == nil {