Load whatever is well-formed and means one thing: ignore a part with no effect, print a constant sample, render any JSON value, draw no weight-0 item, and let a name, a rows file, a struct or a CLI flag go unused
This commit is contained in:
@@ -186,9 +186,6 @@ func intRangeArgs(a []string) error {
|
||||
if lo > hi {
|
||||
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
|
||||
}
|
||||
if lo == hi {
|
||||
return fmt.Errorf("int(%d,%d) is the constant %d; write it as text", lo, hi, lo)
|
||||
}
|
||||
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
|
||||
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
|
||||
}
|
||||
@@ -213,9 +210,6 @@ func floatArgs(a []string) error {
|
||||
if dp < 0 || dp > maxDecimals {
|
||||
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
|
||||
}
|
||||
if lo == hi {
|
||||
return fmt.Errorf("float(%s,%s,%d) is the constant %q; write it as text", a[0], a[1], dp, strconv.FormatFloat(lo, 'f', dp, 64))
|
||||
}
|
||||
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
||||
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
||||
}
|
||||
|
||||
@@ -11,32 +11,12 @@ import (
|
||||
|
||||
const dayLayout = "2006-01-02"
|
||||
|
||||
// The instants a layout is proved against: layoutProbe2 is alike in no field, while
|
||||
// layoutDay differs from layoutProbe in its date fields alone and layoutClock in its
|
||||
// clock fields alone, so formatting two of them tells which kind a layout names.
|
||||
var (
|
||||
layoutProbe = time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC)
|
||||
layoutProbe2 = time.Date(2010, 11, 12, 13, 14, 15, 0, time.UTC)
|
||||
layoutDay = time.Date(2010, 11, 12, 4, 5, 6, 0, time.UTC)
|
||||
layoutClock = time.Date(2001, 2, 3, 13, 14, 15, 0, time.UTC)
|
||||
)
|
||||
|
||||
func namesAField(layout string) bool {
|
||||
return layoutProbe.Format(layout) != layoutProbe2.Format(layout)
|
||||
}
|
||||
func namesADateField(layout string) bool {
|
||||
return layoutProbe.Format(layout) != layoutDay.Format(layout)
|
||||
}
|
||||
func namesAClockField(layout string) bool {
|
||||
return layoutProbe.Format(layout) != layoutClock.Format(layout)
|
||||
}
|
||||
|
||||
// quotedLayout reports whether an arg carries the single quotes a layout is written in.
|
||||
func quotedLayout(a string) bool {
|
||||
return len(a) >= 2 && a[0] == '\'' && a[len(a)-1] == '\''
|
||||
}
|
||||
|
||||
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
||||
// layoutArg is the Go layout a quoted arg holds, refused when unquoted.
|
||||
// docs/decisions.md#a-layout-is-always-quoted
|
||||
func layoutArg(a string) (string, error) {
|
||||
if !quotedLayout(a) {
|
||||
@@ -46,11 +26,7 @@ func layoutArg(a string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("layout %s is not quoted; write '%s'", a, bare)
|
||||
}
|
||||
layout := a[1 : len(a)-1]
|
||||
if !namesAField(layout) {
|
||||
return "", fmt.Errorf("layout '%s' names no field, so it is the constant %q; write it as text", layout, layout)
|
||||
}
|
||||
return layout, nil
|
||||
return a[1 : len(a)-1], nil
|
||||
}
|
||||
|
||||
// layoutOf is layoutArg for an arg a check already validated.
|
||||
@@ -100,30 +76,15 @@ func dateArgs(a []string) error {
|
||||
if to.Before(from) {
|
||||
return fmt.Errorf("date(from,to,layout): from %s is after to %s", a[0], a[1])
|
||||
}
|
||||
layout, err := layoutArg(a[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !namesADateField(layout) {
|
||||
return fmt.Errorf("date(from,to,layout): '%s' names no date field; write time('%s')", layout, layout)
|
||||
}
|
||||
if from.Equal(to) && !namesAClockField(layout) {
|
||||
return fmt.Errorf("date(%s,%s,'%s') is the constant %q; write it as text", a[0], a[1], layout, from.Format(layout))
|
||||
}
|
||||
return nil
|
||||
_, err = layoutArg(a[2])
|
||||
return err
|
||||
}
|
||||
func timeArg(a []string) error {
|
||||
if err := layoutArity("time", 1, a); err != nil {
|
||||
return err
|
||||
}
|
||||
layout, err := layoutArg(a[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if namesADateField(layout) {
|
||||
return fmt.Errorf("time(layout): '%s' names a date field; write date(from,to,layout)", layout)
|
||||
}
|
||||
return nil
|
||||
_, err := layoutArg(a[0])
|
||||
return err
|
||||
}
|
||||
|
||||
// datePrep draws a second in [from 00:00:00, to 23:59:59] UTC; the span is counted
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package datafiles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -38,8 +39,7 @@ type Category struct {
|
||||
rows *rowsFiles
|
||||
}
|
||||
|
||||
// ReadRows reads a rows file beside the category and marks it as named; Walk refuses a rows
|
||||
// file no category read.
|
||||
// ReadRows reads a rows file beside the category.
|
||||
func (c Category) ReadRows(name string) (string, error) { return c.rows.read(name) }
|
||||
|
||||
func (s Source) labelled(p string) string {
|
||||
@@ -101,11 +101,6 @@ func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error
|
||||
}
|
||||
handed = handed || holds
|
||||
}
|
||||
for name, named := range rows.tsv {
|
||||
if !named && !strings.HasPrefix(name, ".") {
|
||||
return false, fmt.Errorf("%s: no category names it in its rows; a table's rows file sits beside a category file naming it", s.labelled(path.Join(full, name)))
|
||||
}
|
||||
}
|
||||
return handed, nil
|
||||
}
|
||||
|
||||
@@ -147,8 +142,8 @@ func (s Source) compileFile(dir []string, file string, rows *rowsFiles, compile
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w", s.labelled(full), err)
|
||||
}
|
||||
var raw any
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
raw, err := DecodeJSON(b)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w", s.labelled(full), err)
|
||||
}
|
||||
if err := compile(Category{Folders: dir, Name: name, JSON: raw, rows: rows}); err != nil {
|
||||
@@ -157,8 +152,7 @@ func (s Source) compileFile(dir []string, file string, rows *rowsFiles, compile
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// rowsFiles is what a category may name beside itself: the rows files of its folder,
|
||||
// each marked once a category names it.
|
||||
// rowsFiles is what a category may name beside itself: the rows files of its folder.
|
||||
type rowsFiles struct {
|
||||
src Source
|
||||
dir string
|
||||
@@ -169,20 +163,30 @@ func newRowsFiles(src Source, dir string, entries []fs.DirEntry) *rowsFiles {
|
||||
files := &rowsFiles{src: src, dir: dir, tsv: map[string]bool{}}
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".tsv") && !e.IsDir() {
|
||||
files.tsv[e.Name()] = false
|
||||
files.tsv[e.Name()] = true
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (r *rowsFiles) read(name string) (string, error) {
|
||||
if _, present := r.tsv[name]; !present {
|
||||
if !r.tsv[name] {
|
||||
return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, r.src.labelled(r.dir))
|
||||
}
|
||||
r.tsv[name] = true
|
||||
b, err := fs.ReadFile(r.src.fsys, path.Join(r.dir, name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", r.src.labelled(path.Join(r.dir, name)), err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// DecodeJSON decodes one JSON value, keeping each number as the json.Number it is written as.
|
||||
func DecodeJSON(b []byte) (any, error) {
|
||||
var v any
|
||||
if !json.Valid(b) {
|
||||
return nil, json.Unmarshal(b, &v)
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(b))
|
||||
dec.UseNumber()
|
||||
return v, dec.Decode(&v)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,15 @@ func (s *State) IntN(n int) int { return s.rand.IntN(n) }
|
||||
|
||||
func (s *State) Float64() float64 { return s.rand.Float64() }
|
||||
|
||||
// Weighted draws an index into cum, a running sum of positive weights, with odds by weight.
|
||||
// Weighted draws an index into cum, a running sum of weights of 0 or more with a positive total,
|
||||
// with odds by weight, so an index of weight 0 is never drawn.
|
||||
func (s *State) Weighted(cum []float64) int {
|
||||
x := s.Float64() * cum[len(cum)-1]
|
||||
return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total
|
||||
total := cum[len(cum)-1]
|
||||
x := s.Float64() * total
|
||||
if i := sort.Search(len(cum), func(i int) bool { return cum[i] > x }); i < len(cum) {
|
||||
return i
|
||||
}
|
||||
return sort.Search(len(cum), func(i int) bool { return cum[i] >= total }) // x rounded up to the total
|
||||
}
|
||||
|
||||
// Seq advances the counter {seq(key)} reads, or {seq()} for an empty key, and returns it.
|
||||
|
||||
+30
-5
@@ -209,8 +209,8 @@ func (t *Table[O]) proveKeys() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sumWeights proves every weight is a positive number, and keeps the weights and their
|
||||
// running sum.
|
||||
// sumWeights proves every weight is a number of 0 or more, some above 0, and keeps the weights
|
||||
// and their running sum.
|
||||
func (t *Table[O]) sumWeights() error {
|
||||
if t.weightIndex < 0 {
|
||||
return nil
|
||||
@@ -219,14 +219,39 @@ func (t *Table[O]) sumWeights() error {
|
||||
total := 0.0
|
||||
for r := range t.cum {
|
||||
w, err := strconv.ParseFloat(t.Cell(r, t.weightIndex), 64)
|
||||
if err != nil || math.IsInf(w, 0) || math.IsNaN(w) || w <= 0 {
|
||||
return fmt.Errorf("%s line %d: weight %q is not a positive number", t.file, r+2, t.Cell(r, t.weightIndex))
|
||||
if err != nil || math.IsInf(w, 0) || math.IsNaN(w) || w < 0 {
|
||||
return fmt.Errorf("%s line %d: weight %q is not a number of 0 or more", t.file, r+2, t.Cell(r, t.weightIndex))
|
||||
}
|
||||
total += w
|
||||
t.weights[r], t.cum[r] = w, total
|
||||
}
|
||||
if math.IsInf(total, 0) {
|
||||
switch {
|
||||
case math.IsInf(total, 0):
|
||||
return fmt.Errorf("%s: the weights sum past the largest number", t.file)
|
||||
case total == 0:
|
||||
return fmt.Errorf("%s: every weight is 0, so no row is drawn", t.file)
|
||||
}
|
||||
return t.proveWeightUnderEachParent()
|
||||
}
|
||||
|
||||
// proveWeightUnderEachParent proves each parent key's rows weigh more than 0 together, so a draw
|
||||
// inside that parent's row has a row to draw.
|
||||
func (t *Table[O]) proveWeightUnderEachParent() error {
|
||||
if t.parentIndex < 0 {
|
||||
return nil
|
||||
}
|
||||
sums, keys := map[string]float64{}, []string{}
|
||||
for r := range t.weights {
|
||||
k := t.Cell(r, t.parentIndex)
|
||||
if _, seen := sums[k]; !seen {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sums[k] += t.weights[r]
|
||||
}
|
||||
for _, k := range keys {
|
||||
if sums[k] == 0 {
|
||||
return fmt.Errorf("%s: every row under %s %q weighs 0, so none is drawn there", t.file, t.header[t.parentIndex], k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user