85456d3833
Stalls compared the due instant to now while Rank compares whole calendar days, so a task due at 09:00 was counted overdue from 09:01 while its own row on the same page still read the reason as today. Stalls now reads dayDelta. A row with no capture time also counted as sitting, because the zero time is January of year 1 and every span from it clears ten days. Rank already guarded that and Stalls did not. Folded the open-versus-candidate partition FormatRU and Spoken each carried into one split helper. The two have to agree on where that line falls.
106 lines
4.1 KiB
Go
106 lines
4.1 KiB
Go
package tasks
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/say"
|
|
)
|
|
|
|
// Counted stall shapes (Vikunja #512, step 5 of docs/plans/15-board-surface.md).
|
|
//
|
|
// She may count. She may not assess. Every shape here is arithmetic over rows he
|
|
// can see — how many, how long, how many undated — and none of it says whether a
|
|
// task matters, whether a blocker is real, or whether something should be
|
|
// dropped. That is the line internal/memory/behavior.go already drew for habits
|
|
// and the reason is the same: a 1.7B asked to judge will agree fluently and
|
|
// launder a guess into a decision.
|
|
//
|
|
// Not a nag either. Nothing here is read by the tick loop; the counts go on
|
|
// /tasks and into the answer when he asks for the list. tickLoop.dayPlan
|
|
// deliberately does not read tasks — keep it that way.
|
|
|
|
// StallDays — how long a live task has to have sat before it is counted as
|
|
// sitting. Ten days rather than a week, because a task captured on a Friday and
|
|
// still open the next Friday is an ordinary week, not a stall.
|
|
//
|
|
// Measured from Created, which is the only clock a live row carries: the store
|
|
// stamps resolved_ts and nothing else, so "no state change in eleven days" is
|
|
// exactly "captured eleven days ago and still live". That is a narrower claim
|
|
// than the plan's wording and it is the one the data supports.
|
|
const StallDays = 10
|
|
|
|
// Stall — one counted shape: how many tasks, and the sentence that says what
|
|
// they have in common. N is always ≥ 1; a shape with no tasks in it is not
|
|
// returned, because "нет просроченных" is a reassurance nobody asked for.
|
|
type Stall struct {
|
|
N int
|
|
Line string
|
|
}
|
|
|
|
// Stalls counts the shapes present in a set of live tasks, in a fixed order:
|
|
// overdue first, then sitting, then unconfirmed. Fixed because the order is what
|
|
// he reads first, and sorting by count would move the sections around every time
|
|
// one number changed.
|
|
//
|
|
// Resolved tasks are not passed in and would not be counted if they were: this
|
|
// is a statement about outstanding work.
|
|
func Stalls(items []Item, now time.Time) []Stall {
|
|
var overdue, sitting, unconfirmed int
|
|
for _, it := range items {
|
|
if it.Status == StatusCandidate {
|
|
unconfirmed++
|
|
// A candidate is Maven's reading of something she read. Counting it as
|
|
// overdue would put her own guess about a deadline in a number he is
|
|
// meant to act on.
|
|
continue
|
|
}
|
|
// Whole calendar days, the reading Rank already uses. An instant
|
|
// comparison calls a task due at 18:00 overdue from 18:01, so the count
|
|
// above the table would say "просрочено" beside a row whose own reason
|
|
// still said "сегодня".
|
|
if it.Due != nil && dayDelta(*it.Due, now) < 0 {
|
|
overdue++
|
|
}
|
|
// A row with no capture time has not sat for anything. Without the
|
|
// guard its zero time is January of year 1 and it always counts.
|
|
if !it.Created.IsZero() && now.Sub(it.Created) >= StallDays*24*time.Hour {
|
|
sitting++
|
|
}
|
|
}
|
|
var out []Stall
|
|
add := func(n int, key string, args map[string]string) {
|
|
if n == 0 {
|
|
return
|
|
}
|
|
out = append(out, Stall{N: n, Line: say.S(key, args)})
|
|
}
|
|
add(overdue, say.StallOverdue, map[string]string{
|
|
"n": fmt.Sprint(overdue), "word": say.CountWord(overdue, "задача", "задачи", "задач"),
|
|
})
|
|
add(sitting, say.StallSitting, map[string]string{
|
|
"n": fmt.Sprint(sitting), "word": say.CountWord(sitting, "задача", "задачи", "задач"),
|
|
"days": fmt.Sprint(StallDays), "dayword": say.Days(StallDays),
|
|
})
|
|
add(unconfirmed, say.StallUnconfirmed, map[string]string{
|
|
"n": fmt.Sprint(unconfirmed), "word": say.CountWord(unconfirmed, "задача", "задачи", "задач"),
|
|
})
|
|
return out
|
|
}
|
|
|
|
// StallsRU joins the shapes into one sentence for the spoken list, or "" when
|
|
// there are none. Empty on purpose: the list read is the answer to his question,
|
|
// and appending "ничего не залежалось" to it every time is a nag with a friendly
|
|
// face.
|
|
func StallsRU(stalls []Stall) string {
|
|
if len(stalls) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(stalls))
|
|
for _, s := range stalls {
|
|
parts = append(parts, s.Line)
|
|
}
|
|
return strings.Join(parts, "; ") + "."
|
|
}
|