package main // Ordering, filtering and the weekly measure the verbs entries, groups and status serve (novox/hq ADR 0459 // §2, §10; issue 1521): pure functions, no store — tools.go's entriesTool, groupsTool and statusTool call // them over what Store.Entries, Store.Groups and Store.Lessons already answered. import ( "sort" "time" ) // SortEntries orders mail entries as entries answers them (1521/AC1): those marked matters first, nearest // due date first among them (none last, within that group); then the rest, newest first. func SortEntries(entries []Entry) []Entry { sorted := append([]Entry(nil), entries...) sort.SliceStable(sorted, func(i, j int) bool { a, b := sorted[i], sorted[j] aMatters, bMatters := a.Mark == MarkMatters, b.Mark == MarkMatters if aMatters != bMatters { return aMatters } if aMatters { aNone, bNone := a.DueDate.IsZero(), b.DueDate.IsZero() if aNone != bNone { return bNone // a has a due date, b does not: a first (none last) } if !aNone { return a.DueDate.Before(b.DueDate) // nearest due date first } } return a.Date.After(b.Date) // newest first, otherwise }) return sorted } // FilterEntries keeps only entries of the given mail group (when group is not 0) and the given mark (when // mark is not "") (1521/AC1, AC4: "filtered by mail group or by mark when asked"). func FilterEntries(entries []Entry, group int64, mark Mark) []Entry { var out []Entry for _, e := range entries { if group != 0 && e.Group != group { continue } if mark != "" && e.Mark != mark { continue } out = append(out, e) } return out } // SortGroups orders mail groups largest first (1521/AC2), by label ascending to break a tie. func SortGroups(groups []Group) []Group { sorted := append([]Group(nil), groups...) sort.SliceStable(sorted, func(i, j int) bool { if sorted[i].Count != sorted[j].Count { return sorted[i].Count > sorted[j].Count } return sorted[i].Label < sorted[j].Label }) return sorted } // WeeksCovered is how many trailing weeks the status verb's own measure covers (ADR 0459 §10: "for each of // the last 8 weeks"). const WeeksCovered = 8 // WeekMeasure is one week's reading-lessons-per-100-read-mails measure (ADR 0459 §10, 1521/AC3). type WeekMeasure struct { WeekStart time.Time MailsRead int Lessons int // Rate is lessons given that week, per 100 mails read that week; nil when MailsRead is 0 ("a week with no // mail read says so" — this nil is that saying). Rate *float64 } // weekStart is the Monday, UTC midnight, of the week t falls in. func weekStart(t time.Time) time.Time { t = t.UTC() offset := (int(t.Weekday()) + 6) % 7 // Monday=0 ... Sunday=6 return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -offset) } // WeeklyMeasure computes the reading-lessons-per-100-read-mails measure for each of the last WeeksCovered // weeks, the oldest first, ending with the week now falls in (1521/AC3): lessons given that week, divided by // mails read (judged) that week, times 100; a week with no mail read carries no rate. func WeeklyMeasure(entries []Entry, lessons []Lesson, now time.Time) []WeekMeasure { start := weekStart(now).AddDate(0, 0, -7*(WeeksCovered-1)) out := make([]WeekMeasure, WeeksCovered) for i := range out { out[i].WeekStart = start.AddDate(0, 0, 7*i) } indexOf := func(t time.Time) int { if t.IsZero() || t.Before(start) { return -1 } idx := int(t.UTC().Sub(start).Hours() / (24 * 7)) if idx < 0 || idx >= WeeksCovered { return -1 } return idx } for _, e := range entries { if i := indexOf(e.Judged); i >= 0 { out[i].MailsRead++ } } for _, l := range lessons { if i := indexOf(l.Made); i >= 0 { out[i].Lessons++ } } for i := range out { if out[i].MailsRead > 0 { rate := float64(out[i].Lessons) / float64(out[i].MailsRead) * 100 out[i].Rate = &rate } } return out }