diff --git a/bound_test.go b/bound_test.go index b1f3054..1d53851 100644 --- a/bound_test.go +++ b/bound_test.go @@ -176,6 +176,32 @@ func TestDottedTokenErrors(t *testing.T) { } } +func TestSamePathReadTwiceReadsOneValue(t *testing.T) { + // A path is drawn once per expansion, so reading it twice reads one value — + // what a name shown in a display form and again in an address needs. + f := engine(7) + for i := 0; i < 300; i++ { + got := mustRender(t, f, `{"format":"{p.first}|{p.first}", + "p":[{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}]}`) + parts := strings.Split(got, "|") + if parts[0] != parts[1] { + t.Fatalf("draw %d = %q, want one value read twice", i, got) + } + } +} + +func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) { + // Two tails of one head stay in the same variant, each keeping its own value. + f := engine(8) + for i := 0; i < 300; i++ { + got := mustRender(t, f, `{"format":"{p.a}{p.b}", + "p":[{"format":"x","a":"A","b":"1"},{"format":"y","a":"B","b":"2"}]}`) + if got != "A1" && got != "B2" { + t.Fatalf("draw %d = %q, want a row's own pair", i, got) + } + } +} + func TestBoundPathIsReachableByFake(t *testing.T) { // Binding changes how a format reads a sibling, not what List and Fake offer: // the sub-fields stay addressable on their own. diff --git a/render.go b/render.go index 841ea2f..8e89665 100644 --- a/render.go +++ b/render.go @@ -139,9 +139,12 @@ func expand(s *session, t *template) string { b.Grow(t.grow) // One draw per bound head, held for this expansion only: a nested template and // each repeat iteration get their own, since each is its own expansion. - var bound map[string]node + var bound *draws if len(t.bound) > 0 { - bound = make(map[string]node, len(t.bound)) + bound = &draws{ + variant: make(map[string]node, len(t.bound)), + value: make(map[string]string, len(t.bound)), + } } for i := range t.ops { o := &t.ops[i] @@ -159,30 +162,42 @@ func expand(s *session, t *template) string { return b.String() } +// draws is what an expansion has already drawn for its bound heads: the variant +// each head was drawn as, so every path under it reads one row, and the value each +// path read, so the same path read twice reads one value. +type draws struct { + variant map[string]node + value map[string]string +} + // resolve renders one field alternation: the '|' alternatives, one picked at // random. An arm's key is a sibling field or a {..path} reference, which linkRefs -// bound into fields too. A head the format addresses by dotted path is drawn once -// and held in bound, so {place.postal-code} and {place.locality} read one row; -// the arm's tail then walks into that draw. checkTokens, checkPath and linkRefs -// prove every step, so this cannot fail. -func resolve(s *session, arms []arm, t *template, bound map[string]node) string { +// bound into fields too. A field the format addresses by dotted path is bound: it +// is drawn once for the expansion, so {place.postal-code} and {place.locality} +// read one row and either read twice gives one value. checkTokens, checkPath and +// linkRefs prove every step, so this cannot fail. +func resolve(s *session, arms []arm, t *template, bound *draws) string { a := arms[s.IntN(len(arms))] - n := t.fields[a.key] - if t.bound[a.key] { - held, drew := bound[a.key] - if !drew { - held = drawn(s, n) - bound[a.key] = held - } - n = held + if !t.bound[a.key] { + return render(s, t.fields[a.key]) + } + if v, read := bound.value[a.name]; read { + return v + } + n, drew := bound.variant[a.key] + if !drew { + n = drawn(s, t.fields[a.key]) + bound.variant[a.key] = n } if len(a.tail) > 0 { var err error if n, err = descend(s, n, a.tail); err != nil { - panic(fmt.Sprintf("fakes: %s: %v", strings.Join(append([]string{a.key}, a.tail...), "."), err)) + panic(fmt.Sprintf("fakes: %s: %v", a.name, err)) } } - return render(s, n) + v := render(s, n) + bound.value[a.name] = v + return v } // drawn resolves a choice to one variant, so a bound head is a concrete node the diff --git a/template.go b/template.go index 7aafd23..2a8d749 100644 --- a/template.go +++ b/template.go @@ -177,6 +177,7 @@ func fieldTokens(format string) []string { // bound under) and the tail of a dotted path into it. A non-empty tail is what makes // the arm a bound draw: its head is drawn once per expansion (see boundHeads). type arm struct { + name string // as written, and the key a bound draw's value is held under key string tail []string } @@ -185,13 +186,13 @@ type arm struct { // dots — linkRefs binds it whole — so only a sibling name reads as a path. func splitArm(name string) arm { if isRef(name) { - return arm{key: name} + return arm{name: name, key: name} } head, tail, dotted := strings.Cut(name, ".") if !dotted { - return arm{key: name} + return arm{name: name, key: name} } - return arm{key: head, tail: strings.Split(tail, ".")} + return arm{name: name, key: head, tail: strings.Split(tail, ".")} } // splitArms splits a token body's '|' alternatives.