feat(csvx): BoolishTimestamp retains the original word via Raw()

Unrecognized non-empty values keep the trimmed original in Raw() so
callers can apply column-specific word policy post-parse (e.g. reading
"disabled" as true in a Disabled column) instead of string-matching the
error message.
This commit is contained in:
AJ ONeal 2026-10-24 00:54:50 -06:00
parent 59fae2e7ae
commit 914b83a8d6
No known key found for this signature in database
2 changed files with 16 additions and 2 deletions

View File

@ -9,14 +9,23 @@ import (
// BoolishTimestamp accepts optional true/false-like values and dates or // BoolishTimestamp accepts optional true/false-like values and dates or
// timestamps from human-edited CSV/TSV data. Valid reports that a value was // timestamps from human-edited CSV/TSV data. Valid reports that a value was
// supplied; Bool reports its true-like meaning. Error records an unrecognized // supplied; Bool reports its true-like meaning. Error records an unrecognized
// non-empty value without making the CSV decoder reject the row. // non-empty value without making the CSV decoder reject the row. Raw reports
// the trimmed original value for unrecognized values so callers can apply
// their own column-specific word policy (e.g. reading "disabled" as true in
// a Disabled column).
type BoolishTimestamp struct { type BoolishTimestamp struct {
Valid bool Valid bool
Bool bool Bool bool
Timestamp time.Time Timestamp time.Time
Error error Error error
raw string
} }
// Raw returns the trimmed original value that was parsed, retained for
// unrecognized values so callers can apply their own word policy.
func (v BoolishTimestamp) Raw() string { return v.raw }
// UnmarshalCSV parses a human value. Recognized true-like values and dates // UnmarshalCSV parses a human value. Recognized true-like values and dates
// set Bool; recognized false-like values leave it unset. Unrecognized // set Bool; recognized false-like values leave it unset. Unrecognized
// non-empty values are conservatively false with the error retained so the // non-empty values are conservatively false with the error retained so the
@ -46,8 +55,10 @@ func (v *BoolishTimestamp) UnmarshalCSV(data []byte) error {
} }
} }
// Unrecognized: valid but false, error recorded (Bool must not default // Unrecognized: valid but false, error recorded (Bool must not default
// to true for values this type does not understand). // to true for values this type does not understand). The original value
// is retained for caller-side word policy.
v.Bool = false v.Bool = false
v.raw = raw
v.Error = fmt.Errorf("unrecognized boolish timestamp %q", raw) v.Error = fmt.Errorf("unrecognized boolish timestamp %q", raw)
return nil return nil
} }

View File

@ -223,6 +223,9 @@ func TestBoolishTimestamp(t *testing.T) {
if !v.Valid || v.Bool || v.Error == nil { if !v.Valid || v.Bool || v.Error == nil {
t.Fatalf("%q: expected valid+false+error, got %+v", word, v) t.Fatalf("%q: expected valid+false+error, got %+v", word, v)
} }
if v.Raw() != word {
t.Fatalf("%q: Raw() = %q, want the original word retained", word, v.Raw())
}
} }
// Empty / null sentinels: `\N` exact; word forms any case. The full // Empty / null sentinels: `\N` exact; word forms any case. The full