2020-01-09 18:33:22 +01:00
|
|
|
// Vikunja is a todo-list application to facilitate your life.
|
|
|
|
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
2019-12-07 15:30:51 +01:00
|
|
|
//
|
2020-01-09 18:33:22 +01:00
|
|
|
// 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.
|
2019-12-07 15:30:51 +01:00
|
|
|
//
|
2020-01-09 18:33:22 +01:00
|
|
|
// 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.
|
2019-12-07 15:30:51 +01:00
|
|
|
//
|
2020-01-09 18:33:22 +01:00
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2019-12-07 15:30:51 +01:00
|
|
|
|
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
|
|
|
"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()
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2019-12-07 16:56:18 +01:00
|
|
|
} else if boolLHS && !boolRHS {
|
|
|
|
return 1
|
2019-12-07 15:30:51 +01:00
|
|
|
}
|
|
|
|
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("DoneAtUnix"),
|
|
|
|
taskPropertyDueDateUnix: mustMakeComparator("DueDateUnix"),
|
|
|
|
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
|
|
|
|
taskPropertyListID: mustMakeComparator("ListID"),
|
|
|
|
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
|
|
|
|
taskPropertyPriority: mustMakeComparator("Priority"),
|
|
|
|
taskPropertyStartDateUnix: mustMakeComparator("StartDateUnix"),
|
|
|
|
taskPropertyEndDateUnix: mustMakeComparator("EndDateUnix"),
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|