Skip to content

Commit

Permalink
spreadsheet: support sorting a sheet
Browse files Browse the repository at this point in the history
The file format doesn't support sorting, so we
need to sort the sheet rows and renumber after sorting.
  • Loading branch information
tbaliance committed Sep 27, 2017
1 parent 73e0a18 commit c5be453
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 2 deletions.
3 changes: 3 additions & 0 deletions _examples/spreadsheet/sort-filter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ func main() {

}

// sort column C, starting a row 2 to skip the header row
sheet.Sort("C", 2, spreadsheet.SortOrderDescending)

if err := ss.Validate(); err != nil {
log.Fatalf("error validating sheet: %s", err)
}
Expand Down
Binary file modified _examples/spreadsheet/sort-filter/sort-filter.xlsx
Binary file not shown.
9 changes: 9 additions & 0 deletions spreadsheet/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,15 @@ func (c Cell) GetCachedFormulaResult() string {
return ""
}

func (c Cell) getRawSortValue() (string, bool) {
if c.HasFormula() {
v := c.GetCachedFormulaResult()
return v, format.IsNumber(v)
}
v, _ := c.GetRawValue()
return v, format.IsNumber(v)
}

// SetCachedFormulaResult sets the cached result of a formula. This is normally
// not needed but is used internally when expanding an array formula.
func (c Cell) SetCachedFormulaResult(s string) {
Expand Down
92 changes: 92 additions & 0 deletions spreadsheet/compare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2017 Baliance. All rights reserved.
//
// Use of this source code is governed by the terms of the Affero GNU General
// Public License version 3.0 as published by the Free Software Foundation and
// appearing in the file LICENSE included in the packaging of this file. A
// commercial license can be purchased by contacting [email protected].

package spreadsheet

import (
"strconv"
)

// SortOrder is a column sort order.
//go:generate stringer -type=SortOrder
type SortOrder byte

// SortOrder constants
const (
SortOrderAscending SortOrder = iota
SortOrderDescending
)

// Comparer is used to compare rows based off a column and cells based off of
// their value.
type Comparer struct {
Order SortOrder
}

// LessRows compares two rows based off of a column. If the column doesn't exist
// in one row, that row is 'less'.
func (c Comparer) LessRows(column string, lhs, rhs Row) bool {
var lhsCell, rhsCell Cell
for _, c := range lhs.Cells() {
cellCol, _, _ := ParseCellReference(c.Reference())
if cellCol == column {
lhsCell = c
break
}
}

for _, c := range rhs.Cells() {
cellCol, _, _ := ParseCellReference(c.Reference())
if cellCol == column {
rhsCell = c
break
}
}

return c.LessCells(lhsCell, rhsCell)
}

// LessCells returns true if the lhs value is less than the rhs value. If the
// cells contain numeric values, their value interpreted as a floating point is
// compared. Otherwise their string contents are compared.
func (c Comparer) LessCells(lhs, rhs Cell) bool {
if c.Order == SortOrderDescending {
lhs, rhs = rhs, lhs
}

// handle zero-value cells first as we can get those based off of LessRows
// above
if lhs.X() == nil {
if rhs.X() == nil {
return false
}
return true
}
if rhs.X() == nil {
return false
}

lhsValue, lhsIsNum := lhs.getRawSortValue()
rhsValue, rhsIsNum := rhs.getRawSortValue()

switch {
// both numbers
case lhsIsNum && rhsIsNum:
lf, _ := strconv.ParseFloat(lhsValue, 64)
rf, _ := strconv.ParseFloat(rhsValue, 64)
return lf < rf
// numbers sort before non-numbers
case lhsIsNum:
return true
case rhsIsNum:
return false
}

lhsValue = lhs.GetFormattedValue()
rhsValue = lhs.GetFormattedValue()
return lhsValue < rhsValue
}
14 changes: 14 additions & 0 deletions spreadsheet/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,17 @@ func (r Row) Cell(col string) Cell {
}
return r.AddNamedCell(col)
}

// renumberAs assigns a new row number and fixes any cell references within the
// row so they refer to the new row number. This is used when sorting to fix up
// moved rows.
func (r Row) renumberAs(rowNumber uint32) {
r.x.RAttr = gooxml.Uint32(rowNumber)
for _, c := range r.Cells() {
col, _, err := ParseCellReference(c.Reference())
if err == nil {
newRef := fmt.Sprintf("%s%d", col, rowNumber)
c.x.RAttr = gooxml.String(newRef)
}
}
}
37 changes: 37 additions & 0 deletions spreadsheet/sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,3 +724,40 @@ func (s *Sheet) Protection() SheetProtection {
}
return SheetProtection{s.x.SheetProtection}
}

// Sort sorts all of the rows within a sheet by the contents of a column. As the
// file format doesn't suppot indicating that a column should be sorted by the
// viewing/editing program, we actually need to reorder rows and change cell
// references during a sort. If the sheet contains formulas, you should call
// RecalculateFormulas() prior to sorting. The column is in the form "C" and
// specifies the column to sort by. The firstRow is a 1-based index and
// specifies the firstRow to include in the sort, allowing skipping over a
// header row.
func (s *Sheet) Sort(column string, firstRow uint32, order SortOrder) {
sheetData := s.x.SheetData.Row
rows := s.Rows()
// figure out which row to start the sort at
for i, r := range rows {
if r.RowNumber() == firstRow {
sheetData = s.x.SheetData.Row[i:]
break
}
}

// perform the sort
cmp := Comparer{Order: order}
sort.Slice(sheetData, func(i, j int) bool {
return cmp.LessRows(column,
Row{s.w, s.x, sheetData[i]},
Row{s.w, s.x, sheetData[j]})
})

// since we probably moved some rows, we need to go and fix up their row
// number and cell references
for i, r := range s.Rows() {
correctRowNumber := uint32(i + 1)
if r.RowNumber() != correctRowNumber {
r.renumberAs(correctRowNumber)
}
}
}
21 changes: 21 additions & 0 deletions spreadsheet/sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package spreadsheet_test

import (
"fmt"
"math"
"math/rand"
"testing"
Expand Down Expand Up @@ -261,5 +262,25 @@ func TestMergedCellValidation(t *testing.T) {
if err := sheet.Validate(); err == nil {
t.Errorf("expected validation error due to overlapping merged cells")
}
}

func TestSortNumbers(t *testing.T) {
wb := spreadsheet.New()
sheet := wb.AddSheet()
sheet.Cell("C1").SetNumber(5)
sheet.Cell("C2").SetNumber(4)
sheet.Cell("C3").SetNumber(3)
sheet.Cell("C4").SetNumber(2)
sheet.Cell("C5").SetNumber(1)

sheet.Sort("C", 1, spreadsheet.SortOrderAscending)

for i := 1; i <= 5; i++ {
ref := fmt.Sprintf("C%d", i)
exp := float64(i)
got, _ := sheet.Cell(ref).GetValueAsNumber()
if got != exp {
t.Errorf("expected %f in %s, got %f", exp, ref, got)
}
}
}
16 changes: 16 additions & 0 deletions spreadsheet/sortorder_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions spreadsheet/standardformat_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c5be453

Please sign in to comment.