Skip to content

Commit 9dcf853

Browse files
committed
Use Comparer interface a la Java Comparable
Signed-off-by: James Hamlin <jfhamlin@gmail.com>
1 parent e49ab6c commit 9dcf853

8 files changed

Lines changed: 186 additions & 61 deletions

File tree

pkg/lang/apersistentvector.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type (
1313
IPersistentVector
1414
IHashEq
1515
Reversible
16+
Comparer
1617
}
1718

1819
apvSeq struct {

pkg/lang/keyword.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,10 @@ func (k Keyword) ApplyTo(args ISeq) interface{} {
9898
func (k Keyword) Hash() uint32 {
9999
return k.hash
100100
}
101+
102+
func (k Keyword) Compare(other any) int {
103+
if otherKw, ok := other.(Keyword); ok {
104+
return strings.Compare(k.String(), otherKw.String())
105+
}
106+
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Keyword with %T", other)))
107+
}

pkg/lang/mapentry.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package lang
22

3+
import "fmt"
4+
35
// MapEntry represents a key-value pair in a map.
46
type MapEntry struct {
57
hasheq uint32
@@ -118,3 +120,29 @@ func (me *MapEntry) ValAt(key any) any {
118120
func (me *MapEntry) ValAtDefault(key, notFound any) any {
119121
return apersistentVectorValAtDefault(me, key, notFound)
120122
}
123+
124+
func (me *MapEntry) Compare(other any) int {
125+
otherVec, ok := other.(IPersistentVector)
126+
if !ok {
127+
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare MapEntry with %T", other)))
128+
}
129+
130+
myCount := me.Count()
131+
otherCount := otherVec.Count()
132+
133+
// Compare lengths first
134+
if myCount < otherCount {
135+
return -1
136+
} else if myCount > otherCount {
137+
return 1
138+
}
139+
140+
// Compare element by element
141+
for i := 0; i < myCount; i++ {
142+
cmp := Compare(me.Nth(i), otherVec.Nth(i))
143+
if cmp != 0 {
144+
return cmp
145+
}
146+
}
147+
return 0
148+
}

pkg/lang/sort.go

Lines changed: 19 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package lang
33
import (
44
"fmt"
55
"sort"
6+
"strings"
67
)
78

89
// SortSlice performs an in-place stable sort on the given array using the provided comparator.
@@ -28,7 +29,7 @@ func SortSlice(slice []any, comp any) {
2829
if boolResult, ok := result.(bool); ok {
2930
return boolResult
3031
}
31-
32+
3233
// Numeric comparator returns:
3334
// -1 if first arg is less than second
3435
// 0 if args are equal
@@ -47,7 +48,12 @@ func SortSlice(slice []any, comp any) {
4748
// 'less than', 'equal to', or 'greater than' y.
4849
// Handles nil values (nil is less than everything except nil).
4950
func Compare(x, y any) int {
50-
// Handle nil cases first
51+
// Identity check
52+
if x == y {
53+
return 0
54+
}
55+
56+
// Handle nil cases
5157
if IsNil(x) {
5258
if IsNil(y) {
5359
return 0
@@ -59,41 +65,19 @@ func Compare(x, y any) int {
5965
}
6066

6167
// Handle numbers using the Numbers.Compare method
62-
xNum, xIsNum := AsNumber(x)
63-
yNum, yIsNum := AsNumber(y)
64-
if xIsNum && yIsNum {
65-
return Numbers.Compare(xNum, yNum)
68+
if xNum, xIsNum := AsNumber(x); xIsNum {
69+
return Numbers.Compare(xNum, y)
6670
}
6771

68-
// Handle strings
69-
if xStr, xOk := x.(string); xOk {
70-
if yStr, yOk := y.(string); yOk {
71-
if xStr < yStr {
72-
return -1
73-
} else if xStr > yStr {
74-
return 1
75-
}
76-
return 0
77-
}
78-
}
79-
80-
// Handle keywords
81-
if xKw, xOk := x.(Keyword); xOk {
82-
if yKw, yOk := y.(Keyword); yOk {
83-
return Compare(xKw.String(), yKw.String())
84-
}
72+
// Check if x implements Comparer interface
73+
if xComp, ok := x.(Comparer); ok {
74+
return xComp.Compare(y)
8575
}
8676

87-
// Handle symbols
88-
if xSym, xOk := x.(*Symbol); xOk {
89-
if ySym, yOk := y.(*Symbol); yOk {
90-
// Compare namespace first
91-
nsComp := Compare(xSym.Namespace(), ySym.Namespace())
92-
if nsComp != 0 {
93-
return nsComp
94-
}
95-
// Then compare name
96-
return Compare(xSym.Name(), ySym.Name())
77+
// Handle strings (built-in type, doesn't implement Comparer)
78+
if xStr, xOk := x.(string); xOk {
79+
if yStr, yOk := y.(string); yOk {
80+
return strings.Compare(xStr, yStr)
9781
}
9882
}
9983

@@ -109,32 +93,6 @@ func Compare(x, y any) int {
10993
}
11094
}
11195

112-
// Handle vectors (including MapEntry which is a vector)
113-
if xVec, xOk := x.(IPersistentVector); xOk {
114-
if yVec, yOk := y.(IPersistentVector); yOk {
115-
xCount := xVec.Count()
116-
yCount := yVec.Count()
117-
minCount := xCount
118-
if yCount < minCount {
119-
minCount = yCount
120-
}
121-
// Compare element by element
122-
for i := 0; i < minCount; i++ {
123-
cmp := Compare(xVec.Nth(i), yVec.Nth(i))
124-
if cmp != 0 {
125-
return cmp
126-
}
127-
}
128-
// If all compared elements are equal, shorter vector is less
129-
if xCount < yCount {
130-
return -1
131-
} else if xCount > yCount {
132-
return 1
133-
}
134-
return 0
135-
}
136-
}
137-
138-
// If we can't compare, panic with an error
139-
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare %T with %T", x, y)))
96+
// Default error - cannot compare
97+
panic(NewIllegalArgumentError(fmt.Sprintf("%T cannot be cast to Comparable", x)))
14098
}

pkg/lang/subvector.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,29 @@ func (v *SubVector) Invoke(args ...any) any {
165165
func (v *SubVector) HashEq() uint32 {
166166
return apersistentVectorHashEq(&v.hasheq, v)
167167
}
168+
169+
func (v *SubVector) Compare(other any) int {
170+
otherVec, ok := other.(IPersistentVector)
171+
if !ok {
172+
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare SubVector with %T", other)))
173+
}
174+
175+
myCount := v.Count()
176+
otherCount := otherVec.Count()
177+
178+
// Compare lengths first
179+
if myCount < otherCount {
180+
return -1
181+
} else if myCount > otherCount {
182+
return 1
183+
}
184+
185+
// Compare element by element
186+
for i := 0; i < myCount; i++ {
187+
cmp := Compare(v.Nth(i), otherVec.Nth(i))
188+
if cmp != 0 {
189+
return cmp
190+
}
191+
}
192+
return 0
193+
}

pkg/lang/symbol.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lang
22

33
import (
4+
"fmt"
45
"strings"
56
)
67

@@ -48,6 +49,29 @@ func (s *Symbol) Name() string {
4849
return s.name
4950
}
5051

52+
func (s *Symbol) Compare(other any) int {
53+
otherSym, ok := other.(*Symbol)
54+
if !ok {
55+
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Symbol with %T", other)))
56+
}
57+
58+
// Compare namespace first
59+
if s.ns != otherSym.ns {
60+
if s.ns == "" && otherSym.ns != "" {
61+
return -1
62+
}
63+
if s.ns != "" && otherSym.ns == "" {
64+
return 1
65+
}
66+
if nsComp := strings.Compare(s.ns, otherSym.ns); nsComp != 0 {
67+
return nsComp
68+
}
69+
}
70+
71+
// Then compare name
72+
return strings.Compare(s.name, otherSym.name)
73+
}
74+
5175
func (s *Symbol) FullName() string {
5276
return s.String()
5377
}

pkg/lang/vector.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,32 @@ func (v *Vector) AsTransient() ITransientCollection {
272272
}
273273
}
274274

275+
func (v *Vector) Compare(other any) int {
276+
otherVec, ok := other.(IPersistentVector)
277+
if !ok {
278+
panic(NewIllegalArgumentError(fmt.Sprintf("Cannot compare Vector with %T", other)))
279+
}
280+
281+
myCount := v.Count()
282+
otherCount := otherVec.Count()
283+
284+
// Compare lengths first
285+
if myCount < otherCount {
286+
return -1
287+
} else if myCount > otherCount {
288+
return 1
289+
}
290+
291+
// Compare element by element
292+
for i := 0; i < myCount; i++ {
293+
cmp := Compare(v.Nth(i), otherVec.Nth(i))
294+
if cmp != 0 {
295+
return cmp
296+
}
297+
}
298+
return 0
299+
}
300+
275301
func toSlice(x any) []any {
276302
if x == nil {
277303
return nil

test/glojure/test_glojure/sort.glj

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,60 @@
197197
;; Should be sorted by key
198198
(is (= [[:a 1] [:b 2] [:c 3]] sorted)))))
199199

200+
(deftest test-non-comparable-types
201+
(testing "Non-comparable types throw errors"
202+
;; Lists are not comparable
203+
(is (thrown? go/any (compare '(1 2) '(1 2))))
204+
(is (thrown? go/any (sort ['(1 2) '(3 4)])))
205+
206+
;; Maps are not comparable
207+
(is (thrown? go/any (compare {:a 1} {:b 2})))
208+
(is (thrown? go/any (sort [{:a 1} {:b 2}])))
209+
210+
;; Sets are not comparable
211+
(is (thrown? go/any (compare #{1 2} #{3 4})))
212+
(is (thrown? go/any (sort [#{1 2} #{3 4}])))
213+
214+
;; Mixed incompatible types
215+
(is (thrown? go/any (compare 1 :a)))
216+
(is (thrown? go/any (compare "string" 'symbol)))
217+
(is (thrown? go/any (compare :keyword [1 2 3])))))
218+
219+
(deftest test-vector-comparison
220+
(testing "Vector comparison details"
221+
;; Vectors compare lexicographically
222+
(is (= -1 (compare [1 2] [1 3])))
223+
(is (= 1 (compare [1 3] [1 2])))
224+
(is (= 0 (compare [1 2 3] [1 2 3])))
225+
226+
;; Shorter vectors are less than longer vectors with same prefix
227+
(is (= -1 (compare [1 2] [1 2 3])))
228+
(is (= 1 (compare [1 2 3] [1 2])))
229+
230+
;; Nested vectors
231+
(is (= -1 (compare [[1 2] [3 4]] [[1 2] [3 5]])))
232+
(is (= 0 (compare [[1 2] [3 4]] [[1 2] [3 4]])))
233+
234+
;; SubVectors behave like vectors
235+
(let [v [1 2 3 4 5]
236+
sv1 (subvec v 1 3) ; [2 3]
237+
sv2 (subvec v 2 4)] ; [3 4]
238+
(is (= -1 (compare sv1 sv2)))
239+
(is (= -1 (compare sv1 [3 4]))))))
240+
241+
(deftest test-symbol-namespace-comparison
242+
(testing "Symbols compare namespace-first"
243+
;; No namespace < with namespace
244+
(is (= -1 (compare 'x 'a/x)))
245+
(is (= 1 (compare 'a/x 'x)))
246+
247+
;; Different namespaces
248+
(is (= -1 (compare 'a/x 'b/x)))
249+
(is (= 1 (compare 'b/x 'a/x)))
250+
251+
;; Same namespace, different names
252+
(is (= -1 (compare 'ns/a 'ns/b)))
253+
(is (= 1 (compare 'ns/b 'ns/a)))))
254+
200255
;; Run tests
201256
(run-tests)

0 commit comments

Comments
 (0)