Skip to content

Commit 57c43e1

Browse files
committed
Tweak new defaultComparator and its documentation
Handle more things correctly: NaN, null, undefined, boolean. Also include Date in DefaultComparable
1 parent 067bd60 commit 57c43e1

File tree

4 files changed

+82
-96
lines changed

4 files changed

+82
-96
lines changed

b+tree.d.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export declare type EditRangeResult<V, R = number> = {
88
/**
99
* Types that BTree supports by default
1010
*/
11-
export declare type DefaultComparable = number | string | (number | string)[] | {
12-
valueOf: () => number | string | (number | string)[];
11+
export declare type DefaultComparable = number | string | Date | boolean | null | undefined | (number | string)[] | {
12+
valueOf: () => number | string | Date | boolean | null | undefined | (number | string)[];
1313
};
1414
/**
1515
* Compares DefaultComparables to form a strict partial ordering.
@@ -22,15 +22,18 @@ export declare type DefaultComparable = number | string | (number | string)[] |
2222
*/
2323
export declare function defaultComparator(a: DefaultComparable, b: DefaultComparable): number;
2424
/**
25-
* Compares finite numbers to form a strict partial ordering.
25+
* Compares items using the < and > operators. This function is probably slightly
26+
* faster than the defaultComparator for Dates and strings, but has not been benchmarked.
27+
* Unlike defaultComparator, this comparator doesn't support mixed types correctly,
28+
* i.e. use it with `BTree<string>` or `BTree<Date>` but not `BTree<string|Date>`.
2629
*
27-
* Handles +/-0 like Map: -0 is equal to +0.
30+
* Note: null compares as less than any number or Date, but in general null is
31+
* incomparable with strings, and undefined is not comparable with other types
32+
* using the > and < operators
2833
*/
29-
export declare function compareFiniteNumbers(a: number, b: number): number;
30-
/**
31-
* Compares strings lexically to form a strict partial ordering.
32-
*/
33-
export declare function compareStrings(a: string, b: string): number;
34+
export declare function simpleComparator(a: string, b: string): number;
35+
export declare function simpleComparator(a: number | null, b: number | null): number;
36+
export declare function simpleComparator(a: Date | null, b: Date | null): number;
3437
/**
3538
* A reasonably fast collection of key-value pairs with a powerful API.
3639
* Largely compatible with the standard Map. BTree is a B+ tree data structure,

b+tree.js

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var __extends = (this && this.__extends) || (function () {
1313
};
1414
})();
1515
Object.defineProperty(exports, "__esModule", { value: true });
16-
exports.EmptyBTree = exports.compareStrings = exports.compareFiniteNumbers = exports.defaultComparator = void 0;
16+
exports.EmptyBTree = exports.simpleComparator = exports.defaultComparator = void 0;
1717
/**
1818
* Compares DefaultComparables to form a strict partial ordering.
1919
*
@@ -25,70 +25,55 @@ exports.EmptyBTree = exports.compareStrings = exports.compareFiniteNumbers = exp
2525
*/
2626
function defaultComparator(a, b) {
2727
// Special case finite numbers first for performance.
28-
// Note that the trick of using 'a - b' the checking for NaN to detect non numbers values does not work if the strings are numeric (ex: "5"),
29-
// leading most comparison functions using that approach to fail to have transitivity.
28+
// Note that the trick of using 'a - b' and checking for NaN to detect non-numbers
29+
// does not work if the strings are numeric (ex: "5"). This would leading most
30+
// comparison functions using that approach to fail to have transitivity.
3031
if (Number.isFinite(a) && Number.isFinite(b)) {
31-
// Does not partially order NaNs or infinite values, but thats fine since they can't reach here.
32-
// This will handle -0 and 0 as equal.
3332
return a - b;
3433
}
35-
// Compare types and order values of different types by type.
36-
// This prevents implicit conversion of strings to numbers from causing invaliding ordering,
37-
// and generally simplifies which cases need to be considered below.
34+
// The default < and > operators are not totally ordered. To allow types to be mixed
35+
// in a single collection, compare types and order values of different types by type.
3836
var ta = typeof a;
3937
var tb = typeof b;
4038
if (ta !== tb) {
4139
return ta < tb ? -1 : 1;
4240
}
4341
if (ta === 'object') {
42+
// standardized JavaScript bug: null is not an object, but typeof says it is
43+
if (a === null)
44+
return b === null ? 0 : -1;
45+
else if (b === null)
46+
return 1;
4447
a = a.valueOf();
4548
b = b.valueOf();
4649
ta = typeof a;
4750
tb = typeof b;
48-
// Deal with one producing a string, and the other a number
51+
// Deal with the two valueOf()s producing different types
4952
if (ta !== tb) {
5053
return ta < tb ? -1 : 1;
5154
}
5255
}
53-
// a and b are now the same type, and either a number, string or array.
54-
// use Object.is to make NaN compare equal to NaN.
55-
// This treats also -0 as not equal to 0, which is handled separately below.
56-
if (Object.is(a, b))
57-
return 0;
58-
// All comparisons with NaN return false, so NaNs will pass here.
56+
// a and b are now the same type, and will be a number, string or array
57+
// (which we assume holds numbers or strings), or something unsupported.
5958
if (a < b)
6059
return -1;
61-
// Since a and b might be arrays, we cannot rely on === or ==, only < and > do something useful for ordering arrays.
62-
// To find if two arrays are equal using comparison operators, both < and > must be checked (even == returns false if not the same object).
6360
if (a > b)
6461
return 1;
62+
if (a === b)
63+
return 0;
6564
// Order NaN less than other numbers
6665
if (Number.isNaN(a))
67-
return -1;
68-
if (Number.isNaN(b))
66+
return Number.isNaN(b) ? 0 : -1;
67+
else if (Number.isNaN(b))
6968
return 1;
70-
// Handles 0 and -0 case, as well as equal (but not same object) arrays case.
71-
return 0;
69+
return 0; // unreachable?
7270
}
7371
exports.defaultComparator = defaultComparator;
7472
;
75-
/**
76-
* Compares finite numbers to form a strict partial ordering.
77-
*
78-
* Handles +/-0 like Map: -0 is equal to +0.
79-
*/
80-
function compareFiniteNumbers(a, b) {
81-
return a - b;
82-
}
83-
exports.compareFiniteNumbers = compareFiniteNumbers;
84-
;
85-
/**
86-
* Compares strings lexically to form a strict partial ordering.
87-
*/
88-
function compareStrings(a, b) {
89-
return a > b ? 1 : a === b ? 0 : -1;
73+
function simpleComparator(a, b) {
74+
return a > b ? 1 : a < b ? -1 : 0;
9075
}
91-
exports.compareStrings = compareStrings;
76+
exports.simpleComparator = simpleComparator;
9277
;
9378
/**
9479
* A reasonably fast collection of key-value pairs with a powerful API.

b+tree.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import BTree, {IMap, EmptyBTree, defaultComparator, compareFiniteNumbers, compareStrings} from './b+tree';
1+
import BTree, {IMap, EmptyBTree, defaultComparator, simpleComparator} from './b+tree';
22
import SortedArray from './sorted-array';
33
import MersenneTwister from 'mersenne-twister';
44

@@ -50,8 +50,8 @@ describe('defaultComparator', () =>
5050

5151
describe('compareFiniteNumbers', () =>
5252
{
53-
const sorted = [-10, -1, -0, 0, 1, 2, 10];
54-
testComparison(compareFiniteNumbers, sorted, sorted, [[-0, 0]]);
53+
const sorted = [-Infinity, -10, -1, -0, 0, 1, 2, 10, Infinity];
54+
testComparison(simpleComparator, sorted, sorted, [[-0, 0]]);
5555
});
5656

5757
describe('compareStrings', () =>
@@ -68,15 +68,15 @@ describe('compareStrings', () =>
6868
'10',
6969
"NaN",
7070
];;
71-
testComparison(compareStrings, [], values, []);
71+
testComparison(simpleComparator, [], values, []);
7272
});
7373

7474
/**
7575
* Tests a comparison function, ensuring it produces a strict partial order over the provided values.
7676
* Additionally confirms that the comparison function has the correct definition of equality via expectedDuplicates.
7777
*/
7878
function testComparison(comparison: (a: any, b: any) => number, inOrder: any[], values: any[], expectedDuplicates: [any, any][] = []) {
79-
function check(a: any, b: any): number {
79+
function compare(a: any, b: any): number {
8080
const v = comparison(a, b);
8181
expect(typeof v).toEqual('number');
8282
expect(v === v).toEqual(true); // Not NaN
@@ -91,7 +91,7 @@ function testComparison(comparison: (a: any, b: any) => number, inOrder: any[],
9191
let duplicates = [];
9292
for (let i = 0; i < values.length; i++) {
9393
for (let j = i + 1; j < values.length; j++) {
94-
if (check(values[i], values[j]) === 0) {
94+
if (compare(values[i], values[j]) === 0) {
9595
duplicates.push([values[i], values[j]]);
9696
}
9797
}
@@ -117,16 +117,16 @@ function testComparison(comparison: (a: any, b: any) => number, inOrder: any[],
117117
const asymmetric = []
118118
for (const a of values) {
119119
// irreflexive: compare(a, a) === 0
120-
if(check(a, a) !== 0) irreflexive.push(a);
120+
if(compare(a, a) !== 0) irreflexive.push(a);
121121
for (const b of values) {
122122
for (const c of values) {
123123
// transitive: if compare(a, b) < 0 and compare(b, c) < 0 then compare(a, c) < 0
124-
if (check(a, b) < 0 && check(b, c) < 0) {
125-
if(check(a, c) !== -1) transitive.push([a, b, c]);
124+
if (compare(a, b) < 0 && compare(b, c) < 0) {
125+
if(compare(a, c) !== -1) transitive.push([a, b, c]);
126126
}
127127
}
128128
// sign(compare(a, b)) === -sign(compare(b, a))
129-
if(check(a, b) !== -check(b, a)) asymmetric.push([a, b]);
129+
if(compare(a, b) !== -compare(b, a)) asymmetric.push([a, b]);
130130
}
131131
}
132132
expect(irreflexive).toEqual([]);

b+tree.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type index = number;
3333
/**
3434
* Types that BTree supports by default
3535
*/
36-
export type DefaultComparable = number | string | (number | string)[] | { valueOf: ()=> number | string | (number | string)[] };
36+
export type DefaultComparable = number | string | Date | boolean | null | undefined | (number | string)[] | { valueOf: ()=> number | string | Date | boolean | null | undefined | (number | string)[] };
3737

3838
/**
3939
* Compares DefaultComparables to form a strict partial ordering.
@@ -46,69 +46,67 @@ export type DefaultComparable = number | string | (number | string)[] | { valueO
4646
*/
4747
export function defaultComparator(a: DefaultComparable, b: DefaultComparable): number {
4848
// Special case finite numbers first for performance.
49-
// Note that the trick of using 'a - b' the checking for NaN to detect non numbers values does not work if the strings are numeric (ex: "5"),
50-
// leading most comparison functions using that approach to fail to have transitivity.
49+
// Note that the trick of using 'a - b' and checking for NaN to detect non-numbers
50+
// does not work if the strings are numeric (ex: "5"). This would leading most
51+
// comparison functions using that approach to fail to have transitivity.
5152
if (Number.isFinite(a) && Number.isFinite(b)) {
52-
// Does not partially order NaNs or infinite values, but thats fine since they can't reach here.
53-
// This will handle -0 and 0 as equal.
5453
return a as number - (b as number);
5554
}
5655

57-
// Compare types and order values of different types by type.
58-
// This prevents implicit conversion of strings to numbers from causing invaliding ordering,
59-
// and generally simplifies which cases need to be considered below.
56+
// The default < and > operators are not totally ordered. To allow types to be mixed
57+
// in a single collection, compare types and order values of different types by type.
6058
let ta = typeof a;
6159
let tb = typeof b;
6260
if (ta !== tb) {
6361
return ta < tb ? -1 : 1;
6462
}
6563

66-
if (ta === 'object'){
67-
a = a.valueOf() as DefaultComparable;
68-
b = b.valueOf() as DefaultComparable;
64+
if (ta === 'object') {
65+
// standardized JavaScript bug: null is not an object, but typeof says it is
66+
if (a === null)
67+
return b === null ? 0 : -1;
68+
else if (b === null)
69+
return 1;
70+
71+
a = a!.valueOf() as DefaultComparable;
72+
b = b!.valueOf() as DefaultComparable;
6973
ta = typeof a;
7074
tb = typeof b;
71-
// Deal with one producing a string, and the other a number
75+
// Deal with the two valueOf()s producing different types
7276
if (ta !== tb) {
7377
return ta < tb ? -1 : 1;
7478
}
7579
}
7680

77-
// a and b are now the same type, and either a number, string or array.
78-
79-
// use Object.is to make NaN compare equal to NaN.
80-
// This treats also -0 as not equal to 0, which is handled separately below.
81-
if (Object.is(a, b)) return 0;
82-
83-
// All comparisons with NaN return false, so NaNs will pass here.
84-
if (a < b) return -1;
85-
86-
// Since a and b might be arrays, we cannot rely on === or ==, only < and > do something useful for ordering arrays.
87-
// To find if two arrays are equal using comparison operators, both < and > must be checked (even == returns false if not the same object).
88-
if (a > b) return 1
81+
// a and b are now the same type, and will be a number, string or array
82+
// (which we assume holds numbers or strings), or something unsupported.
83+
if (a! < b!) return -1;
84+
if (a! > b!) return 1;
85+
if (a === b) return 0;
8986

9087
// Order NaN less than other numbers
91-
if (Number.isNaN(a)) return -1;
92-
if (Number.isNaN(b)) return 1;
93-
94-
// Handles 0 and -0 case, as well as equal (but not same object) arrays case.
95-
return 0;
88+
if (Number.isNaN(a))
89+
return Number.isNaN(b) ? 0 : -1;
90+
else if (Number.isNaN(b))
91+
return 1;
92+
return 0; // unreachable?
9693
};
9794

9895
/**
99-
* Compares finite numbers to form a strict partial ordering.
96+
* Compares items using the < and > operators. This function is probably slightly
97+
* faster than the defaultComparator for Dates and strings, but has not been benchmarked.
98+
* Unlike defaultComparator, this comparator doesn't support mixed types correctly,
99+
* i.e. use it with `BTree<string>` or `BTree<Date>` but not `BTree<string|Date>`.
100100
*
101-
* Handles +/-0 like Map: -0 is equal to +0.
102-
*/
103-
export function compareFiniteNumbers(a: number, b: number): number {
104-
return a - b;
105-
};
106-
107-
/**
108-
* Compares strings lexically to form a strict partial ordering.
101+
* Note: null compares as less than any number or Date, but in general null is
102+
* incomparable with strings, and undefined is not comparable with other types
103+
* using the > and < operators
109104
*/
110-
export function compareStrings(a: string, b:string): number {
111-
return a > b ? 1 : a === b ? 0 : -1;
105+
export function simpleComparator(a: string, b:string): number;
106+
export function simpleComparator(a: number|null, b:number|null): number;
107+
export function simpleComparator(a: Date|null, b:Date|null): number;
108+
export function simpleComparator(a: any, b: any): number {
109+
return a > b ? 1 : a < b ? -1 : 0;
112110
};
113111

114112
/**

0 commit comments

Comments
 (0)