@@ -0,0 +1,508 @@
// Package rows holds a table's rows: the TSV they are read from, the options proved over
// them, the links between tables, and the rows a draw takes or a selector names.
package rows
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
)
// Table is the rows of one table category. P is what owns them, which a step to a linked
// table reaches through Payload.
type Table [ P any ] struct {
payload P
segment string // path's last segment
path string // the category's path from the data root, which a selector is written at
file string
options Columns
header [ ] string
col map [ string ] int
cells [ ] string // rows × columns, flat
keyIndex int // keyIndex through parentIndex are column indexes, -1 where the option is absent
nameIndex int
weightIndex int
parentIndex int
weights [ ] float64 // nil when uniform
cum [ ] float64 // cumulative weights, nil when uniform
byKey map [ string ] int
parent * Table [ P ]
children map [ string ] * Table [ P ]
once sync . Once
lookup rowLookup
}
// Columns names the column each option reads, "" where the option is absent.
type Columns struct {
Key , Name , Parent , Weight string
}
// rowLookup is what selection and linked draws look up, built on the first draw
// that needs it.
type rowLookup struct {
byName map [ string ] [ ] int
rowsByParent map [ string ] [ ] int
childCum map [ string ] [ ] float64 // cumulative weights per parent key, nil when uniform
}
// Parse reads the TSV data, the rows of the table at path, and proves what the options
// claim of them. payload is what owns the rows.
func Parse [ P any ] ( payload P , segment , path , file , data string , o Columns ) ( * Table [ P ] , error ) {
t := & Table [ P ] { payload : payload , segment : segment , path : path , file : file , options : o , keyIndex : - 1 , nameIndex : - 1 , weightIndex : - 1 , parentIndex : - 1 }
if err := t . parseRows ( data ) ; err != nil {
return nil , fmt . Errorf ( "%s: %w" , file , err )
}
if err := t . bindOptions ( ) ; err != nil {
return nil , err
}
if err := t . checkSelectorCells ( ) ; err != nil {
return nil , fmt . Errorf ( "%s: %w" , file , err )
}
return t , nil
}
func ( t * Table [ P ] ) Payload ( ) P { return t . payload }
func ( t * Table [ P ] ) Segment ( ) string { return t . segment }
func ( t * Table [ P ] ) Path ( ) string { return t . path }
func ( t * Table [ P ] ) File ( ) string { return t . file }
func ( t * Table [ P ] ) Options ( ) Columns { return t . options }
func ( t * Table [ P ] ) Header ( ) [ ] string { return t . header }
func ( t * Table [ P ] ) Parent ( ) * Table [ P ] { return t . parent }
// LinkColumn is the index of the column holding the parent's key, -1 where there is none.
func ( t * Table [ P ] ) LinkColumn ( ) int { return t . parentIndex }
func ( t * Table [ P ] ) Column ( name string ) ( int , bool ) {
i , ok := t . col [ name ]
return i , ok
}
func ( t * Table [ P ] ) Len ( ) int { return len ( t . cells ) / len ( t . header ) }
func ( t * Table [ P ] ) Cell ( row , col int ) string { return t . cells [ row * len ( t . header ) + col ] }
// parseRows reads the TSV: the header line names the columns, each following line
// is a row of as many cells. Cells are substrings of data, so the file is held once.
func ( t * Table [ P ] ) parseRows ( data string ) error {
data = strings . TrimSuffix ( strings . TrimPrefix ( data , "\xEF\xBB\xBF" ) , "\n" )
header , rest , _ := strings . Cut ( data , "\n" )
header = strings . TrimSuffix ( header , "\r" )
if header == "" {
return fmt . Errorf ( "has no header line naming its columns" )
}
t . header = strings . Split ( header , "\t" )
t . col = make ( map [ string ] int , len ( t . header ) )
for i , name := range t . header {
if err := grammar . CheckName ( name ) ; err != nil {
return fmt . Errorf ( "column %w" , err )
}
if _ , dup := t . col [ name ] ; dup {
return fmt . Errorf ( "column %q is named twice" , name )
}
t . col [ name ] = i
}
if header == data {
return fmt . Errorf ( "has no rows below its header; a table is at least two rows" )
}
m := len ( t . header )
t . cells = make ( [ ] string , 0 , m * ( strings . Count ( rest , "\n" ) + 1 ) )
for line := 2 ; ; line ++ {
row , more , found := strings . Cut ( rest , "\n" )
row = strings . TrimSuffix ( row , "\r" )
if err := t . appendRow ( row , line ) ; err != nil {
return err
}
if ! found {
break
}
rest = more
}
if t . Len ( ) < 2 {
return fmt . Errorf ( "has one row, which is a template; write it as one" )
}
return nil
}
// appendRow splits one line into as many cells as the header has columns.
func ( t * Table [ P ] ) appendRow ( row string , line int ) error {
if row == "" {
return fmt . Errorf ( "line %d is empty; every line below the header is a row" , line )
}
m := len ( t . header )
for n := 0 ; n < m ; n ++ {
cell , next , tab := strings . Cut ( row , "\t" )
if ! tab && n < m - 1 || tab && n == m - 1 {
return fmt . Errorf ( "line %d has %s cells than the %d columns" , line , map [ bool ] string { true : "more" , false : "fewer" } [ tab ] , m )
}
t . cells = append ( t . cells , cell )
row = next
}
return nil
}
// bindOptions resolves each option to its column and proves what it claims of the
// cells: a key is unique, a weight a positive number.
func ( t * Table [ P ] ) bindOptions ( ) error {
o := t . options
for _ , opt := range [ ] struct {
name , value string
into * int
} { { "key" , o . Key , & t . keyIndex } , { "name" , o . Name , & t . nameIndex } , { "parent" , o . Parent , & t . parentIndex } , { "weight" , o . Weight , & t . weightIndex } } {
if opt . value == "" {
continue
}
i , ok := t . col [ opt . value ]
if ! ok {
return fmt . Errorf ( "%s names no column %q of %s; the columns are %v" , opt . name , opt . value , t . file , t . header )
}
* opt . into = i
}
if t . nameIndex >= 0 && t . keyIndex < 0 && t . parentIndex < 0 {
return fmt . Errorf ( "name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column, or a parent inside which each name is one row; add key" )
}
if err := t . proveKeys ( ) ; err != nil {
return err
}
if err := t . proveNamesInsideParent ( ) ; err != nil {
return err
}
return t . sumWeights ( )
}
// proveNamesInsideParent proves a name without a key names one row inside its parent.
func ( t * Table [ P ] ) proveNamesInsideParent ( ) error {
if t . keyIndex >= 0 || t . nameIndex < 0 {
return nil
}
inside := make ( map [ string ] int , t . Len ( ) )
for r := 0 ; r < t . Len ( ) ; r ++ {
k := t . Cell ( r , t . parentIndex ) + "\t" + t . Cell ( r , t . nameIndex )
if first , dup := inside [ k ] ; dup {
return fmt . Errorf ( "%s line %d: name %q repeats line %d inside %s %q; a name selects one row inside its parent; drop one, or add a key column" , t . file , r + 2 , t . Cell ( r , t . nameIndex ) , first + 2 , t . header [ t . parentIndex ] , t . Cell ( r , t . parentIndex ) )
}
inside [ k ] = r
}
return nil
}
// proveKeys proves every key names one row, and keeps the map a link is proved by.
func ( t * Table [ P ] ) proveKeys ( ) error {
if t . keyIndex < 0 {
return nil
}
t . byKey = make ( map [ string ] int , t . Len ( ) )
for r := 0 ; r < t . Len ( ) ; r ++ {
k := t . Cell ( r , t . keyIndex )
if k == "" {
return fmt . Errorf ( "%s line %d: the key is empty" , t . file , r + 2 )
}
if first , dup := t . byKey [ k ] ; dup {
return fmt . Errorf ( "%s line %d: key %q repeats line %d; a key selects one row" , t . file , r + 2 , k , first + 2 )
}
t . byKey [ k ] = r
}
for r := 0 ; r < t . Len ( ) && t . nameIndex >= 0 ; r ++ {
if n := t . Cell ( r , t . nameIndex ) ; t . byKey [ n ] != r {
if other , isKey := t . byKey [ n ] ; isKey {
return fmt . Errorf ( "%s line %d: name %q is the key of line %d, which a selector reads first, so the name could never select this row" , t . file , r + 2 , n , other + 2 )
}
}
}
return nil
}
// sumWeights proves every weight is a positive number, and keeps the weights and their
// running sum.
func ( t * Table [ P ] ) sumWeights ( ) error {
if t . weightIndex < 0 {
return nil
}
t . weights , t . cum = make ( [ ] float64 , t . Len ( ) ) , make ( [ ] float64 , t . Len ( ) )
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 ) )
}
total += w
t . weights [ r ] , t . cum [ r ] = w , total
}
if math . IsInf ( total , 0 ) {
return fmt . Errorf ( "%s: the weights sum past the largest number" , t . file )
}
return nil
}
// checkSelectorCells refuses a key or name cell a selector cannot spell.
func ( t * Table [ P ] ) checkSelectorCells ( ) error {
for r := 0 ; r < t . Len ( ) ; r ++ {
for _ , col := range [ ] int { t . keyIndex , t . nameIndex } {
if col < 0 {
continue
}
if c := grammar . UnspellableInSelector ( t . Cell ( r , col ) ) ; c != "" {
return fmt . Errorf ( "line %d: %s %q contains %q, which a selector cannot spell" , r + 2 , t . header [ col ] , t . Cell ( r , col ) , c )
}
}
}
return nil
}
// Link links t to p, the table its link column names, after proving p has a key, every
// link cell is one, and every row of p is linked to. sibling is the table beside t
// named name, nil where none is. It is the only writer of a table's parent and children.
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
func ( t * Table [ P ] ) Link ( p * Table [ P ] , sibling func ( name string ) * Table [ P ] ) error {
name := t . header [ t . parentIndex ]
if p . keyIndex < 0 {
return fmt . Errorf ( "parent %q has no key column to link to" , name )
}
if err := t . checkAncestors ( p , sibling ) ; err != nil {
return err
}
linked := make ( map [ string ] bool , p . Len ( ) )
for r := 0 ; r < t . Len ( ) ; r ++ {
k := t . Cell ( r , t . parentIndex )
if _ , ok := p . byKey [ k ] ; ! ok {
return fmt . Errorf ( "%s line %d: %s %q is no key of %s" , t . file , r + 2 , name , k , p . file )
}
linked [ k ] = true
}
for r := 0 ; r < p . Len ( ) ; r ++ {
if k := p . Cell ( r , p . keyIndex ) ; ! linked [ k ] {
return fmt . Errorf ( "%s links no row to %s %q; every %s row needs one, or drop line %d of %s" , t . file , name , k , name , r + 2 , p . file )
}
}
t . parent = p
if p . children == nil {
p . children = map [ string ] * Table [ P ] { }
}
p . children [ t . segment ] = t
return nil
}
// checkAncestors refuses a cycle in the chain of parents starting at p, t's parent, and
// refuses an ancestor that has a column named like t.
func ( t * Table [ P ] ) checkAncestors ( p * Table [ P ] , sibling func ( name string ) * Table [ P ] ) error {
var ancestors [ ] * Table [ P ]
for q , seen := p , map [ * Table [ P ] ] bool { t : true } ; q != nil ; q = sibling ( q . header [ q . parentIndex ] ) {
if seen [ q ] {
return fmt . Errorf ( "parent cycle: %s reaches itself through its parents" , q . segment )
}
seen [ q ] = true
ancestors = append ( ancestors , q )
if q . parentIndex < 0 {
break
}
}
for _ , q := range ancestors {
if _ , clash := q . col [ t . segment ] ; clash {
return fmt . Errorf ( "%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one" , t . segment , q . segment , q . segment , t . segment )
}
}
return nil
}
// Children is the tables linked to t, by segment.
func ( t * Table [ P ] ) Children ( ) [ ] * Table [ P ] {
out := make ( [ ] * Table [ P ] , 0 , len ( t . children ) )
for _ , c := range t . children {
out = append ( out , c )
}
sort . Slice ( out , func ( i , j int ) bool { return out [ i ] . segment < out [ j ] . segment } )
return out
}
// Descendant is the table named name among those linked to t, at any depth.
func ( t * Table [ P ] ) Descendant ( name string ) * Table [ P ] {
if c , ok := t . children [ name ] ; ok {
return c
}
for _ , c := range t . children {
if d := c . Descendant ( name ) ; d != nil {
return d
}
}
return nil
}
// StepUp proves a ".." after a row of t can step up: t has a parent, rest has no empty
// segment and names it, and no selector follows, since a row selected below the parent row
// could lie outside it.
func ( t * Table [ P ] ) StepUp ( rest [ ] string ) error {
if t . parent == nil {
return fmt . Errorf ( ` %s has no parent table for ".." to step up to ` , t . segment )
}
if err := grammar . CheckSegments ( rest ) ; err != nil {
return err
}
switch {
case len ( rest ) == 0 || rest [ 0 ] != t . parent . segment :
return fmt . Errorf ( ` ".." steps up from %s to its parent table, so name that next: %s ` , t . segment , t . climbTo ( rest ) )
case grammar . HasSelector ( rest ) :
return fmt . Errorf ( ` no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s ` , grammar . JoinSegments ( append ( [ ] string { t . parent . path } , rest [ 1 : ] ... ) ) )
}
return nil
}
// climbTo spells the ".." steps from t up to the ancestor rest names first, or the one step to
// t's parent where no ancestor is named.
func ( t * Table [ P ] ) climbTo ( rest [ ] string ) string {
var b strings . Builder
for a := t . parent ; a != nil ; a = a . parent {
b . WriteString ( ".." + a . segment )
if len ( rest ) > 0 && a . segment == rest [ 0 ] {
return b . String ( )
}
}
return ".." + t . parent . segment
}
func ( t * Table [ P ] ) builtLookup ( ) * rowLookup {
t . once . Do ( func ( ) {
if t . nameIndex >= 0 {
t . lookup . byName = make ( map [ string ] [ ] int , t . Len ( ) )
for r := 0 ; r < t . Len ( ) ; r ++ {
n := t . Cell ( r , t . nameIndex )
t . lookup . byName [ n ] = append ( t . lookup . byName [ n ] , r )
}
}
if t . parentIndex >= 0 {
t . lookup . rowsByParent = map [ string ] [ ] int { }
for r := 0 ; r < t . Len ( ) ; r ++ {
k := t . Cell ( r , t . parentIndex )
t . lookup . rowsByParent [ k ] = append ( t . lookup . rowsByParent [ k ] , r )
}
if t . weights != nil {
t . lookup . childCum = make ( map [ string ] [ ] float64 , len ( t . lookup . rowsByParent ) )
for k , rows := range t . lookup . rowsByParent {
cum , total := make ( [ ] float64 , len ( rows ) ) , 0.0
for i , r := range rows {
total += t . weights [ r ]
cum [ i ] = total
}
t . lookup . childCum [ k ] = cum
}
}
}
} )
return & t . lookup
}
func ( t * Table [ P ] ) Draw ( s * drawstate . State ) int {
if t . cum == nil {
return s . IntN ( t . Len ( ) )
}
return s . Weighted ( t . cum )
}
// drawUnder picks a row among those linked to parent row pr.
func ( t * Table [ P ] ) drawUnder ( s * drawstate . State , pr int ) int {
lookup := t . builtLookup ( )
k := t . parent . Cell ( pr , t . parent . keyIndex )
rows := lookup . rowsByParent [ k ]
if lookup . childCum == nil {
return rows [ s . IntN ( len ( rows ) ) ]
}
return rows [ s . Weighted ( lookup . childCum [ k ] ) ]
}
// DrawIn is the row of t a path or a named pick reads: the one pinned in p, else one drawn inside the nearest
// pinned ancestor — its parent drawn inside that first where the ancestor is further up — or
// over the whole table, and pinned with its ancestors.
func ( t * Table [ P ] ) DrawIn ( s * drawstate . State , p * Pins [ P ] ) int {
if r , ok := p . Pinned ( t ) ; ok {
return r
}
r := - 1
for a := t . parent ; a != nil && r < 0 ; a = a . parent {
if _ , ok := p . Pinned ( a ) ; ! ok {
continue
}
if t . parent != a {
t . parent . DrawIn ( s , p )
}
pr , _ := p . Pinned ( t . parent )
r = t . drawUnder ( s , pr )
}
if r < 0 {
r = t . Draw ( s )
}
p . pin ( t , r )
return r
}
// parentRow is the row of t's parent that row r links to.
func ( t * Table [ P ] ) parentRow ( r int ) int { return t . parent . byKey [ t . Cell ( r , t . parentIndex ) ] }
// under reports whether row r of t sits inside row pr of ancestor a.
func ( t * Table [ P ] ) under ( r int , a * Table [ P ] , pr int ) bool {
for c , row := t , r ; c . parent != nil ; c , row = c . parent , c . parentRow ( row ) {
if c . parent == a {
return c . parentRow ( row ) == pr
}
}
return false
}
// find is the row a selector names: by key first, then by name, where a name
// naming several rows resolves inside the ancestors pinned.
func ( t * Table [ P ] ) find ( sel string , pins * Pins [ P ] ) ( int , error ) {
if t . keyIndex < 0 && t . nameIndex < 0 {
return 0 , fmt . Errorf ( "%s has no key or name column to select a row by" , t . path )
}
if r , ok := t . byKey [ sel ] ; ok {
return r , nil
}
rows := t . builtLookup ( ) . byName [ sel ]
if len ( rows ) > 1 {
rows = pins . inside ( t , rows )
}
switch len ( rows ) {
case 1 :
return rows [ 0 ] , nil
case 0 :
return 0 , fmt . Errorf ( "no row of %s has key or name %q" , t . path , sel )
}
return 0 , t . ambiguous ( sel , rows )
}
// ambiguous names each row a selector could have meant: a keyed table by its keys,
// and one told apart only by its parent by the path that selects it.
func ( t * Table [ P ] ) ambiguous ( sel string , rows [ ] int ) error {
spellings := make ( [ ] string , len ( rows ) )
for i , r := range rows {
if t . keyIndex < 0 {
spellings [ i ] = t . selectorSpelling ( r )
} else {
spellings [ i ] = t . Cell ( r , t . keyIndex )
}
}
listed := strings . Join ( spellings , ", " )
if t . keyIndex < 0 {
return fmt . Errorf ( "%q names %d rows of %s; select it inside its %s, one of %s" , sel , len ( rows ) , t . path , t . parent . path , listed )
}
inside := ""
if t . parent != nil {
inside = fmt . Sprintf ( ", or select it inside its %s" , t . parent . path )
}
return fmt . Errorf ( "%q names %d rows of %s; select one by key, one of %s%s" , sel , len ( rows ) , t . path , listed , inside )
}
// selectorSpelling is how a path writes a selected row, for messages: by key, or
// by name inside its parent's row.
func ( t * Table [ P ] ) selectorSpelling ( r int ) string {
switch {
case t . keyIndex >= 0 :
return t . path + "[" + t . Cell ( r , t . keyIndex ) + "]"
case t . nameIndex >= 0 :
return t . parent . selectorSpelling ( t . parentRow ( r ) ) + "." + t . segment + "[" + t . Cell ( r , t . nameIndex ) + "]"
}
return fmt . Sprintf ( "%s line %d" , t . file , r + 2 )
}