checkvist-api/filter.go
Oliver Jakoubek cb30b178be Add Filter builder, Archive/Unarchive, WithRepeat, and GoDoc examples
- 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
2026-01-14 14:39:27 +01:00

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
}