blob: 414a30ae1bd8c586781f491bff497e2e840df4ad [file] [log] [blame]
/*
* Copyright 2015 The Kythe Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package compare implements comparisons between Kythe values.
package compare
import (
"bytes"
"fmt"
"strings"
"github.com/golang/protobuf/proto"
"github.com/google/go-cmp/cmp"
spb "kythe.io/kythe/proto/storage_go_proto"
)
// An Option changes the behavior of the generic comparisons.
type Option interface{ isOption() }
// And is an Option that unions other Options.
func And(opt ...Option) Option { return and(opt) }
type and []Option
func (and) isOption() {}
// With is an Option that provides a custom comparison function for the final
// values being compared. Only the last With Option passed to Compare will be
// honored.
type With func(a, b interface{}) Order
func (With) isOption() {}
type reverseOrder struct{}
func (reverseOrder) isOption() {}
// Reversed returns an Option that reverses the resulting Order from a
// comparison.
func Reversed() Option { return reverseOrder{} }
// By is an Option that transforms a value before performing a comparison. It
// should work on both sides of the comparison equivalently.
type By func(interface{}) interface{}
func (By) isOption() {}
// Compare returns the Order between two arbitrary values of the same type.
//
// Only the following types are currently supported: {string, int, int32, []byte}.
//
// Options may be provided to change the semantics of the comparison. Other
// types may be compared if an appropriate By Option transforms the values into
// a supported type or a With Option is provided for the types given.
//
// Note: this function panics if a and b are different types
func Compare(a, b interface{}, opts ...Option) (o Order) {
var reversed bool
defer func() {
if reversed {
o = o.Reverse()
}
}()
var with With
var handleOpt func(opt Option)
handleOpt = func(opt Option) {
switch opt := opt.(type) {
case By:
a = opt(a)
b = opt(b)
case reverseOrder:
reversed = !reversed
case With:
with = opt
case and:
for _, nested := range opt {
handleOpt(nested)
}
default:
panic(fmt.Errorf("unhandled Option type: %T", opt))
}
}
for _, opt := range opts {
handleOpt(opt)
}
if with != nil {
return with(a, b)
}
switch a := a.(type) {
case int:
return Ints(a, b.(int))
case int32:
return Ints(int(a), int(b.(int32)))
case string:
return Strings(a, b.(string))
case []byte:
return Bytes(a, b.([]byte))
default:
panic(fmt.Errorf("unhandled Compare type: %T", a))
}
}
// AndThen returns o if o != EQ. Otherwise, the Order between a and b is
// determined and returned. AndThen can be used to chain comparisons.
//
// Examples:
// Compare(a, b, By(someField)).AndThen(a, b, By(someOtherField))
//
// Entries(e1, e2).AndThen(e1.FactValue, e2.FactValue)
func (o Order) AndThen(a, b interface{}, opts ...Option) Order {
if o != EQ {
return o
}
return Compare(a, b, opts...)
}
// Seq sequences comparisons on the same two values over different
// Options. If len(opts) == 0, Seq merely returns Compare(a, b).
// For len(opts) > 0, Seq returns
// Compare(a, b, opts[0]).AndThen(a, b, opts[1])....AndThen(a, b, opts[len(opts)-1]).
func Seq(a, b interface{}, opts ...Option) Order {
if len(opts) == 0 {
return Compare(a, b)
}
for _, opt := range opts {
// Inlined, short-circuiting AndThen
if o := Compare(a, b, opt); o != EQ {
return o
}
}
return EQ
}
// ToOrder returns LT if c < 0, EQ if c == 0, or GT if c > 0.
func ToOrder(c int) Order {
if c < 0 {
return LT
} else if c > 0 {
return GT
}
return EQ
}
// An Order represents an ordering relationship between values.
type Order int
// LT, EQ, and GT are the standard values for an Order.
const (
LT Order = -1 // lhs < rhs
EQ Order = 0 // lhs == rhs
GT Order = 1 // lhs > rhs
)
// Reverse reverses the Order: LT -> GT, GT -> LT; EQ -> EQ.
func (o Order) Reverse() Order {
return Order(-1 * o)
}
func (o Order) String() string {
switch o {
case LT:
return "LT"
case GT:
return "GT"
case EQ:
return "EQ"
default:
return fmt.Sprintf("Order(%d)", o)
}
}
// Strings returns LT if s < t, EQ if s == t, or GT if s > t.
func Strings(s, t string) Order { return Order(strings.Compare(s, t)) }
// Bytes returns LT if s < t, EQ if s == t, or GT if s > t.
func Bytes(s, t []byte) Order { return Order(bytes.Compare(s, t)) }
// Ints returns LT if a < b, EQ if a == b, or GT if a > b.
func Ints(a, b int) Order { return ToOrder(a - b) }
// Options for comparing components of *spb.VName protobuf messages.
var (
ByVNameSignature = By(func(x interface{}) interface{} {
return x.(*spb.VName).GetSignature()
})
ByVNameCorpus = By(func(x interface{}) interface{} {
return x.(*spb.VName).GetCorpus()
})
ByVNameRoot = By(func(x interface{}) interface{} {
return x.(*spb.VName).GetRoot()
})
ByVNamePath = By(func(x interface{}) interface{} {
return x.(*spb.VName).GetPath()
})
ByVNameLanguage = By(func(x interface{}) interface{} {
return x.(*spb.VName).GetLanguage()
})
)
// VNames returns LT if v1 precedes v2, EQ if v1 and v2 are equal, or GT if v1
// follows v2, in standard order. The ordering for VNames is defined by
// lexicographic comparison of [signature, corpus, root, path, language].
func VNames(v1, v2 *spb.VName) Order {
return Seq(v1, v2,
ByVNameSignature, ByVNameCorpus, ByVNameRoot, ByVNamePath, ByVNameLanguage)
}
// VNamesEqual reports whether v1 and v2 are equal.
func VNamesEqual(v1, v2 *spb.VName) bool { return VNames(v1, v2) == EQ }
// Options for comparing components of *spb.Entry protobuf messages.
var (
ByEntrySource = And(By(func(x interface{}) interface{} {
return x.(*spb.Entry).GetSource()
}), With(func(a, b interface{}) Order { return VNames(a.(*spb.VName), b.(*spb.VName)) }))
ByEntryEdgeKind = By(func(x interface{}) interface{} { return x.(*spb.Entry).GetEdgeKind() })
ByEntryFactName = By(func(x interface{}) interface{} { return x.(*spb.Entry).GetFactName() })
ByEntryTarget = And(By(func(x interface{}) interface{} {
return x.(*spb.Entry).GetTarget()
}), With(func(a, b interface{}) Order { return VNames(a.(*spb.VName), b.(*spb.VName)) }))
)
// Entries reports whether e1 is LT, GT, or EQ to e2 in entry order, ignoring
// fact values (if any).
//
// The ordering for entries is defined by lexicographic comparison of
// [source, edge kind, fact name, target].
func Entries(e1, e2 *spb.Entry) Order {
return Seq(e1, e2,
ByEntrySource, ByEntryEdgeKind, ByEntryFactName, ByEntryTarget)
}
// ByEntries is a min-heap of entries, ordered by Entries.
type ByEntries []*spb.Entry
// Implement the sort.Interface
func (s ByEntries) Len() int { return len(s) }
func (s ByEntries) Less(i, j int) bool { return Entries(s[i], s[j]) == LT }
func (s ByEntries) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Push implements part of the heap.Interface
func (s *ByEntries) Push(v interface{}) { *s = append(*s, v.(*spb.Entry)) }
// Pop implements part of the heap.Interface
func (s *ByEntries) Pop() interface{} {
old := *s
n := len(old) - 1
out := old[n]
*s = old[:n]
return out
}
// ValueEntries reports whether e1 is LT, GT, or EQ to e2 in entry order,
// including fact values (if any).
func ValueEntries(e1, e2 *spb.Entry) Order { return Entries(e1, e2).AndThen(e1.FactValue, e2.FactValue) }
// EntriesEqual reports whether e1 and e2 are equivalent, including their fact
// values (if any).
func EntriesEqual(e1, e2 *spb.Entry) bool { return ValueEntries(e1, e2) == EQ }
// ProtoDiff returns a human-readable report of the differences between two
// values, ensuring that any proto.Message values are compared correctly with
// proto.Equal.
//
// See github.com/google/go-cmp/cmp for more details.
func ProtoDiff(x, y interface{}, opts ...cmp.Option) string {
return cmp.Diff(x, y, makeProtoOpts(opts)...)
}
func makeProtoOpts(opts []cmp.Option) []cmp.Option {
protoOpts := append([]cmp.Option{}, opts...)
protoOpts = append(protoOpts,
cmp.Comparer(proto.Equal),
ignoreProtoXXXFields,
)
return protoOpts
}
var ignoreProtoXXXFields = cmp.FilterPath(func(p cmp.Path) bool {
for _, s := range p {
if strings.HasPrefix(s.String(), ".XXX_") {
return true
}
}
return false
}, cmp.Ignore())