diff --git a/proto/tableaupb/wellknown_util.go b/proto/tableaupb/wellknown_util.go index d9602f61..b48df6ed 100644 --- a/proto/tableaupb/wellknown_util.go +++ b/proto/tableaupb/wellknown_util.go @@ -1,5 +1,7 @@ package tableaupb +import "slices" + // NewFraction creates a new fraction. func NewFraction(num, den int32) *Fraction { return &Fraction{Num: num, Den: den} @@ -29,13 +31,18 @@ func NewIntegerComparator(sign Comparator_Sign, num int32) *Comparator { } // Compare returns true if the given fraction matches the given comparator. +// +// Deprecated: Use [Fraction.Match] instead. func Compare(left *Fraction, cmp *Comparator) bool { - return left.Cmp(cmp) + return left.Match(cmp) } -func (f *Fraction) Cmp(cmp *Comparator) bool { +// Match reports whether the fraction satisfies the comparator's +// relational condition (==, !=, <, <=, >, >=). Comparison is done by +// cross-multiplication to stay in exact integer arithmetic for performance +// and accuracy. +func (f *Fraction) Match(cmp *Comparator) bool { other := cmp.GetValue() - // cross-multiply to compare lval := int64(f.GetNum()) * int64(other.GetDen()) rval := int64(other.GetNum()) * int64(f.GetDen()) switch cmp.GetSign() { @@ -52,6 +59,24 @@ func (f *Fraction) Cmp(cmp *Comparator) bool { case Comparator_SIGN_GREATER_OR_EQUAL: return lval >= rval default: - panic("invalid compare operator") + // Unknown sign is treated as no-match; proto enums are open-ended (forward-compat), not a bug. + return false + } +} + +// MatchAny reports whether the fraction matches any of the given comparators (OR logic). +// It returns false if no comparators are provided. +func (f *Fraction) MatchAny(cmps ...*Comparator) bool { + return slices.ContainsFunc(cmps, f.Match) +} + +// MatchAll reports whether the fraction matches all of the given comparators (AND logic). +// It returns true if no comparators are provided (vacuous truth). +func (f *Fraction) MatchAll(cmps ...*Comparator) bool { + for _, cmp := range cmps { + if !f.Match(cmp) { + return false + } } + return true } diff --git a/proto/tableaupb/wellknown_util_test.go b/proto/tableaupb/wellknown_util_test.go index a253c59e..c07f6e53 100644 --- a/proto/tableaupb/wellknown_util_test.go +++ b/proto/tableaupb/wellknown_util_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestCompare(t *testing.T) { +func TestMatch(t *testing.T) { type args struct { left *Fraction cmp *Comparator @@ -71,11 +71,104 @@ func TestCompare(t *testing.T) { }, want: true, }, + { + name: "unknown sign", + args: args{ + left: NewFraction(1, 2), + cmp: NewComparator(Comparator_Sign(42), 1, 2), // unrecognized sign → no match + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.args.left.Match(tt.args.cmp); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMatchAny(t *testing.T) { + left := NewFraction(1, 2) + tests := []struct { + name string + cmps []*Comparator + want bool + }{ + { + name: "empty", + cmps: nil, + want: false, + }, + { + name: "any match (first)", + cmps: []*Comparator{ + NewComparator(Comparator_SIGN_EQUAL, 1, 2), // 1/2 == 1/2 → true + NewComparator(Comparator_SIGN_LESS, 1, 4), // 1/2 < 1/4 → false + }, + want: true, + }, + { + name: "any match (last)", + cmps: []*Comparator{ + NewComparator(Comparator_SIGN_LESS, 1, 4), // 1/2 < 1/4 → false + NewComparator(Comparator_SIGN_GREATER, 1, 4), // 1/2 > 1/4 → true + }, + want: true, + }, + { + name: "no match", + cmps: []*Comparator{ + NewComparator(Comparator_SIGN_LESS, 1, 4), // 1/2 < 1/4 → false + NewComparator(Comparator_SIGN_GREATER_OR_EQUAL, 3, 4), // 1/2 >= 3/4 → false + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := left.MatchAny(tt.cmps...); got != tt.want { + t.Errorf("MatchAny() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMatchAll(t *testing.T) { + left := NewFraction(1, 2) + tests := []struct { + name string + cmps []*Comparator + want bool + }{ + { + name: "empty (vacuous truth)", + cmps: nil, + want: true, + }, + { + name: "all match", + cmps: []*Comparator{ + NewComparator(Comparator_SIGN_EQUAL, 2, 4), // 1/2 == 2/4 → true + NewComparator(Comparator_SIGN_GREATER, 1, 4), // 1/2 > 1/4 → true + NewComparator(Comparator_SIGN_LESS_OR_EQUAL, 3, 4), // 1/2 <= 3/4 → true + }, + want: true, + }, + { + name: "one fails", + cmps: []*Comparator{ + NewComparator(Comparator_SIGN_EQUAL, 2, 4), // 1/2 == 2/4 → true + NewComparator(Comparator_SIGN_LESS, 1, 4), // 1/2 < 1/4 → false + }, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := Compare(tt.args.left, tt.args.cmp); got != tt.want { - t.Errorf("Compare() = %v, want %v", got, tt.want) + if got := left.MatchAll(tt.cmps...); got != tt.want { + t.Errorf("MatchAll() = %v, want %v", got, tt.want) } }) }