spreadsheet: support adding/removing an auto filter

This commit is contained in:
Todd 2017-09-07 14:57:04 -05:00
parent d8554f54de
commit f70810321d
6 changed files with 164 additions and 0 deletions

View File

@ -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")
}

Binary file not shown.

View File

@ -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)
}

View File

@ -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))
}
}
}

View File

@ -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")
}
}

View File

@ -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 {