vikunja-api/pkg/models/task_collection_sort.go
konrad db2d868eed Return iso dates for everything date related from the api (#130)
Remove traces of unix timestamp

Revert renaming reminder table column

Fix staticcheck

Remove unused table call

Add migration for renaming reminders table

Fix issues with using TimeStamp

Fix lint

Updated all created / updated fields to use TimeStamps

Add comments

Convert all created / updated fields to datetime

Add time util package

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/130
2020-02-08 12:48:49 +00:00

241 lines
7.3 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/timeutil"
"fmt"
"reflect"
"sort"
)
type (
sortParam struct {
sortBy sortProperty
orderBy sortOrder // asc or desc
}
sortProperty string
sortOrder string
)
const (
taskPropertyID sortProperty = "id"
taskPropertyText sortProperty = "text"
taskPropertyDescription sortProperty = "description"
taskPropertyDone sortProperty = "done"
taskPropertyDoneAtUnix sortProperty = "done_at_unix"
taskPropertyDueDateUnix sortProperty = "due_date_unix"
taskPropertyCreatedByID sortProperty = "created_by_id"
taskPropertyListID sortProperty = "list_id"
taskPropertyRepeatAfter sortProperty = "repeat_after"
taskPropertyPriority sortProperty = "priority"
taskPropertyStartDateUnix sortProperty = "start_date_unix"
taskPropertyEndDateUnix sortProperty = "end_date_unix"
taskPropertyHexColor sortProperty = "hex_color"
taskPropertyPercentDone sortProperty = "percent_done"
taskPropertyUID sortProperty = "uid"
taskPropertyCreated sortProperty = "created"
taskPropertyUpdated sortProperty = "updated"
)
func (p sortProperty) String() string {
return string(p)
}
const (
orderInvalid sortOrder = "invalid"
orderAscending sortOrder = "asc"
orderDescending sortOrder = "desc"
)
func (o sortOrder) String() string {
return string(o)
}
func getSortOrderFromString(s string) sortOrder {
if s == "asc" {
return orderAscending
}
if s == "desc" {
return orderDescending
}
return orderInvalid
}
func (sp *sortParam) validate() error {
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
}
switch sp.sortBy {
case
taskPropertyID,
taskPropertyText,
taskPropertyDescription,
taskPropertyDone,
taskPropertyDoneAtUnix,
taskPropertyDueDateUnix,
taskPropertyCreatedByID,
taskPropertyListID,
taskPropertyRepeatAfter,
taskPropertyPriority,
taskPropertyStartDateUnix,
taskPropertyEndDateUnix,
taskPropertyHexColor,
taskPropertyPercentDone,
taskPropertyUID,
taskPropertyCreated,
taskPropertyUpdated:
return nil
}
return ErrInvalidSortParam{SortBy: sp.sortBy}
}
type taskComparator func(lhs, rhs *Task) int64
func mustMakeComparator(fieldName string) taskComparator {
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(fieldName)
if !ok {
panic(fmt.Sprintf("Field '%s' has not been found on Task", fieldName))
}
extractProp := func(task *Task) interface{} {
return reflect.ValueOf(task).Elem().FieldByIndex(field.Index).Interface()
}
// Special case for handling TimeStamp types
if field.Type.Name() == "TimeStamp" {
return func(lhs, rhs *Task) int64 {
return int64(extractProp(lhs).(timeutil.TimeStamp)) - int64(extractProp(rhs).(timeutil.TimeStamp))
}
}
switch field.Type.Kind() {
case reflect.Int64:
return func(lhs, rhs *Task) int64 {
return extractProp(lhs).(int64) - extractProp(rhs).(int64)
}
case reflect.Float64:
return func(lhs, rhs *Task) int64 {
floatLHS, floatRHS := extractProp(lhs).(float64), extractProp(rhs).(float64)
if floatLHS > floatRHS {
return 1
} else if floatLHS < floatRHS {
return -1
}
return 0
}
case reflect.String:
return func(lhs, rhs *Task) int64 {
strLHS, strRHS := extractProp(lhs).(string), extractProp(rhs).(string)
if strLHS > strRHS {
return 1
} else if strLHS < strRHS {
return -1
}
return 0
}
case reflect.Bool:
return func(lhs, rhs *Task) int64 {
boolLHS, boolRHS := extractProp(lhs).(bool), extractProp(rhs).(bool)
if !boolLHS && boolRHS {
return -1
} else if boolLHS && !boolRHS {
return 1
}
return 0
}
default:
panic(fmt.Sprintf("Unsupported type for sorting: %s", field.Type.Name()))
}
}
// This is a map of properties that can be sorted by
// and their appropriate comparator function.
// The comparator function sorts in ascending mode.
var propertyComparators = map[sortProperty]taskComparator{
taskPropertyID: mustMakeComparator("ID"),
taskPropertyText: mustMakeComparator("Text"),
taskPropertyDescription: mustMakeComparator("Description"),
taskPropertyDone: mustMakeComparator("Done"),
taskPropertyDoneAtUnix: mustMakeComparator("DoneAt"),
taskPropertyDueDateUnix: mustMakeComparator("DueDate"),
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
taskPropertyListID: mustMakeComparator("ListID"),
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
taskPropertyPriority: mustMakeComparator("Priority"),
taskPropertyStartDateUnix: mustMakeComparator("StartDate"),
taskPropertyEndDateUnix: mustMakeComparator("EndDate"),
taskPropertyHexColor: mustMakeComparator("HexColor"),
taskPropertyPercentDone: mustMakeComparator("PercentDone"),
taskPropertyUID: mustMakeComparator("UID"),
taskPropertyCreated: mustMakeComparator("Created"),
taskPropertyUpdated: mustMakeComparator("Updated"),
}
// Creates a taskComparator that sorts by the first comparator and falls back to
// the second one (and so on...) if the properties were equal.
func combineComparators(comparators ...taskComparator) taskComparator {
return func(lhs, rhs *Task) int64 {
for _, compare := range comparators {
res := compare(lhs, rhs)
if res != 0 {
return res
}
}
return 0
}
}
func sortTasks(tasks []*Task, by []*sortParam) {
// Always sort at least by id asc so we have a consistent order of items every time
// If we would not do this, we would get a different order for items with the same content every time
// the slice is sorted. To circumvent this, we always order at least by ID.
if len(by) == 0 ||
(len(by) > 0 && by[len(by)-1].sortBy != taskPropertyID) { // Don't sort by ID last if the id parameter is already passed as the last parameter.
by = append(by, &sortParam{sortBy: taskPropertyID, orderBy: orderAscending})
}
comparators := make([]taskComparator, 0, len(by))
for _, param := range by {
comparator, ok := propertyComparators[param.sortBy]
if !ok {
panic("No suitable comparator for sortBy found! Param was " + param.sortBy)
}
// This is a descending sort, so we need to negate the comparator (i.e. switch the inputs).
if param.orderBy == orderDescending {
oldComparator := comparator
comparator = func(lhs, rhs *Task) int64 {
return oldComparator(lhs, rhs) * -1
}
}
comparators = append(comparators, comparator)
}
combinedComparator := combineComparators(comparators...)
sort.Slice(tasks, func(i, j int) bool {
lhs, rhs := tasks[i], tasks[j]
res := combinedComparator(lhs, rhs)
return res <= 0
})
}