- Implement client-side Filter builder with tag, status, due date, and search filters - Add unit tests for Filter with performance benchmark - Add Archive/Unarchive methods to ChecklistService - Add WithRepeat method to TaskBuilder for recurring tasks - Create GoDoc examples for all major functionality
152 lines
3.6 KiB
Go
152 lines
3.6 KiB
Go
package checkvist
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// filter.go contains the Filter builder for client-side task filtering.
|
|
// The Checkvist API does not support server-side filtering, so filtering
|
|
// is performed locally after fetching all tasks.
|
|
|
|
// Filter provides a builder pattern for filtering tasks client-side.
|
|
type Filter struct {
|
|
tasks []Task
|
|
filters []func(Task) bool
|
|
}
|
|
|
|
// NewFilter creates a new Filter with the given tasks.
|
|
func NewFilter(tasks []Task) *Filter {
|
|
return &Filter{tasks: tasks}
|
|
}
|
|
|
|
// WithTag filters tasks that have the specified tag.
|
|
func (f *Filter) WithTag(tag string) *Filter {
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
return taskHasTag(t, tag)
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithTags filters tasks that have all of the specified tags (AND logic).
|
|
func (f *Filter) WithTags(tags ...string) *Filter {
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
for _, tag := range tags {
|
|
if !taskHasTag(t, tag) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithStatus filters tasks by their status.
|
|
func (f *Filter) WithStatus(status TaskStatus) *Filter {
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
return t.Status == status
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithDueBefore filters tasks with due dates before the specified time.
|
|
func (f *Filter) WithDueBefore(deadline time.Time) *Filter {
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
if t.DueDate == nil {
|
|
return false
|
|
}
|
|
return t.DueDate.Before(deadline)
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithDueAfter filters tasks with due dates after the specified time.
|
|
func (f *Filter) WithDueAfter(after time.Time) *Filter {
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
if t.DueDate == nil {
|
|
return false
|
|
}
|
|
return t.DueDate.After(after)
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithDueOn filters tasks with due dates on the specified day.
|
|
func (f *Filter) WithDueOn(day time.Time) *Filter {
|
|
year, month, d := day.Date()
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
if t.DueDate == nil {
|
|
return false
|
|
}
|
|
ty, tm, td := t.DueDate.Date()
|
|
return ty == year && tm == month && td == d
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithOverdue filters tasks that are overdue (due date is before today).
|
|
func (f *Filter) WithOverdue() *Filter {
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
if t.DueDate == nil {
|
|
return false
|
|
}
|
|
return t.DueDate.Before(today) && t.Status == StatusOpen
|
|
})
|
|
return f
|
|
}
|
|
|
|
// WithSearch filters tasks whose content contains the search query (case-insensitive).
|
|
func (f *Filter) WithSearch(query string) *Filter {
|
|
lowerQuery := strings.ToLower(query)
|
|
f.filters = append(f.filters, func(t Task) bool {
|
|
return strings.Contains(strings.ToLower(t.Content), lowerQuery)
|
|
})
|
|
return f
|
|
}
|
|
|
|
// Apply applies all filters and returns the filtered tasks.
|
|
func (f *Filter) Apply() []Task {
|
|
if len(f.filters) == 0 {
|
|
result := make([]Task, len(f.tasks))
|
|
copy(result, f.tasks)
|
|
return result
|
|
}
|
|
|
|
result := make([]Task, 0, len(f.tasks))
|
|
for _, task := range f.tasks {
|
|
if f.matches(task) {
|
|
result = append(result, task)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// matches checks if a task matches all filters.
|
|
func (f *Filter) matches(task Task) bool {
|
|
for _, filter := range f.filters {
|
|
if !filter(task) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// taskHasTag checks if a task has a specific tag.
|
|
func taskHasTag(t Task, tag string) bool {
|
|
// Check parsed Tags map first
|
|
if t.Tags != nil && t.Tags[tag] {
|
|
return true
|
|
}
|
|
// Fall back to checking TagsAsText
|
|
if t.TagsAsText == "" {
|
|
return false
|
|
}
|
|
lowerTag := strings.ToLower(tag)
|
|
for _, part := range strings.Split(t.TagsAsText, ",") {
|
|
if strings.ToLower(strings.TrimSpace(part)) == lowerTag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|