Reuse canonical item occurrence evidence
This commit is contained in:
@@ -93,6 +93,14 @@ func ValidSourceRefs(index source.DocumentIndex, refs []source.SourceRef) bool {
|
||||
// comparable through SourceRefOrder's literal fallback so malformed candidates
|
||||
// are still safe to sort and diagnose.
|
||||
func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||
left.SourceRefs = order.Canonicalize(left.SourceRefs)
|
||||
right.SourceRefs = order.Canonicalize(right.SourceRefs)
|
||||
return CanonicalLess(order, left, right)
|
||||
}
|
||||
|
||||
// CanonicalLess defines the canonical occurrence order without modifying or
|
||||
// canonicalizing source references. Callers must provide canonical evidence.
|
||||
func CanonicalLess(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||
leftPosition, leftHasEvidence := order.EarliestValid(left.SourceRefs)
|
||||
rightPosition, rightHasEvidence := order.EarliestValid(right.SourceRefs)
|
||||
if leftHasEvidence != rightHasEvidence {
|
||||
@@ -122,12 +130,20 @@ func Less(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||
if less, decided := optionalQuantityLess(left.Quantity, right.Quantity); decided {
|
||||
return less
|
||||
}
|
||||
return sourceRefsLess(order, order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs))
|
||||
return sourceRefsLess(order, left.SourceRefs, right.SourceRefs)
|
||||
}
|
||||
|
||||
// ExactEqual reports whether occurrences are exact duplicates after their display
|
||||
// fields and evidence have been canonicalized for the supplied source order.
|
||||
func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) bool {
|
||||
left.SourceRefs = order.Canonicalize(left.SourceRefs)
|
||||
right.SourceRefs = order.Canonicalize(right.SourceRefs)
|
||||
return CanonicalExactEqual(left, right)
|
||||
}
|
||||
|
||||
// CanonicalExactEqual reports whether canonical occurrences are exact
|
||||
// duplicates without modifying or canonicalizing their source references.
|
||||
func CanonicalExactEqual(left, right dnd.ItemOccurrence) bool {
|
||||
if left.ItemID != right.ItemID || DisplayValue(left.Name) != DisplayValue(right.Name) || left.Kind != right.Kind ||
|
||||
DisplayValue(left.From) != DisplayValue(right.From) || DisplayValue(left.To) != DisplayValue(right.To) ||
|
||||
(left.Quantity == nil) != (right.Quantity == nil) {
|
||||
@@ -136,13 +152,21 @@ func ExactEqual(order shared.SourceRefOrder, left, right dnd.ItemOccurrence) boo
|
||||
if left.Quantity != nil && *left.Quantity != *right.Quantity {
|
||||
return false
|
||||
}
|
||||
return SourceRefsEqual(order.Canonicalize(left.SourceRefs), order.Canonicalize(right.SourceRefs))
|
||||
return SourceRefsEqual(left.SourceRefs, right.SourceRefs)
|
||||
}
|
||||
|
||||
// ExactIdentity returns a collision-safe duplicate key after display and
|
||||
// evidence canonicalization. It is intended for callers that have already
|
||||
// decided the occurrence is eligible for duplicate handling.
|
||||
func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) string {
|
||||
occurrence.SourceRefs = order.Canonicalize(occurrence.SourceRefs)
|
||||
return CanonicalExactIdentity(occurrence)
|
||||
}
|
||||
|
||||
// CanonicalExactIdentity returns a collision-safe duplicate key without
|
||||
// modifying or canonicalizing source references. Callers must provide
|
||||
// canonical evidence.
|
||||
func CanonicalExactIdentity(occurrence dnd.ItemOccurrence) string {
|
||||
var key strings.Builder
|
||||
writeKeyString(&key, occurrence.ItemID)
|
||||
writeKeyString(&key, DisplayValue(occurrence.Name))
|
||||
@@ -155,14 +179,24 @@ func ExactIdentity(order shared.SourceRefOrder, occurrence dnd.ItemOccurrence) s
|
||||
key.WriteByte('1')
|
||||
writeKeyInt(&key, *occurrence.Quantity)
|
||||
}
|
||||
for _, ref := range order.Canonicalize(occurrence.SourceRefs) {
|
||||
writeKeyString(&key, ref.SourceID)
|
||||
writeKeyInt(&key, ref.StartUnitID)
|
||||
writeKeyInt(&key, ref.EndUnitID)
|
||||
}
|
||||
writeKeySourceRefs(&key, occurrence.SourceRefs)
|
||||
return key.String()
|
||||
}
|
||||
|
||||
func writeKeySourceRefs(builder *strings.Builder, refs []source.SourceRef) {
|
||||
if refs == nil {
|
||||
builder.WriteByte('0')
|
||||
return
|
||||
}
|
||||
builder.WriteByte('1')
|
||||
writeKeyInt(builder, len(refs))
|
||||
for _, ref := range refs {
|
||||
writeKeyString(builder, ref.SourceID)
|
||||
writeKeyInt(builder, ref.StartUnitID)
|
||||
writeKeyInt(builder, ref.EndUnitID)
|
||||
}
|
||||
}
|
||||
|
||||
func optionalStringLess(left, right string) (bool, bool) {
|
||||
leftPresent, rightPresent := HolderPresent(left), HolderPresent(right)
|
||||
if leftPresent != rightPresent {
|
||||
|
||||
@@ -108,6 +108,14 @@ func TestLessAndExactEqualityCanonicalizeEvidence(t *testing.T) {
|
||||
if ExactIdentity(order, first) != ExactIdentity(order, second) {
|
||||
t.Fatal("ExactIdentity() differs for canonical duplicates")
|
||||
}
|
||||
canonicalFirst := first
|
||||
canonicalFirst.SourceRefs = order.Canonicalize(first.SourceRefs)
|
||||
if !CanonicalExactEqual(canonicalFirst, second) || CanonicalExactIdentity(canonicalFirst) != CanonicalExactIdentity(second) {
|
||||
t.Fatal("canonical helpers did not recognize canonical duplicates")
|
||||
}
|
||||
if CanonicalExactEqual(first, second) || CanonicalExactIdentity(first) == CanonicalExactIdentity(second) {
|
||||
t.Fatal("canonical helpers repaired noncanonical source references")
|
||||
}
|
||||
|
||||
quantity := 1
|
||||
differentQuantity := second
|
||||
@@ -132,6 +140,12 @@ func TestSourceReferenceHelpers(t *testing.T) {
|
||||
if ValidSourceRefs(source.NewDocumentIndex(doc), []source.SourceRef{{SourceID: "other", StartUnitID: 10, EndUnitID: 20}}) {
|
||||
t.Fatal("ValidSourceRefs() = true, want invalid source identifier rejection")
|
||||
}
|
||||
nilEvidence := dnd.ItemOccurrence{ItemID: "item"}
|
||||
emptyEvidence := nilEvidence
|
||||
emptyEvidence.SourceRefs = []source.SourceRef{}
|
||||
if CanonicalExactIdentity(nilEvidence) == CanonicalExactIdentity(emptyEvidence) {
|
||||
t.Fatal("CanonicalExactIdentity() conflated nil and empty source references")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLessSortsMalformedReferencesDeterministically(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user