From 56c21bcf03ebdebfae84739220a566d77fba098b Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Thu, 7 May 2026 23:38:06 +0200 Subject: [PATCH] fix(go): accept single-digit day/month in attendance date headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseDates was using "02.01.2006" / "01/02/2006" which require zero-padded fields. The Czech attendance sheet headers contain dates like "1.6.2026", "23.3.2026", "6.4.2026" — Go silently dropped those columns while Python's strptime accepted them. Effect was a missing 2026-06 month on /api/juniors plus undercounted attendance in any month with single-digit columns; surfaced via make parity. Use the unpadded reference forms "2.1.2006" / "1/2/2006" instead — Go's time.Parse accepts both padded and unpadded inputs against them. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 5 +++++ go/internal/services/membership/sources.go | 8 ++++++- .../services/membership/sources_test.go | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba4960a..a3909d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2026-05-07 23:37 CEST — fix(go): accept single-digit day/month in attendance date headers + +- `go/internal/services/membership/sources.go`: `parseDates` now uses Go time formats `2.1.2006` and `1/2/2006` (single-digit reference forms, which accept both padded and unpadded inputs) instead of `02.01.2006` and `01/02/2006`. The Czech attendance sheet headers contain dates like `1.6.2026`, `23.3.2026`, `6.4.2026` — Go silently dropped those columns under the strict zero-padded format, while Python's `strptime("%d.%m.%Y")` accepted them. Effect was a missing `2026-06` month entirely on `/api/juniors` plus undercounted attendance for any month with single-digit columns; both surfaced as diffs in `make parity`. +- `sources_test.go::TestParseDates_SingleDigitDayMonth` added as a regression guard covering both Czech and US format flavours with and without leading zeros. + ## 2026-05-07 23:17 CEST — fix(go): pass raw value to FormatDate so numeric serial-day dates format - `go/internal/services/membership/sources.go`: transaction-row parser now passes `row[idxDate]` directly to `matching.FormatDate` (via a new `getRaw` helper) instead of stringifying first via `getVal`. The Sheets API returns numeric serial-day values as `float64` for date-formatted cells; pre-stringifying them defeated `FormatDate`'s `case float64:` dispatch, causing all numeric dates to leak through as `"46147"` style strings instead of `"2026-05-05"`. diff --git a/go/internal/services/membership/sources.go b/go/internal/services/membership/sources.go index 8f70507..832a9b2 100644 --- a/go/internal/services/membership/sources.go +++ b/go/internal/services/membership/sources.go @@ -142,7 +142,13 @@ func parseDates(header []string) []struct { } var dt time.Time var err error - for _, fmt_ := range []string{"02.01.2006", "01/02/2006"} { + // Use the unpadded reference forms ("2.1" and "1/2"): Go's time.Parse + // accepts both single-digit and zero-padded inputs against them, so + // "1.6.2026", "01.06.2026", "23.3.2026" all parse. Czech sheet authors + // drop the leading zero on dates ≤ 9 — Python's strptime is lenient + // the same way; the previous "02.01.2006" form silently dropped those + // columns and undercounted attendance. + for _, fmt_ := range []string{"2.1.2006", "1/2/2006"} { dt, err = time.Parse(fmt_, raw) if err == nil { break diff --git a/go/internal/services/membership/sources_test.go b/go/internal/services/membership/sources_test.go index e18c3c2..41b1344 100644 --- a/go/internal/services/membership/sources_test.go +++ b/go/internal/services/membership/sources_test.go @@ -174,6 +174,28 @@ func TestLoadExceptions(t *testing.T) { } } +// TestParseDates_SingleDigitDayMonth covers the regression where Go's strict +// "02.01.2006" format dropped header cells written without leading zeros +// (e.g. "1.6.2026", "23.3.2026"), causing attendance undercounts and missing +// months on the /api/juniors response. Czech sheet authors drop the zero +// pad freely; Python's strptime tolerates it, so the parsers must match. +func TestParseDates_SingleDigitDayMonth(t *testing.T) { + // Czech form ("DD.MM.YYYY", with leading zeros optional) is the primary + // path. The "M/D/YYYY" fallback mirrors Python's %m/%d/%Y secondary + // strptime branch — month-first, day-second. + header := []string{"Jméno", "Tier", "", "01.06.2026", "1.6.2026", "23.3.2026", "6.4.2026", "01/02/2026", "1/2/2026"} + got := parseDates(header) + want := []string{"2026-06", "2026-06", "2026-03", "2026-04", "2026-01", "2026-01"} + if len(got) != len(want) { + t.Fatalf("parseDates: got %d entries, want %d (%v)", len(got), len(want), got) + } + for i, e := range got { + if e.month != want[i] { + t.Errorf("parseDates[%d].month = %q, want %q (raw=%q)", i, e.month, want[i], header[e.col]) + } + } +} + // TTL smoke test: second call within TTL must not call fetch again. func TestLoadAdults_CacheHit(t *testing.T) { dir := t.TempDir() -- 2.49.1