Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ type formulaFuncs struct {
// SLN
// SLOPE
// SMALL
// SORTBY
// SQRT
// SQRTPI
// STANDARDIZE
Expand Down
290 changes: 290 additions & 0 deletions calcrange.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.

package excelize

import (
"container/list"
"fmt"
"sort"
"strings"
)

// sortbyArgs holds the parsed arguments for the SORTBY function.
type sortbyArgs struct {
array []formulaArg
cols int
rows int
sortKeys []sortbyKey
}

// sortbyKey represents a single sort key with its associated array and order.
type sortbyKey struct {
byArray []formulaArg
cols int
rows int
ascending bool // true = ascending (1 or omitted), false = descending (-1)
}

// rowWithKeys pairs a data row with its corresponding sort key values.
type rowWithKeys struct {
rowData []formulaArg
sortKeys [][]formulaArg // up to 3 sets of sort keys
}

// SORTBY function sorts the contents of a range or array based on the values
// in a corresponding range or array. The syntax of the function is:
//
// SORTBY(array, by_array1, [sort_order1], [by_array2, sort_order2], [by_array3, sort_order3])
func (fn *formulaFuncs) SORTBY(argsList *list.List) formulaArg {
args, errArg := getSortbyArgs(argsList)
if errArg != nil {
return *errArg
}

// Build composite sort keys for each row
rowsWithKeys := make([]rowWithKeys, args.rows)
for i := 0; i < args.rows; i++ {
rowsWithKeys[i].rowData = args.array[i*args.cols : (i+1)*args.cols]
rowsWithKeys[i].sortKeys = make([][]formulaArg, len(args.sortKeys))

for keyIdx, sortKey := range args.sortKeys {
rowsWithKeys[i].sortKeys[keyIdx] = sortKey.byArray[i*sortKey.cols : (i+1)*sortKey.cols]
}
}

// Sort using multi-key comparison
sort.Slice(rowsWithKeys, func(i, j int) bool {
return compareRowsForSortby(rowsWithKeys[i], rowsWithKeys[j], args.sortKeys)
})

// Build result matrix
result := make([][]formulaArg, args.rows)
for i, row := range rowsWithKeys {
result[i] = row.rowData
}

return newMatrixFormulaArg(result)
}

// getSortbyArgs parses and validates the arguments for the SORTBY function.
// Syntax: SORTBY(array, by_array1, [sort_order1], [by_array2, sort_order2], [by_array3, sort_order3])
func getSortbyArgs(argsList *list.List) (sortbyArgs, *formulaArg) {
res := sortbyArgs{}
argsLen := argsList.Len()

// Validate argument count: 2, 3, 5, or 7 arguments
if argsLen < 2 {
errArg := newErrorFormulaArg(formulaErrorVALUE, "SORTBY requires at least 2 arguments")
return res, &errArg
}
if argsLen > 7 {
msg := fmt.Sprintf("SORTBY takes at most 7 arguments, received %d", argsLen)
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
return res, &errArg
}
// Validate argument pattern: must be 2, 3, 5, or 7
if argsLen != 2 && argsLen != 3 && argsLen != 5 && argsLen != 7 {
msg := fmt.Sprintf("SORTBY requires 2, 3, 5, or 7 arguments, received %d", argsLen)
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
return res, &errArg
}

// Parse first argument (array to sort)
firstArg := argsList.Front()
res.array = firstArg.Value.(formulaArg).ToList()
if len(res.array) == 0 {
errArg := newErrorFormulaArg(formulaErrorVALUE, "missing first argument to SORTBY")
return res, &errArg
}
if res.array[0].Type == ArgError {
return res, &res.array[0]
}

// Calculate dimensions of main array
tempList := list.New()
tempList.PushBack(firstArg.Value)
rmin, rmax := calcColsRowsMinMax(false, tempList)
cmin, cmax := calcColsRowsMinMax(true, tempList)
res.cols, res.rows = cmax-cmin+1, rmax-rmin+1

// Parse sort keys (up to 3)
currentArg := firstArg.Next()
keyCount := 0

for currentArg != nil && keyCount < 3 {
var key sortbyKey

// Parse by_array
key.byArray = currentArg.Value.(formulaArg).ToList()
if len(key.byArray) == 0 {
errArg := newErrorFormulaArg(formulaErrorVALUE, "missing by_array argument to SORTBY")
return res, &errArg
}
if key.byArray[0].Type == ArgError {
return res, &key.byArray[0]
}

// Calculate dimensions of by_array
tempList := list.New()
tempList.PushBack(currentArg.Value)
rmin, rmax := calcColsRowsMinMax(false, tempList)
cmin, cmax := calcColsRowsMinMax(true, tempList)
key.cols, key.rows = cmax-cmin+1, rmax-rmin+1

// Validate dimensions match
if key.rows != res.rows {
msg := fmt.Sprintf("by_array dimensions (%d rows) do not match array dimensions (%d rows)",
key.rows, res.rows)
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
return res, &errArg
}

// Parse optional sort_order (default to ascending)
key.ascending = true
currentArg = currentArg.Next()

// Check if next argument exists and could be sort_order
if currentArg != nil {
// Try to parse as number
sortOrderArg := currentArg.Value.(formulaArg).ToNumber()

// Determine if this should be a sort_order based on argument count and position
expectedSortOrder := false
if keyCount == 0 && (argsLen == 3 || argsLen == 5 || argsLen == 7) {
expectedSortOrder = true
} else if keyCount == 1 && (argsLen == 5 || argsLen == 7) {
expectedSortOrder = true
} else if keyCount == 2 && argsLen == 7 {
expectedSortOrder = true
}

if sortOrderArg.Type == ArgError {
// Not a valid number
if expectedSortOrder {
// We expected a sort_order but got an error
return res, &sortOrderArg
}
// Otherwise, this might be the next by_array, don't consume it
} else {
// Valid number - check if it's 1 or -1
if sortOrderArg.Number == -1 {
key.ascending = false
currentArg = currentArg.Next()
} else if sortOrderArg.Number == 1 {
key.ascending = true
currentArg = currentArg.Next()
} else {
msg := fmt.Sprintf("sort_order must be 1 or -1, received %v", sortOrderArg.Number)
errArg := newErrorFormulaArg(formulaErrorVALUE, msg)
return res, &errArg
}
}
}

res.sortKeys = append(res.sortKeys, key)
keyCount++
}

return res, nil
}

// compareRowsForSortby compares two rows using multiple sort keys.
// Returns true if row i should come before row j.
func compareRowsForSortby(i, j rowWithKeys, sortKeys []sortbyKey) bool {
// Compare using each sort key in order
for keyIdx, sortKey := range sortKeys {
iKeys := i.sortKeys[keyIdx]
jKeys := j.sortKeys[keyIdx]

// Compare row keys column by column
for colIdx := 0; colIdx < len(iKeys) && colIdx < len(jKeys); colIdx++ {
comparison := compareSortbyValues(iKeys[colIdx], jKeys[colIdx])

if comparison == 0 {
// Equal, continue to next column
continue
}

// Apply sort order
if sortKey.ascending {
return comparison < 0
}
return comparison > 0
}

// All columns in this key are equal, check next key
}

// All keys are equal, maintain original order (stable sort)
return false
}

// compareSortbyValues compares two formulaArg values according to Excel sort rules:
// Numbers < Strings < Errors < Blanks (for ascending order)
// Returns: -1 if a < b, 0 if a == b, 1 if a > b
func compareSortbyValues(a, b formulaArg) int {
// Define type priority for sorting
getTypePriority := func(arg formulaArg) int {
switch arg.Type {
case ArgNumber:
return 1
case ArgString:
return 2
case ArgError:
return 3
case ArgEmpty:
return 4
default:
return 5
}
}

aPriority := getTypePriority(a)
bPriority := getTypePriority(b)

// Different types: compare by type priority
if aPriority != bPriority {
if aPriority < bPriority {
return -1
}
return 1
}

// Same type: compare values
switch a.Type {
case ArgNumber:
if a.Number < b.Number {
return -1
}
if a.Number > b.Number {
return 1
}
return 0

case ArgString:
// Case-insensitive comparison
aStr := strings.ToLower(a.Value())
bStr := strings.ToLower(b.Value())
return strings.Compare(aStr, bStr)

case ArgError:
// Errors are compared by their string representation
return strings.Compare(a.Error, b.Error)

case ArgEmpty:
// All empty values are equal
return 0

default:
return 0
}
}
Loading