Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/entries.go
T

122 lines
3.8 KiB
Go

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
}