Files
compass_teams_nhs/internal/period/period.go
Sam Rolfe 047af0f31d Phase 6b/7b: per-child decoded periods, separate brief blocks, idempotent calendar (AUD-119)
- Parse each child's own "Today" day widget for the timetable (FR-16); drop the
  school-wide week calendar's lesson grid and clean the leaked routine rows.
- Add the editable codes map (FR-17): subjects, rooms, teachers and period
  labels, seeded, an unknown code falls back to the raw code; `school codes` CLI.
- Brief: Finn's block first, then Harry's, four sections always present (FR-9);
  periods decoded into plain English; the undated tail is one counted line (FR-18).
- Calendar: write periods + work due + work overdue + major events, per child,
  on every pass. Identity is the stable per-item key, so a re-run never re-inserts
  (FR-8). morning-brief.sh now runs scan -> calendar push -> notify.
- Day-widget labels are 12-hour: 1-6 read as PM, fixing afternoon periods.

Proven live on .13: image 0.6.2-phase6b7b; both children's briefs decoded and
separate; 35 events in calendar.school; runs 2 and 3 of the scan added nothing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-16 10:25:55 +11:00

55 lines
1.7 KiB
Go

// Package period understands the Compass lesson label:
//
// [period] - [subject code] - [room code] - [teacher initials]
//
// for example "1 - 7SCF - H14LAB - KNI". It is a leaf package so the parser,
// the brief and the calendar all agree on what a routine class period is
// (AGENTS.md §4 FR-16/FR-17, §7).
package period
import (
"regexp"
"strings"
)
// labelRe matches the period part of a lesson label: "5", "C" or
// "RLE GROUP D". A school event's first part ("End of Year Senior Music
// Concert") does not match, which is how a lesson is told from an event.
var labelRe = regexp.MustCompile(`^(\d{1,2}|[A-Z]|RLE GROUP [A-Z])$`)
// IsPeriodLabel reports whether s is a period slot rather than an event name.
func IsPeriodLabel(s string) bool {
return labelRe.MatchString(strings.TrimSpace(s))
}
// Split breaks a lesson title into its components. ok is false when the first
// part is not a period label, so the caller renders the title unchanged.
//
// A lesson may have fewer than four parts when Compass omits the room or the
// teacher ("1 - 11HLEGB5 -"), so missing components come back as "".
func Split(title string) (period, subject, room, teacher string, ok bool) {
parts := SplitDash(title)
if len(parts) < 2 || !IsPeriodLabel(parts[0]) {
return "", "", "", "", false
}
period = parts[0]
subject = parts[1]
if len(parts) >= 3 {
room = parts[2]
}
if len(parts) >= 4 {
teacher = parts[3]
}
return period, subject, room, teacher, true
}
// SplitDash splits on " - " and trims each part.
func SplitDash(s string) []string {
raw := strings.Split(s, " - ")
out := make([]string, 0, len(raw))
for _, p := range raw {
out = append(out, strings.TrimSpace(p))
}
return out
}