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:
2026-10-07 23:39:59 +02:00
committed by Lillem4n
parent 052a52fc7a
commit ee236d7955
14 changed files with 165 additions and 213 deletions
-6
View File
@@ -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)
}
+6 -45
View File
@@ -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
+18 -14
View File
@@ -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)
}
+8 -3
View File
@@ -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
View File
@@ -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
}