package filter
import (
"html"
"strings"
"github.com/tayloree/publix-deals/internal/api"
)
// Options holds all filter criteria.
type Options struct {
BOGO bool
Category string
Department string
Query string
Limit int
}
// Apply filters a slice of SavingItems according to the given options.
func Apply(items []api.SavingItem, opts Options) []api.SavingItem {
wantCategory := opts.Category != ""
wantDepartment := opts.Department != ""
wantQuery := opts.Query != ""
needsFiltering := opts.BOGO || wantCategory || wantDepartment || wantQuery
if !needsFiltering {
if opts.Limit > 0 && opts.Limit < len(items) {
return items[:opts.Limit]
}
return items
}
var result []api.SavingItem
if opts.Limit > 0 && opts.Limit < len(items) {
result = make([]api.SavingItem, 0, opts.Limit)
} else {
result = make([]api.SavingItem, 0, len(items))
}
category := opts.Category
department := strings.ToLower(opts.Department)
query := strings.ToLower(opts.Query)
for _, item := range items {
if opts.BOGO || wantCategory {
hasBogo := !opts.BOGO
hasCategory := !wantCategory
for _, c := range item.Categories {
if !hasBogo && strings.EqualFold(c, "bogo") {
hasBogo = true
}
if !hasCategory && strings.EqualFold(c, category) {
hasCategory = true
}
if hasBogo && hasCategory {
break
}
}
if !hasBogo || !hasCategory {
continue
}
}
if wantDepartment && !strings.Contains(strings.ToLower(Deref(item.Department)), department) {
continue
}
if wantQuery {
title := strings.ToLower(CleanText(Deref(item.Title)))
desc := strings.ToLower(CleanText(Deref(item.Description)))
if !strings.Contains(title, query) && !strings.Contains(desc, query) {
continue
}
}
result = append(result, item)
if opts.Limit > 0 && len(result) >= opts.Limit {
break
}
}
if len(result) == 0 {
return nil
}
return result
}
// Categories returns a map of category name to count across all items.
func Categories(items []api.SavingItem) map[string]int {
cats := make(map[string]int)
for _, item := range items {
for _, c := range item.Categories {
cats[c]++
}
}
return cats
}
// Deref safely dereferences a string pointer, returning "" for nil.
func Deref(s *string) string {
if s == nil {
return ""
}
return *s
}
// CleanText unescapes HTML entities and normalizes whitespace.
func CleanText(s string) string {
if !strings.ContainsAny(s, "&\r\n") {
return strings.TrimSpace(s)
}
s = html.UnescapeString(s)
if !strings.ContainsAny(s, "\r\n") {
return strings.TrimSpace(s)
}
s = strings.ReplaceAll(s, "\r\n", " ")
s = strings.ReplaceAll(s, "\r", " ")
s = strings.ReplaceAll(s, "\n", " ")
return strings.TrimSpace(s)
}
// ContainsIgnoreCase reports whether any element in slice matches val case-insensitively.
func ContainsIgnoreCase(slice []string, val string) bool {
for _, s := range slice {
if strings.EqualFold(s, val) {
return true
}
}
return false
}