227 lines
6.4 KiB
Go
227 lines
6.4 KiB
Go
package search
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// DateRange represents a time range [Start, End) in a specific year.
|
|
type DateRange struct {
|
|
Start time.Time
|
|
End time.Time
|
|
Year int // the year this range belongs to (for scoring)
|
|
}
|
|
|
|
// BuildCELFilter creates a CEL filter string for a date range using created_ts.
|
|
func BuildCELFilter(dr DateRange) string {
|
|
return fmt.Sprintf("created_ts >= %d && created_ts < %d",
|
|
dr.Start.Unix(), dr.End.Unix())
|
|
}
|
|
|
|
func isLeapYear(year int) bool {
|
|
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
|
}
|
|
|
|
func daysInMonth(year int, month time.Month) int {
|
|
return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
|
}
|
|
|
|
// tier1Ranges returns date ranges for the exact same day in previous years.
|
|
// Special handling for Feb 29 / Feb 28.
|
|
func tier1Ranges(today time.Time, maxYears int, loc *time.Location) []DateRange {
|
|
month := today.Month()
|
|
day := today.Day()
|
|
currentYear := today.Year()
|
|
|
|
var ranges []DateRange
|
|
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
if month == time.February && day == 29 {
|
|
// Today is Feb 29 — only include leap years
|
|
if !isLeapYear(y) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Check that this day exists in that year
|
|
if day > daysInMonth(y, month) {
|
|
continue
|
|
}
|
|
|
|
start := time.Date(y, month, day, 0, 0, 0, 0, loc)
|
|
end := start.AddDate(0, 0, 1)
|
|
ranges = append(ranges, DateRange{Start: start, End: end, Year: y})
|
|
}
|
|
|
|
// Special: if today is Feb 28 in a non-leap year, also check Feb 29 in past leap years
|
|
if month == time.February && day == 28 && !isLeapYear(currentYear) {
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
if isLeapYear(y) {
|
|
start := time.Date(y, time.February, 29, 0, 0, 0, 0, loc)
|
|
end := start.AddDate(0, 0, 1)
|
|
ranges = append(ranges, DateRange{Start: start, End: end, Year: y})
|
|
}
|
|
}
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier2Ranges returns date ranges for the same day-of-month in previous months (last 24 months).
|
|
// Excludes the current month (covered by Tier 1).
|
|
func tier2Ranges(today time.Time, loc *time.Location) []DateRange {
|
|
day := today.Day()
|
|
currentYear := today.Year()
|
|
currentMonth := today.Month()
|
|
|
|
var ranges []DateRange
|
|
|
|
for i := 1; i <= 24; i++ {
|
|
t := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, loc).AddDate(0, -i, 0)
|
|
y := t.Year()
|
|
m := t.Month()
|
|
|
|
// Skip if this day doesn't exist in that month
|
|
if day > daysInMonth(y, m) {
|
|
continue
|
|
}
|
|
|
|
start := time.Date(y, m, day, 0, 0, 0, 0, loc)
|
|
end := start.AddDate(0, 0, 1)
|
|
ranges = append(ranges, DateRange{Start: start, End: end, Year: y})
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier3Ranges returns ±3 day ranges around the exact date in previous years,
|
|
// excluding the exact day itself (which is Tier 1).
|
|
func tier3Ranges(today time.Time, maxYears int, loc *time.Location) []DateRange {
|
|
month := today.Month()
|
|
day := today.Day()
|
|
currentYear := today.Year()
|
|
|
|
var ranges []DateRange
|
|
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
// The center date in that year
|
|
if day > daysInMonth(y, month) {
|
|
continue
|
|
}
|
|
center := time.Date(y, month, day, 0, 0, 0, 0, loc)
|
|
weekStart := center.AddDate(0, 0, -3)
|
|
weekEnd := center.AddDate(0, 0, 4) // +3 days inclusive = +4 exclusive
|
|
|
|
// We need to exclude the center day: split into [weekStart, center) and [center+1day, weekEnd)
|
|
centerEnd := center.AddDate(0, 0, 1)
|
|
|
|
ranges = append(ranges,
|
|
DateRange{Start: weekStart, End: center, Year: y},
|
|
DateRange{Start: centerEnd, End: weekEnd, Year: y},
|
|
)
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier4Ranges returns the same month in previous years,
|
|
// excluding the ±3 day window around the exact date (covered by Tier 1+3).
|
|
func tier4Ranges(today time.Time, maxYears int, loc *time.Location) []DateRange {
|
|
month := today.Month()
|
|
day := today.Day()
|
|
currentYear := today.Year()
|
|
|
|
var ranges []DateRange
|
|
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
monthStart := time.Date(y, month, 1, 0, 0, 0, 0, loc)
|
|
monthEnd := time.Date(y, month+1, 1, 0, 0, 0, 0, loc)
|
|
|
|
d := min(day, daysInMonth(y, month))
|
|
center := time.Date(y, month, d, 0, 0, 0, 0, loc)
|
|
excludeStart := center.AddDate(0, 0, -3)
|
|
excludeEnd := center.AddDate(0, 0, 4)
|
|
|
|
// Before the exclude window
|
|
if excludeStart.After(monthStart) {
|
|
ranges = append(ranges, DateRange{Start: monthStart, End: excludeStart, Year: y})
|
|
}
|
|
// After the exclude window
|
|
if excludeEnd.Before(monthEnd) {
|
|
ranges = append(ranges, DateRange{Start: excludeEnd, End: monthEnd, Year: y})
|
|
}
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier5Ranges returns the same quarter in previous years, excluding the current month.
|
|
func tier5Ranges(today time.Time, maxYears int, loc *time.Location) []DateRange {
|
|
month := today.Month()
|
|
currentYear := today.Year()
|
|
|
|
qStart := (month-1)/3*3 + 1 // first month of quarter (1, 4, 7, 10)
|
|
var quarterMonths [3]time.Month
|
|
for i := range 3 {
|
|
quarterMonths[i] = time.Month(int(qStart) + i)
|
|
}
|
|
|
|
var ranges []DateRange
|
|
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
for _, m := range quarterMonths {
|
|
if m == month {
|
|
continue // exclude current month (covered by Tier 4)
|
|
}
|
|
start := time.Date(y, m, 1, 0, 0, 0, 0, loc)
|
|
end := time.Date(y, m+1, 1, 0, 0, 0, 0, loc)
|
|
ranges = append(ranges, DateRange{Start: start, End: end, Year: y})
|
|
}
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier6Ranges returns the same half-year in previous years, excluding the current quarter.
|
|
func tier6Ranges(today time.Time, maxYears int, loc *time.Location) []DateRange {
|
|
month := today.Month()
|
|
currentYear := today.Year()
|
|
|
|
// Half-year: H1 = Jan-Jun, H2 = Jul-Dec
|
|
var halfStart time.Month
|
|
if month <= 6 {
|
|
halfStart = 1
|
|
} else {
|
|
halfStart = 7
|
|
}
|
|
|
|
// Current quarter start
|
|
qStart := (month-1)/3*3 + 1
|
|
|
|
var ranges []DateRange
|
|
|
|
for y := currentYear - maxYears; y < currentYear; y++ {
|
|
for m := halfStart; m < halfStart+6; m++ {
|
|
// Skip months in the current quarter
|
|
if m >= qStart && m < qStart+3 {
|
|
continue
|
|
}
|
|
start := time.Date(y, m, 1, 0, 0, 0, 0, loc)
|
|
end := time.Date(y, m+1, 1, 0, 0, 0, 0, loc)
|
|
ranges = append(ranges, DateRange{Start: start, End: end, Year: y})
|
|
}
|
|
}
|
|
|
|
return ranges
|
|
}
|
|
|
|
// tier7Ranges returns 2 to 6 months ago (recent past).
|
|
func tier7Ranges(today time.Time, loc *time.Location) []DateRange {
|
|
end := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, loc).AddDate(0, -2, 0)
|
|
start := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, loc).AddDate(0, -6, 0)
|
|
|
|
return []DateRange{
|
|
{Start: start, End: end, Year: 0}, // Year=0 means "recent"
|
|
}
|
|
}
|