1
- import BTree , { IMap , EmptyBTree } from './b+tree' ;
1
+ import BTree , { IMap , EmptyBTree , defaultComparator , compareFiniteNumbers , compareFiniteNumbersOrStringOrArray , compareStrings } from './b+tree' ;
2
2
import SortedArray from './sorted-array' ;
3
3
import MersenneTwister from 'mersenne-twister' ;
4
4
@@ -14,6 +14,153 @@ function addToBoth<K,V>(a: IMap<K,V>, b: IMap<K,V>, k: K, v: V) {
14
14
expect ( a . set ( k , v ) ) . toEqual ( b . set ( k , v ) ) ;
15
15
}
16
16
17
+ describe ( 'defaultComparator' , ( ) =>
18
+ {
19
+ const dateA = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 5 ) ) ;
20
+ const dateA2 = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 5 ) ) ;
21
+ const dateB = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 6 ) ) ;
22
+ const values = [
23
+ dateA ,
24
+ dateA2 ,
25
+ dateB ,
26
+ dateA . valueOf ( ) ,
27
+ '24x' ,
28
+ '0' ,
29
+ '1' ,
30
+ '3' ,
31
+ 'String' ,
32
+ '10' ,
33
+ 0 ,
34
+ "NaN" ,
35
+ NaN ,
36
+ Infinity ,
37
+ - 0 ,
38
+ - Infinity ,
39
+ 1 ,
40
+ 10 ,
41
+ 2 ,
42
+ [ ] ,
43
+ '[]' ,
44
+ [ 1 ] ,
45
+ [ '1' ]
46
+ ] ;
47
+ const sorted = [ - Infinity , - 10 , - 1 , - 0 , 0 , 1 , 2 , 10 , Infinity ] ;
48
+ testComparison ( defaultComparator , sorted , values , [ [ dateA , dateA2 ] , [ 0 , - 0 ] , [ [ 1 ] , [ '1' ] ] ] ) ;
49
+ } ) ;
50
+
51
+ describe ( 'compareFiniteNumbers' , ( ) =>
52
+ {
53
+ const sorted = [ - 10 , - 1 , - 0 , 0 , 1 , 2 , 10 ] ;
54
+ testComparison ( compareFiniteNumbers , sorted , sorted , [ [ - 0 , 0 ] ] ) ;
55
+ } ) ;
56
+
57
+ describe ( 'compareStrings' , ( ) =>
58
+ {
59
+ const values = [
60
+ '24x' ,
61
+ '+0' ,
62
+ '0.0' ,
63
+ '0' ,
64
+ '-0' ,
65
+ '1' ,
66
+ '3' ,
67
+ 'String' ,
68
+ '10' ,
69
+ "NaN" ,
70
+ ] ; ;
71
+ testComparison ( compareStrings , [ ] , values , [ ] ) ;
72
+ } ) ;
73
+
74
+ describe ( 'compareFiniteNumbersOrStringOrArray' , ( ) =>
75
+ {
76
+ const values = [
77
+ '24x' ,
78
+ '0' ,
79
+ '1' ,
80
+ '3' ,
81
+ 'String' ,
82
+ '10' ,
83
+ 0 ,
84
+ "NaN" ,
85
+ - 0 ,
86
+ 1 ,
87
+ 10 ,
88
+ 2 ,
89
+ [ ] ,
90
+ '[]' ,
91
+ [ 1 ] ,
92
+ [ '1' ]
93
+ ] ;
94
+ const sorted = [ - 10 , - 1 , - 0 , 0 , 1 , 2 , 10 ] ;
95
+ testComparison ( compareFiniteNumbersOrStringOrArray , sorted , values , [ [ 0 , - 0 ] , [ [ 1 ] , [ '1' ] ] ] ) ;
96
+ } ) ;
97
+
98
+
99
+ /**
100
+ * Tests a comparison function, ensuring it produces a strict partial order over the provided values.
101
+ * Additionally confirms that the comparison function has the correct definition of equality via expectedDuplicates.
102
+ */
103
+ function testComparison ( comparison : ( a : any , b : any ) => number , inOrder : any [ ] , values : any [ ] , expectedDuplicates : [ any , any ] [ ] = [ ] ) {
104
+ function check ( a : any , b : any ) : number {
105
+ const v = comparison ( a , b ) ;
106
+ expect ( typeof v ) . toEqual ( 'number' ) ;
107
+ expect ( v === v ) . toEqual ( true ) ; // Not NaN
108
+ return Math . sign ( v ) ;
109
+ }
110
+
111
+ test ( 'comparison has correct order' , ( ) => {
112
+ expect ( [ ...inOrder ] . sort ( comparison ) ) . toMatchObject ( inOrder ) ;
113
+ } ) ;
114
+
115
+ test ( 'comparison deffierantes values' , ( ) => {
116
+ let duplicates = [ ] ;
117
+ for ( let i = 0 ; i < values . length ; i ++ ) {
118
+ for ( let j = i + 1 ; j < values . length ; j ++ ) {
119
+ if ( check ( values [ i ] , values [ j ] ) === 0 ) {
120
+ duplicates . push ( [ values [ i ] , values [ j ] ] ) ;
121
+ }
122
+ }
123
+ }
124
+ expect ( duplicates ) . toMatchObject ( expectedDuplicates ) ;
125
+ } ) ;
126
+
127
+ test ( 'comparison forms a strict partial ordering' , ( ) => {
128
+ // To be a strict partial order, the function must be:
129
+ // irreflexive: not a < a
130
+ // transitive: if a < b and b < c then a < c
131
+ // asymmetric: if a < b then not b < a
132
+
133
+ // Since our comparison has three outputs, we adjust that to, we need to tighten the rules that involve 'not a < b' (where we have two possible outputs) as follows:
134
+ // irreflexive: compare(a, a) === 0
135
+ // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0
136
+ // asymmetric: sign(compare(a, b)) === -sign(compare(b, a))
137
+
138
+ // This can is brute forced in O(n^3) time below:
139
+ // Violations
140
+ const irreflexive = [ ]
141
+ const transitive = [ ]
142
+ const asymmetric = [ ]
143
+ for ( const a of values ) {
144
+ // irreflexive: compare(a, a) === 0
145
+ if ( check ( a , a ) !== 0 ) irreflexive . push ( a ) ;
146
+ for ( const b of values ) {
147
+ for ( const c of values ) {
148
+ // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0
149
+ if ( check ( a , b ) < 0 && check ( b , c ) < 0 ) {
150
+ if ( check ( a , c ) !== - 1 ) transitive . push ( [ a , b , c ] ) ;
151
+ }
152
+ }
153
+ // sign(compare(a, b)) === -sign(compare(b, a))
154
+ if ( check ( a , b ) !== - check ( b , a ) ) asymmetric . push ( [ a , b ] ) ;
155
+ }
156
+ }
157
+ expect ( irreflexive ) . toEqual ( [ ] ) ;
158
+ expect ( transitive ) . toEqual ( [ ] ) ;
159
+ expect ( asymmetric ) . toEqual ( [ ] ) ;
160
+ } ) ;
161
+ }
162
+
163
+
17
164
describe ( 'Simple tests on leaf nodes' , ( ) =>
18
165
{
19
166
test ( 'A few insertions (fanout 8)' , insert8 . bind ( null , 8 ) ) ;
@@ -427,4 +574,33 @@ function testBTree(maxNodeSize: number)
427
574
expect ( tree . nextLowerPair ( undefined ) ) . toEqual ( [ 300 , 600 ] ) ;
428
575
expect ( tree . nextHigherPair ( undefined ) ) . toEqual ( [ - 10 , - 20 ] ) ;
429
576
} ) ;
577
+
578
+ test ( 'Regression test for invalid default comparator causing malformed trees' , ( ) => {
579
+ const key = '24e26f0b-3c1a-47f8-a7a1-e8461ddb69ce6' ;
580
+ const tree = new BTree < string , { } > ( undefined , undefined , maxNodeSize ) ;
581
+ // The defaultComparator was not transitive for these inputs due to comparing numeric strings to each other numerically,
582
+ // but lexically when compared to non-numeric strings. This resulted in keys not being orderable, and the tree behaving incorrectly.
583
+ const inputs : [ string , { } ] [ ] = [
584
+ [ key , { } ] ,
585
+ [ '0' , { } ] ,
586
+ [ '1' , { } ] ,
587
+ [ '2' , { } ] ,
588
+ [ '3' , { } ] ,
589
+ [ '4' , { } ] ,
590
+ [ 'Cheese' , { } ] ,
591
+ [ '10' , { } ] ,
592
+ [ '11' , { } ] ,
593
+ [ '12' , { } ] ,
594
+ [ '13' , { } ] ,
595
+ [ '15' , { } ] ,
596
+ [ '16' , { } ] ,
597
+ ] ;
598
+
599
+ for ( const [ id , node ] of inputs ) {
600
+ expect ( tree . set ( id , node ) ) . toBeTruthy ( ) ;
601
+ tree . checkValid ( ) ;
602
+ expect ( tree . get ( key ) ) . not . toBeUndefined ( ) ;
603
+ }
604
+ expect ( tree . get ( key ) ) . not . toBeUndefined ( ) ;
605
+ } ) ;
430
606
}
0 commit comments