Files
zeekayandHanzo Dev 997e8a55bf normalize: read dates, numbers, quantities and money, and refuse what is ambiguous
The package comment named Entity, Date, Number, Measure and Rows, none of
which existed. Four kinds now do, each a Kind[T]: Parse reads text under an
Anchor, Canon writes the one canonical form, and every Kind is also a
Datatype for code that holds several side by side.

- Date reads into a half-open Span with a Grain: "March 2023" is
  [2023-03-01, 2023-04-01). Relative dates count from Anchor.At, the
  source's own date; nothing calls time.Now.
- Number reads exactly into a big.Rat; Quantity adds a unit, folded to its
  symbol only where the word has one meaning; Money adds an ISO 4217 code.

What the text leaves open is refused with ErrAmbiguous, never guessed: day
and month with no Anchor.Order, a clock time with no offset or Anchor.Zone,
or one a zone repeats, "1,234" with no Anchor.Point, a two-digit year, and a
sign several currencies write. One generic test holds Canon∘Parse
idempotent for every kind.

The package comment now names what is there, and the normalize example
shows the kinds.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 00:35:46 -07:00

101 lines
3.4 KiB
Go

package normalize
import (
"errors"
"fmt"
"time"
)
// Errors the typed readers report, wrapped with the kind, the text and the
// reason. Test for them with errors.Is.
var (
// ErrAmbiguous means the text has more than one reading and the Anchor
// does not say which: a day and a month in an order it does not give, a
// time with no zone, a separator that may group digits or mark a
// decimal, a currency sign several currencies use, a relative date with
// no date to count from.
ErrAmbiguous = errors.New("ambiguous")
// ErrSyntax means the text is not a value of the kind at all.
ErrSyntax = errors.New("not readable")
)
// refuse is the error for text a kind will not read.
func refuse(kind, s string, err error, why string) error {
return fmt.Errorf("normalize: %s %q: %w: %s", kind, s, err, why)
}
// Anchor is what a source says about how to read the values in it: the
// context a date or a number needs and the text alone does not carry. The
// zero Anchor assumes nothing, and anything whose reading depends on it is
// refused rather than read one way by default.
type Anchor struct {
// At is the source's own date — when the letter was written, the page
// published, the message sent — and relative dates such as "yesterday"
// are counted from it, on its calendar. It is never the time of reading:
// a document says "yesterday" about the day before it was written, and
// nothing here calls time.Now. Zero refuses a relative date.
At time.Time
// Zone places a time written without an offset. Nil refuses one.
Zone *time.Location
// Order is the order of day and month in an all-numeric date. Zero
// refuses one that does not begin with its year.
Order Order
// Point is the decimal mark, '.' or ','; the other is then a digit group
// separator. Zero reads a mark only where it can be one thing, and
// refuses "1,234" and "1.234", which can be either.
Point rune
}
// Order is the order of the day and the month in an all-numeric date.
type Order int
// The two orders in use. A date that begins with a four-digit year is read
// year, month, day whatever the Order.
const (
DMY Order = iota + 1 // 31/12/2023
MDY // 12/31/2023
)
// Kind is one kind of value: how to read it from text under an Anchor, and how
// to write it in its one canonical form.
//
// Canon writes text that Parse reads back under Anchor{Point: '.'} to the same
// value — ISO 8601 dates with their offsets, numbers with a point and no
// grouping — so Canon∘Parse settles every spelling of a value on one string,
// and applying it again changes nothing.
type Kind[T any] struct {
Name string
Parse func(Anchor, string) (T, error)
Canon func(T) string
}
// Datatype is a Kind seen without its Go type, for code that holds several
// kinds side by side — a schema whose properties each name one — and needs
// only the canonical text.
type Datatype interface {
// String is the kind's name.
String() string
// Norm reads s under a and writes it canonically.
Norm(a Anchor, s string) (string, error)
}
// String returns the kind's name.
func (k Kind[T]) String() string { return k.Name }
// Norm reads s under a and writes the value in its canonical form.
func (k Kind[T]) Norm(a Anchor, s string) (string, error) {
v, err := k.Parse(a, s)
if err != nil {
return "", err
}
return k.Canon(v), nil
}
// Every kind this package defines is a Datatype.
var (
_ Datatype = Date
_ Datatype = Number
_ Datatype = Quantity
_ Datatype = Money
)