1
- import BTree , { IMap , EmptyBTree } from './b+tree' ;
1
+ import BTree , { IMap , EmptyBTree , defaultComparator , simpleComparator } from './b+tree' ;
2
2
import SortedArray from './sorted-array' ;
3
3
import MersenneTwister from 'mersenne-twister' ;
4
4
@@ -14,6 +14,155 @@ 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 ( 'simpleComparator with non-NaN numbers and null' , ( ) =>
52
+ {
53
+ const sorted = [ - Infinity , - 10 , - 1 , - 0 , 0 , null , 1 , 2 , 10 , Infinity ] ;
54
+ testComparison < number | null > ( simpleComparator , sorted , sorted , [ [ - 0 , 0 ] , [ - 0 , null ] , [ 0 , null ] ] ) ;
55
+ } ) ;
56
+
57
+ describe ( 'simpleComparator with strings' , ( ) =>
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 < string > ( simpleComparator , [ ] , values , [ ] ) ;
72
+ } ) ;
73
+
74
+ describe ( 'simpleComparator with Date' , ( ) =>
75
+ {
76
+ const dateA = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 5 ) ) ;
77
+ const dateA2 = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 5 ) ) ;
78
+ const dateB = new Date ( Date . UTC ( 96 , 1 , 2 , 3 , 4 , 6 ) ) ;
79
+ const values = [
80
+ dateA ,
81
+ dateA2 ,
82
+ dateB ,
83
+ null ,
84
+ ] ;
85
+ testComparison < Date > ( simpleComparator , [ ] , values , [ [ dateA , dateA2 ] ] ) ;
86
+ } ) ;
87
+
88
+ describe ( 'simpleComparator arrays' , ( ) =>
89
+ {
90
+ const values = [
91
+ [ ] ,
92
+ [ 1 ] ,
93
+ [ '1' ] ,
94
+ [ 2 ] ,
95
+ ] ;
96
+ testComparison < ( number | string ) [ ] > ( simpleComparator , [ ] , values , [ [ [ 1 ] , [ '1' ] ] ] ) ;
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 < T > ( comparison : ( a : T , b : T ) => number , inOrder : T [ ] , values : T [ ] , expectedDuplicates : [ T , T ] [ ] = [ ] ) {
104
+ function compare ( a : T , b : T ) : number {
105
+ const v = comparison ( a , b ) ;
106
+ expect ( typeof v ) . toEqual ( 'number' ) ;
107
+ if ( v !== v )
108
+ console . log ( '!!!' , a , b ) ;
109
+ expect ( v === v ) . toEqual ( true ) ; // Not NaN
110
+ return Math . sign ( v ) ;
111
+ }
112
+
113
+ test ( 'comparison has correct order' , ( ) => {
114
+ expect ( [ ...inOrder ] . sort ( comparison ) ) . toMatchObject ( inOrder ) ;
115
+ } ) ;
116
+
117
+ test ( 'comparison deffierantes values' , ( ) => {
118
+ let duplicates = [ ] ;
119
+ for ( let i = 0 ; i < values . length ; i ++ ) {
120
+ for ( let j = i + 1 ; j < values . length ; j ++ ) {
121
+ if ( compare ( values [ i ] , values [ j ] ) === 0 ) {
122
+ duplicates . push ( [ values [ i ] , values [ j ] ] ) ;
123
+ }
124
+ }
125
+ }
126
+ expect ( duplicates ) . toMatchObject ( expectedDuplicates ) ;
127
+ } ) ;
128
+
129
+ test ( 'comparison forms a strict partial ordering' , ( ) => {
130
+ // To be a strict partial order, the function must be:
131
+ // irreflexive: not a < a
132
+ // transitive: if a < b and b < c then a < c
133
+ // asymmetric: if a < b then not b < a
134
+
135
+ // 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:
136
+ // irreflexive: compare(a, a) === 0
137
+ // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0
138
+ // asymmetric: sign(compare(a, b)) === -sign(compare(b, a))
139
+
140
+ // This can is brute forced in O(n^3) time below:
141
+ // Violations
142
+ const irreflexive = [ ]
143
+ const transitive = [ ]
144
+ const asymmetric = [ ]
145
+ for ( const a of values ) {
146
+ // irreflexive: compare(a, a) === 0
147
+ if ( compare ( a , a ) !== 0 ) irreflexive . push ( a ) ;
148
+ for ( const b of values ) {
149
+ for ( const c of values ) {
150
+ // transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0
151
+ if ( compare ( a , b ) < 0 && compare ( b , c ) < 0 ) {
152
+ if ( compare ( a , c ) !== - 1 ) transitive . push ( [ a , b , c ] ) ;
153
+ }
154
+ }
155
+ // sign(compare(a, b)) === -sign(compare(b, a))
156
+ if ( compare ( a , b ) !== - compare ( b , a ) ) asymmetric . push ( [ a , b ] ) ;
157
+ }
158
+ }
159
+ expect ( irreflexive ) . toEqual ( [ ] ) ;
160
+ expect ( transitive ) . toEqual ( [ ] ) ;
161
+ expect ( asymmetric ) . toEqual ( [ ] ) ;
162
+ } ) ;
163
+ }
164
+
165
+
17
166
describe ( 'Simple tests on leaf nodes' , ( ) =>
18
167
{
19
168
test ( 'A few insertions (fanout 8)' , insert8 . bind ( null , 8 ) ) ;
@@ -427,4 +576,33 @@ function testBTree(maxNodeSize: number)
427
576
expect ( tree . nextLowerPair ( undefined ) ) . toEqual ( [ 300 , 600 ] ) ;
428
577
expect ( tree . nextHigherPair ( undefined ) ) . toEqual ( [ - 10 , - 20 ] ) ;
429
578
} ) ;
579
+
580
+ test ( 'Regression test for invalid default comparator causing malformed trees' , ( ) => {
581
+ const key = '24e26f0b-3c1a-47f8-a7a1-e8461ddb69ce6' ;
582
+ const tree = new BTree < string , { } > ( undefined , undefined , maxNodeSize ) ;
583
+ // The defaultComparator was not transitive for these inputs due to comparing numeric strings to each other numerically,
584
+ // but lexically when compared to non-numeric strings. This resulted in keys not being orderable, and the tree behaving incorrectly.
585
+ const inputs : [ string , { } ] [ ] = [
586
+ [ key , { } ] ,
587
+ [ '0' , { } ] ,
588
+ [ '1' , { } ] ,
589
+ [ '2' , { } ] ,
590
+ [ '3' , { } ] ,
591
+ [ '4' , { } ] ,
592
+ [ 'Cheese' , { } ] ,
593
+ [ '10' , { } ] ,
594
+ [ '11' , { } ] ,
595
+ [ '12' , { } ] ,
596
+ [ '13' , { } ] ,
597
+ [ '15' , { } ] ,
598
+ [ '16' , { } ] ,
599
+ ] ;
600
+
601
+ for ( const [ id , node ] of inputs ) {
602
+ expect ( tree . set ( id , node ) ) . toBeTruthy ( ) ;
603
+ tree . checkValid ( ) ;
604
+ expect ( tree . get ( key ) ) . not . toBeUndefined ( ) ;
605
+ }
606
+ expect ( tree . get ( key ) ) . not . toBeUndefined ( ) ;
607
+ } ) ;
430
608
}
0 commit comments