mirror of
https://github.com/therootcompany/golib.git
synced 2026-11-04 06:55:00 +00:00
BoolishTimestamp defaulted unrecognized non-empty values to true: any word it does not understand (a misspelling, "disabled", "banana") behaved as on. Unrecognized values are now valid+false with the error retained so callers can apply their own policy, and "disabled"/"disable" are no longer true-like. Both bool types now read the human "not supplied" set through one shared isBoolNull sentinel: empty/whitespace-only, the exact PG dump sentinel \N (case-sensitive), and the case-insensitive words null, \null, none, unknown. Bool previously reached absent only by ParseBool failing; it now matches BoolishTimestamp explicitly. Free-text types must not reuse this set: most of these words are legitimate values there.
75 lines
2.2 KiB
Go
75 lines
2.2 KiB
Go
package csvx
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// BoolishTimestamp accepts optional true/false-like values and dates or
|
|
// timestamps from human-edited CSV/TSV data. Valid reports that a value was
|
|
// supplied; Bool reports its true-like meaning. Error records an unrecognized
|
|
// non-empty value without making the CSV decoder reject the row.
|
|
type BoolishTimestamp struct {
|
|
Valid bool
|
|
Bool bool
|
|
Timestamp time.Time
|
|
Error error
|
|
}
|
|
|
|
// UnmarshalCSV parses a human value. Recognized true-like values and dates
|
|
// set Bool; recognized false-like values leave it unset. Unrecognized
|
|
// non-empty values are conservatively false with the error retained so the
|
|
// collection mapper can apply its own policy.
|
|
func (v *BoolishTimestamp) UnmarshalCSV(data []byte) error {
|
|
*v = BoolishTimestamp{}
|
|
raw := strings.TrimSpace(string(data))
|
|
// Null sentinels (isBoolNull): absent means Valid=false. `\N` is the
|
|
// exact PG dump sentinel and case-sensitive; the word forms are not.
|
|
if isBoolNull(raw) {
|
|
return nil
|
|
}
|
|
v.Valid = true
|
|
switch strings.ToLower(raw) {
|
|
case "false", "f", "n", "no", "0":
|
|
return nil
|
|
case "true", "t", "y", "yes", "1":
|
|
v.Bool = true
|
|
return nil
|
|
}
|
|
for _, layout := range []string{time.RFC3339, "2006-01-02", "2006-01-02 15:04:05Z07:00", "2006-01-02 15:04:05"} {
|
|
parsed, err := time.Parse(layout, raw)
|
|
if err == nil {
|
|
v.Bool = true
|
|
v.Timestamp = parsed
|
|
return nil
|
|
}
|
|
}
|
|
// Unrecognized: valid but false, error recorded (Bool must not default
|
|
// to true for values this type does not understand).
|
|
v.Bool = false
|
|
v.Error = fmt.Errorf("unrecognized boolish timestamp %q", raw)
|
|
return nil
|
|
}
|
|
|
|
// MarshalCSV writes a normalized usable record. An unrecognized value is
|
|
// conservatively serialized as the current UTC timestamp.
|
|
func (v BoolishTimestamp) MarshalCSV() ([]byte, error) {
|
|
if !v.Valid {
|
|
return nil, nil
|
|
}
|
|
if v.Error != nil {
|
|
return []byte(time.Now().UTC().Format("2006-01-02 15:04:05")), nil
|
|
}
|
|
if !v.Bool {
|
|
return []byte("FALSE"), nil
|
|
}
|
|
if !v.Timestamp.IsZero() {
|
|
return []byte(v.Timestamp.UTC().Format("2006-01-02 15:04:05")), nil
|
|
}
|
|
return []byte("TRUE"), nil
|
|
}
|
|
|
|
// IsZero lets encoders treat the zero value as an omitted source value.
|
|
func (v BoolishTimestamp) IsZero() bool { return !v.Valid }
|