Build the mail signal's own words: headline, nearest due date first (issue 1429)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// The mail signal's own words (novox/hq ADR 0461 §3, §4; issue 1429): a pure function, no store, no bus.
|
||||
// Sending it — messenger_check, the safe form, the cap, marking mails told — is 1522.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignalMail is one mail the mail signal may name (ADR 0461 §3): just enough of its entry, never a body.
|
||||
// Date is the mail's own date, the "oldest first" tiebreak among mails with no due date (1429/AC1); it is
|
||||
// not itself said.
|
||||
type SignalMail struct {
|
||||
SenderName string // "" answers "a sender without a name" (1429/AC2), never the address
|
||||
Summary string
|
||||
Date time.Time
|
||||
DueDate time.Time // zero means none
|
||||
}
|
||||
|
||||
// SignalWords is what one mail signal says (ADR 0461 §1): a headline, and at most two lines.
|
||||
type SignalWords struct {
|
||||
Headline string
|
||||
Lines []string
|
||||
}
|
||||
|
||||
// orderSignalMails is 1429/AC1, AC4: nearest due date first (one without a due date after all those with
|
||||
// one), then oldest first among mails sharing that place — "mails carried from earlier (untold)" are simply
|
||||
// part of the same list handed in, ordered exactly as any other mail that matters is.
|
||||
func orderSignalMails(mails []SignalMail) []SignalMail {
|
||||
sorted := append([]SignalMail(nil), mails...)
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
a, b := sorted[i], sorted[j]
|
||||
aNone, bNone := a.DueDate.IsZero(), b.DueDate.IsZero()
|
||||
if aNone != bNone {
|
||||
return bNone // a has a due date, b does not: a first
|
||||
}
|
||||
if !aNone {
|
||||
return a.DueDate.Before(b.DueDate)
|
||||
}
|
||||
return a.Date.Before(b.Date) // oldest first, among mails with no due date
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
// headline is "1 mail matters" or "N mails matter" (1429/AC1, AC3: the same headline whether the words are
|
||||
// said in full or the safe form).
|
||||
func headline(n int) string {
|
||||
if n == 1 {
|
||||
return "1 mail matters"
|
||||
}
|
||||
return fmt.Sprintf("%d mails matter", n)
|
||||
}
|
||||
|
||||
// mailLine is one mail's own line (1429/AC1): "<sender's name> — <summary> — due <date>", the due part left
|
||||
// out entirely when there is none; a sender with no display name is "a sender without a name" (AC2), never
|
||||
// its address.
|
||||
func mailLine(m SignalMail) string {
|
||||
sender := m.SenderName
|
||||
if sender == "" {
|
||||
sender = "a sender without a name"
|
||||
}
|
||||
line := sender + " — " + m.Summary
|
||||
if !m.DueDate.IsZero() {
|
||||
line += " — due " + m.DueDate.Format("2006-01-02")
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// BuildSignal is the mail signal's words in full (1429/AC1, AC2, AC4): the headline, the nearest-due mail's
|
||||
// own line, and — with more than one — the next one's, ending ", and N-2 more on the mail page" once there
|
||||
// are more than two in all.
|
||||
func BuildSignal(mails []SignalMail) SignalWords {
|
||||
ordered := orderSignalMails(mails)
|
||||
n := len(ordered)
|
||||
w := SignalWords{Headline: headline(n)}
|
||||
if n == 0 {
|
||||
return w
|
||||
}
|
||||
w.Lines = append(w.Lines, mailLine(ordered[0]))
|
||||
if n >= 2 {
|
||||
second := mailLine(ordered[1])
|
||||
if n > 2 {
|
||||
second += fmt.Sprintf(", and %d more on the mail page", n-2)
|
||||
}
|
||||
w.Lines = append(w.Lines, second)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// SafeSignal is 1429/AC3: the safe form a refused set of words falls back to — the same headline, one line
|
||||
// naming nothing of any mail.
|
||||
func SafeSignal(n int) SignalWords {
|
||||
return SignalWords{Headline: headline(n), Lines: []string{"open the mail page"}}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func d(day int) time.Time { return time.Date(2026, 11, day, 0, 0, 0, 0, time.UTC) }
|
||||
|
||||
// 1429/AC1: one mail gives headline "1 mail matters" and one line; the first line names the mail with the
|
||||
// nearest due date as "<sender> — <summary> — due <date>", no due part without one.
|
||||
func TestBuildSignalOneMail(t *testing.T) {
|
||||
w := BuildSignal([]SignalMail{{SenderName: "Alice", Summary: "pay the invoice", DueDate: d(1)}})
|
||||
if w.Headline != "1 mail matters" {
|
||||
t.Fatalf("headline = %q", w.Headline)
|
||||
}
|
||||
if len(w.Lines) != 1 || w.Lines[0] != "Alice — pay the invoice — due 2026-11-01" {
|
||||
t.Fatalf("lines = %v", w.Lines)
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC1: a mail with no due date carries no due part.
|
||||
func TestBuildSignalNoDueDate(t *testing.T) {
|
||||
w := BuildSignal([]SignalMail{{SenderName: "Alice", Summary: "fyi"}})
|
||||
if w.Lines[0] != "Alice — fyi" {
|
||||
t.Fatalf("lines[0] = %q, want no due part", w.Lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC1: N mails give "N mails matter"; the first line names the nearest due date, the second the next
|
||||
// one, ending with the "and N-2 more" tail once there are more than two.
|
||||
func TestBuildSignalSeveralMails(t *testing.T) {
|
||||
mails := []SignalMail{
|
||||
{SenderName: "Carol", Summary: "c", Date: d(1)}, // no due date, oldest of the no-due-date ones
|
||||
{SenderName: "Bob", Summary: "b", DueDate: d(10)},
|
||||
{SenderName: "Alice", Summary: "a", DueDate: d(5)}, // nearest due date: first
|
||||
{SenderName: "Dave", Summary: "d", Date: d(2)},
|
||||
}
|
||||
w := BuildSignal(mails)
|
||||
if w.Headline != "4 mails matter" {
|
||||
t.Fatalf("headline = %q", w.Headline)
|
||||
}
|
||||
if len(w.Lines) != 2 {
|
||||
t.Fatalf("lines = %v, want 2", w.Lines)
|
||||
}
|
||||
if !strings.HasPrefix(w.Lines[0], "Alice —") {
|
||||
t.Fatalf("lines[0] = %q, want Alice first (nearest due date)", w.Lines[0])
|
||||
}
|
||||
if !strings.HasPrefix(w.Lines[1], "Bob —") || !strings.HasSuffix(w.Lines[1], ", and 2 more on the mail page") {
|
||||
t.Fatalf("lines[1] = %q, want Bob, and the N-2 tail", w.Lines[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC1: with exactly two mails, the second line carries no "more" tail.
|
||||
func TestBuildSignalTwoMailsNoTail(t *testing.T) {
|
||||
w := BuildSignal([]SignalMail{
|
||||
{SenderName: "Alice", Summary: "a", DueDate: d(1)},
|
||||
{SenderName: "Bob", Summary: "b", DueDate: d(2)},
|
||||
})
|
||||
if strings.Contains(w.Lines[1], "more") {
|
||||
t.Fatalf("lines[1] = %q, want no 'more' tail with only two mails", w.Lines[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC2: a sender without a display name is "a sender without a name", never its address.
|
||||
func TestBuildSignalSenderWithNoName(t *testing.T) {
|
||||
w := BuildSignal([]SignalMail{{SenderName: "", Summary: "x"}})
|
||||
if !strings.HasPrefix(w.Lines[0], "a sender without a name —") {
|
||||
t.Fatalf("lines[0] = %q, want it to start with 'a sender without a name'", w.Lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC3: the safe form keeps the same headline and says only "open the mail page".
|
||||
func TestSafeSignal(t *testing.T) {
|
||||
w := SafeSignal(3)
|
||||
if w.Headline != "3 mails matter" {
|
||||
t.Fatalf("headline = %q", w.Headline)
|
||||
}
|
||||
if len(w.Lines) != 1 || w.Lines[0] != "open the mail page" {
|
||||
t.Fatalf("lines = %v", w.Lines)
|
||||
}
|
||||
one := SafeSignal(1)
|
||||
if one.Headline != "1 mail matters" {
|
||||
t.Fatalf("one mail's headline = %q", one.Headline)
|
||||
}
|
||||
}
|
||||
|
||||
// 1429/AC4: mails carried over from earlier (untold) are counted in N and ordered with the new ones — this
|
||||
// function takes no notice of which are new and which are carried, only their own due date and date.
|
||||
func TestBuildSignalCountsCarriedMailsWithTheNewOnes(t *testing.T) {
|
||||
carried := SignalMail{SenderName: "Old", Summary: "carried", DueDate: d(1)} // told=false from an earlier batch
|
||||
newMail := SignalMail{SenderName: "New", Summary: "fresh", DueDate: d(2)}
|
||||
w := BuildSignal([]SignalMail{carried, newMail})
|
||||
if w.Headline != "2 mails matter" {
|
||||
t.Fatalf("headline = %q, want the carried mail counted too", w.Headline)
|
||||
}
|
||||
if !strings.HasPrefix(w.Lines[0], "Old —") {
|
||||
t.Fatalf("lines[0] = %q, want the carried mail (nearer due date) first", w.Lines[0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user