diff --git a/CHANGELOG.md b/CHANGELOG.md index de8b1c8..5556b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,9 @@ replacement, and each removed path, column or flag. `parent`, a name repeating inside one parent row, a name spelling another row's key, a table named like a column of any table above it, a table whose format or cell references its own family, and, within one render and draw group, a path drawing a table another path selects a - row of, or two paths pinning different rows of one table. + row of, or two paths pinning different rows of one table. Two reads are held to one + draw only where a render can pin both their rows: a path read pins a row's ancestors + with it, while a table read whole draws a row and pins none. - `New` refuses a root choice of templates sharing one format and one set of string fields, naming the rows TSV to write instead. - `misc.territory`, `misc.currency`, `misc.language`, `misc.httpstatus` and diff --git a/README.md b/README.md index feddb0d..cb29fa1 100644 --- a/README.md +++ b/README.md @@ -1140,8 +1140,9 @@ the Development section below, and who ships a register the four above then draw - **The rows of a table are alternatives.** Only one row renders, so a cell in one row and a cell in another never meet, and each may select its own row of another table; the cells of one row, and whatever they reach, do meet, and so does the - format beside them. The rule the fence applies is that two reads meet unless - they sit in two rows of one table, or in a row outside a selected ancestor's. A + format beside them. The rule the fence applies is that two reads meet unless they + sit in two rows of one table, or in rows no render pins at once: a path read pins a + row's ancestors with it, while a table read whole draws a row and pins none. A choice's items get no such treatment yet: two items selecting different rows are still refused. - **A table never reaches its own family, by any route.** A `repeat` iteration and a diff --git a/draw.go b/draw.go index 21d0d55..8f4a893 100644 --- a/draw.go +++ b/draw.go @@ -291,18 +291,20 @@ type drawWalk struct { seen map[drawVisit]bool } -// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and -// the table rows it entered. +// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, the +// table rows it entered, and the table a whole read draws a row of. type drawAt struct { group string route drawRoute alt rowSet + drawn *table } // rowSet is the rows a walk entered, one per table: only one row of a table renders, so reads in // two rows of one table never meet, while reads in one row, across its columns and whatever they -// reach, do. It simulates what the render pins: enter is (*draws).pin and rowOf is its pinned, and -// TestRowSetSimulatesPinning holds the two to one answer. +// reach, do. It stands in for the rows the render pins, so a row a path read pins enters with the +// ancestors (*draws).pin pins alongside it, while a table read whole is drawn a row of and pins +// none. type rowSet []tablePin func (s rowSet) rowOf(t *table) (int, bool) { @@ -314,24 +316,24 @@ func (s rowSet) rowOf(t *table) (int, bool) { return 0, false } -// enter is s with row r of t and the ancestor rows it links to, where t is not in it yet; the set -// is copied, since walks branch. -func (s rowSet) enter(t *table, r int) rowSet { +// enter is s with row r of t, where t is not in it yet, and, where pins says the render pins that +// row rather than draws it, the ancestor rows it links to. The set is copied, since walks branch. +func (s rowSet) enter(t *table, r int, pins bool) rowSet { if _, in := s.rowOf(t); in { return s } out := append(make(rowSet, 0, len(s)+1), s...) for { - if _, in := out.rowOf(t); in { - break - } out = append(out, tablePin{t, r}) - if t.parentT == nil { + if !pins || t.parentT == nil { break } t, r = t.parentT, t.parentRow(r) + if _, in := out.rowOf(t); in { + break + } } - sort.Slice(out, func(i, j int) bool { return out[i].t.category < out[j].t.category }) + sort.Slice(out, func(i, j int) bool { return out[i].t.path < out[j].t.path }) return out } @@ -394,12 +396,15 @@ func (w *drawWalk) walk(n node, at drawAt) { if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { at.group = t.drawGroupKey } + if t, isTable := n.(*table); isTable { + at.drawn = t + } for _, e := range renderEdges(n) { if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil { if at.alt.excludes(cell) { continue } - w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow)}) + w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn), at.drawn}) continue } w.edge(n, e, at) @@ -430,7 +435,7 @@ func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { w.reads = append(w.reads, pathRead{at, a, tr}) } if tr != nil { - tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r) }) + tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r, true) }) } } w.walk(e.to, at) diff --git a/table_test.go b/table_test.go index 4ffbce7..feb939a 100644 --- a/table_test.go +++ b/table_test.go @@ -964,9 +964,9 @@ func TestAmbiguousNameNamesARunnablePath(t *testing.T) { } } -// TestRowSetSimulatesPinning proves the load-time walk's rowSet answers what -// (*draws).pin and pinned answer at render time: the rows one entry brings with it, -// the cells that entry rules out, and which two entries are alternatives. +// TestRowSetSimulatesPinning proves the walk's row set answers what the render's pinning +// answers: a path read pins a row and the ancestors it links to, a whole read draws a row +// and pins none. func TestRowSetSimulatesPinning(t *testing.T) { f := newGenerator(t, writeFiles(t, geo()), WithSeed(1)) var tables []*table @@ -979,54 +979,61 @@ func TestRowSetSimulatesPinning(t *testing.T) { } for _, entered := range tables { for row := 0; row < entered.rows(); row++ { - s := rowSet(nil).enter(entered, row) + if drawn := rowSet(nil).enter(entered, row, false); len(drawn) != 1 || drawn[0] != (tablePin{entered, row}) { + t.Errorf("a drawn %s enters %v, want its own row alone", entered.selectorSpelling(row), drawn) + } + s := rowSet(nil).enter(entered, row, true) var pinned draws + held := 0 pinned.pin(entered, row) - assertRowSetAgrees(t, s, pinned, entered, row, tables) - } - } -} - -// assertRowSetAgrees compares the set entered with row of entered against the same -// row pinned, over every row of tables. -func assertRowSetAgrees(t *testing.T, s rowSet, pinned draws, entered *table, row int, tables []*table) { - t.Helper() - pinned.each(func(tbl *table, r int) { - got, in := s.rowOf(tbl) - if !in || got != r { - t.Errorf("entering %s leaves %s unset in the row set, though pinning it pins %s", entered.selectorSpelling(row), tbl.path, tbl.selectorSpelling(r)) - } - }) - for _, cand := range tables { - for cr := 0; cr < cand.rows(); cr++ { - var beside draws - beside.pin(entered, row) - refused := beside.pinRow(cand, cr) != nil - if got := s.excludes(&template{cellOf: cand, cellRow: cr}); got != refused { - t.Errorf("excludes(%s) = %v beside %s, but pinRow refuses it = %v", cand.selectorSpelling(cr), got, entered.selectorSpelling(row), refused) + pinned.each(func(tbl *table, r int) { + held++ + if got, in := s.rowOf(tbl); !in || got != r { + t.Errorf("entering %s leaves %s unset, though pinning it pins %s", entered.selectorSpelling(row), tbl.path, tbl.selectorSpelling(r)) + } + }) + if held != len(s) { + t.Errorf("entering %s holds %d rows, though pinning it pins %d", entered.selectorSpelling(row), len(s), held) } - a := drawAt{alt: s} - b := drawAt{alt: rowSet(nil).enter(cand, cr)} - if got := alternatives(a, b); got != refused { - t.Errorf("alternatives(%s, %s) = %v, but pinning both refuses = %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), got, refused) + for _, cand := range tables { + for cr := 0; cr < cand.rows(); cr++ { + var beside draws + beside.pin(entered, row) + refused := beside.pinRow(cand, cr) != nil + if got := s.excludes(&template{cellOf: cand, cellRow: cr}); got != refused { + t.Errorf("excludes(%s) = %v beside %s, but pinRow refuses it = %v", cand.selectorSpelling(cr), got, entered.selectorSpelling(row), refused) + } + if got := alternatives(drawAt{alt: s}, drawAt{alt: rowSet(nil).enter(cand, cr, true)}); got != refused { + t.Errorf("alternatives(%s, %s) = %v, but pinning both refuses = %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), got, refused) + } + } } } } } -// TestCellsOfRowsThatNeverMeetAreNotCompared pins what the entered ancestors buy: a -// cell of region 01 and a cell of a municipality of region 12 never render together, -// so the reads inside them are not one render's to reconcile. -func TestCellsOfRowsThatNeverMeetAreNotCompared(t *testing.T) { - files := with(geo(), map[string]string{ - "addr.json": `{"format":"{city}","city":"Lund"}`, +// TestCellReadsMeetWhereTheRenderPairsTheRows pins which cells' reads are one render's to +// reconcile: a path read pins the row's ancestors, so a cell of region 01 and a cell of a +// municipality of region 12 never render together, while a whole read draws its row and pins +// no ancestor, so those same two cells do. +func TestCellReadsMeetWhereTheRenderPairsTheRows(t *testing.T) { + cells := with(geo(), map[string]string{ + "addr.json": `{"format":"{city}","city":["Lund","Malmö"]}`, "municipality.tsv": "code\tname\tregion\tpopulation\tnote\n0180\tStockholm\t01\t980000\t-\n0184\tSolna\t01\t85000\t-\n1280\tMalmö\t12\t360000\t{/addr.city}\n1281\tLund\t12\t130000\t-\n1480\tGöteborg\t14\t590000\t-\n", "region.tsv": "code\tname\tpopulation\tnote\n01\tStockholms län\t2400000\t{/addr}\n12\tSkåne län\t1400000\t-\n14\tVästra Götalands län\t1750000\t-\n", - "x.json": `"{/region.note} {/municipality.note}"`, }) - f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files)), WithSeed(1)) + paths := with(cells, map[string]string{"x.json": `"{/region.note} {/municipality.note}"`}) + f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, paths)), WithSeed(1)) if err != nil { t.Fatalf("New = %v, want the two cells accepted", err) } fake(t, f, "x") + whole := with(cells, map[string]string{ + "municipality.json": `{"format":"{name}={note}","rows":"municipality.tsv","key":"code","name":"name","parent":"region","weight":"population"}`, + "region.json": `{"format":"{name}={note}","rows":"region.tsv","key":"code","name":"name","weight":"population"}`, + "x.json": `"{/region}|{/municipality}"`, + }) + if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, whole)), WithSeed(1)); err == nil || !strings.Contains(err.Error(), "reads a path into") { + t.Fatalf("New = %v, want the two drawn rows held to one draw of addr", err) + } }