From 193c0b4cd50fa6585aa234575262ed7e3bd14e1a Mon Sep 17 00:00:00 2001 From: Simon Accascina Date: Fri, 17 May 2024 15:10:11 +0200 Subject: [PATCH 1/2] Copy exactly sorted map/set impl from `applitopia/immutable-sorted` Copy exactly `applitopia/immutable-sorted`'s implementation of sorted map and sorted set minus the partial sort impplementation https://github.com/applitopia/immutable-sorted/commit/d700f18c719695780c5ce84d7b2a6b2554119dfb Co-authored-by: Applitopia --- __tests__/MultiRequire.js | 5 + __tests__/SortedMap.ts | 451 ++++++ __tests__/SortedSet.ts | 396 +++++ src/CollectionImpl.js | 16 + src/Immutable.js | 14 + src/List.js | 3 +- src/Map.js | 8 +- src/Set.js | 3 +- src/SortedMap.js | 523 +++++++ src/SortedMapBtreeNode.js | 2526 +++++++++++++++++++++++++++++++ src/SortedMapNode.js | 50 + src/SortedSet.js | 158 ++ src/TrieUtils.js | 4 + src/predicates/isSorted copy.js | 19 + src/predicates/isSorted.js | 19 + src/predicates/isSortedMap.js | 20 + src/predicates/isSortedSet.js | 13 + src/utils/deepEqual.js | 4 +- type-definitions/immutable.d.ts | 850 ++++++++++- 19 files changed, 5074 insertions(+), 8 deletions(-) create mode 100644 __tests__/SortedMap.ts create mode 100644 __tests__/SortedSet.ts create mode 100644 src/SortedMap.js create mode 100644 src/SortedMapBtreeNode.js create mode 100644 src/SortedMapNode.js create mode 100644 src/SortedSet.js create mode 100644 src/predicates/isSorted copy.js create mode 100644 src/predicates/isSorted.js create mode 100644 src/predicates/isSortedMap.js create mode 100644 src/predicates/isSortedSet.js diff --git a/__tests__/MultiRequire.js b/__tests__/MultiRequire.js index 44fd2e42ad..eab13c22a5 100644 --- a/__tests__/MultiRequire.js +++ b/__tests__/MultiRequire.js @@ -73,6 +73,11 @@ describe('MultiRequire', () => { expect(Immutable1.OrderedMap.isOrderedMap(c2)).toBe(true); expect(Immutable2.OrderedMap.isOrderedMap(c1)).toBe(true); + c1 = Immutable1.SortedMap(); + c2 = Immutable2.SortedMap(); + expect(Immutable1.SortedMap.isSortedMap(c2)).toBe(true); + expect(Immutable2.SortedMap.isSortedMap(c1)).toBe(true); + c1 = Immutable1.List(); c2 = Immutable2.List(); expect(Immutable1.List.isList(c2)).toBe(true); diff --git a/__tests__/SortedMap.ts b/__tests__/SortedMap.ts new file mode 100644 index 0000000000..9bf745116c --- /dev/null +++ b/__tests__/SortedMap.ts @@ -0,0 +1,451 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/// + +import * as jasmineCheck from 'jasmine-check'; +jasmineCheck.install(); + +import { is, List, Range, Record, Seq, SortedMap } from '../'; + +describe('SortedMap', () => { + it('converts from object', () => { + const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); + expect(m.size).toBe(3); + expect(m.get('a')).toBe('A'); + expect(m.get('b')).toBe('B'); + expect(m.get('c')).toBe('C'); + }); + + it('constructor provides initial values', () => { + const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); + expect(m.size).toBe(3); + expect(m.get('a')).toBe('A'); + expect(m.get('b')).toBe('B'); + expect(m.get('c')).toBe('C'); + }); + + it('constructor provides initial values as array of entries', () => { + const m = SortedMap([ + ['a', 'A'], + ['b', 'B'], + ['c', 'C'], + ]); + expect(m.size).toBe(3); + expect(m.get('a')).toBe('A'); + expect(m.get('b')).toBe('B'); + expect(m.get('c')).toBe('C'); + }); + + it('constructor provides initial values as sequence', () => { + const s = Seq({ a: 'A', b: 'B', c: 'C' }); + const m = SortedMap(s); + expect(m.size).toBe(3); + expect(m.get('a')).toBe('A'); + expect(m.get('b')).toBe('B'); + expect(m.get('c')).toBe('C'); + }); + + it('constructor provides initial values as list of lists', () => { + const l = List([List(['a', 'A']), List(['b', 'B']), List(['c', 'C'])]); + const m = SortedMap(l); + expect(m.size).toBe(3); + expect(m.get('a')).toBe('A'); + expect(m.get('b')).toBe('B'); + expect(m.get('c')).toBe('C'); + }); + + it('constructor is identity when provided map', () => { + const m1 = SortedMap({ a: 'A', b: 'B', c: 'C' }); + const m2 = SortedMap(m1); + expect(m2).toBe(m1); + }); + + it('does not accept a scalar', () => { + expect(() => { + SortedMap(3 as any); + }).toThrow( + 'Expected Array or collection object of [k, v] entries, or keyed object: 3' + ); + }); + + it('does not accept strings (collection, but scalar)', () => { + expect(() => { + SortedMap('abc'); + }).toThrow(); + }); + + it('does not accept non-entries array', () => { + expect(() => { + SortedMap([1, 2, 3] as any); + }).toThrow('Expected [K, V] tuple: 1'); + }); + + it('accepts non-collection array-like objects as keyed collections', () => { + const m = SortedMap({ length: 3, 1: 'one' }); + expect(m.get('length')).toBe(3); + expect(m.get('1')).toBe('one'); + expect(m.toJS()).toEqual({ length: 3, 1: 'one' }); + }); + + it('accepts flattened pairs via of()', () => { + const m: SortedMap = SortedMap.of(1, 'a', 2, 'b', 3, 'c'); + expect(m.size).toBe(3); + expect(m.get(1)).toBe('a'); + expect(m.get(2)).toBe('b'); + expect(m.get(3)).toBe('c'); + }); + + it('does not accept mismatched flattened pairs via of()', () => { + expect(() => { + SortedMap.of(1, 2, 3); + }).toThrow('Missing value for key: 3'); + }); + + it('converts back to JS object', () => { + const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); + expect(m.toObject()).toEqual({ a: 'A', b: 'B', c: 'C' }); + }); + + it('iterates values', () => { + const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); + const iterator = jest.genMockFunction(); + m.forEach(iterator); + expect(iterator.mock.calls).toEqual([ + ['A', 'a', m], + ['B', 'b', m], + ['C', 'c', m], + ]); + }); + + it('merges two maps', () => { + const m1 = SortedMap({ a: 'A', b: 'B', c: 'C' }); + const m2 = SortedMap({ wow: 'OO', d: 'DD', b: 'BB' }); + expect(m2.toObject()).toEqual({ wow: 'OO', d: 'DD', b: 'BB' }); + const m3 = m1.merge(m2); + expect(m3.toObject()).toEqual({ + a: 'A', + b: 'BB', + c: 'C', + wow: 'OO', + d: 'DD', + }); + }); + + it('accepts null as a key', () => { + const m1 = SortedMap(); + const m2 = m1.set(null, 'null'); + const m3 = m2.remove(null); + expect(m1.size).toBe(0); + expect(m2.size).toBe(1); + expect(m3.size).toBe(0); + expect(m2.get(null)).toBe('null'); + }); + + it('is persistent to sets', () => { + const m1 = SortedMap(); + const m2 = m1.set('a', 'Aardvark'); + const m3 = m2.set('b', 'Baboon'); + const m4 = m3.set('c', 'Canary'); + const m5 = m4.set('b', 'Bonobo'); + expect(m1.size).toBe(0); + expect(m2.size).toBe(1); + expect(m3.size).toBe(2); + expect(m4.size).toBe(3); + expect(m5.size).toBe(3); + expect(m3.get('b')).toBe('Baboon'); + expect(m5.get('b')).toBe('Bonobo'); + }); + + it('is persistent to deletes', () => { + const m1 = SortedMap(); + const m2 = m1.set('a', 'Aardvark'); + const m3 = m2.set('b', 'Baboon'); + const m4 = m3.set('c', 'Canary'); + const m5 = m4.remove('b'); + expect(m1.size).toBe(0); + expect(m2.size).toBe(1); + expect(m3.size).toBe(2); + expect(m4.size).toBe(3); + expect(m5.size).toBe(2); + expect(m3.has('b')).toBe(true); + expect(m3.get('b')).toBe('Baboon'); + expect(m5.has('b')).toBe(false); + expect(m5.get('b')).toBe(undefined); + expect(m5.get('c')).toBe('Canary'); + }); + + check.it('deletes down to empty map', [gen.posInt], size => { + let m = Range(0, size).toSortedMap(); + expect(m.size).toBe(size); + for (let ii = size - 1; ii >= 0; ii--) { + m = m.remove(ii); + expect(m.size).toBe(ii); + } + expect(m).toBe(SortedMap()); + }); + + it('can map many items', () => { + let m = SortedMap(); + for (let ii = 0; ii < 2000; ii++) { + m = m.set('thing:' + ii, ii); + } + expect(m.size).toBe(2000); + expect(m.get('thing:1234')).toBe(1234); + }); + + it('can use weird keys', () => { + const m: SortedMap = SortedMap() + .set(NaN, 1) + .set(Infinity, 2) + .set(-Infinity, 3); + + expect(m.get(NaN)).toBe(1); + expect(m.get(Infinity)).toBe(2); + expect(m.get(-Infinity)).toBe(3); + }); + + it('can map items known to hash collide', () => { + // make a big map, so it hashmaps + let m: SortedMap = Range(0, 32).toSortedMap(); + m = m.set('AAA', 'letters').set(64545, 'numbers'); + expect(m.size).toBe(34); + expect(m.get('AAA')).toEqual('letters'); + expect(m.get(64545)).toEqual('numbers'); + }); + + it('can progressively add items known to collide', () => { + // make a big map, so it hashmaps + let map: SortedMap = Range(0, 32).toSortedMap(); + map = map.set('@', '@'); + map = map.set(64, 64); + map = map.set(96, 96); + expect(map.size).toBe(35); + expect(map.get('@')).toBe('@'); + expect(map.get(64)).toBe(64); + expect(map.get(96)).toBe(96); + }); + + it('maps values', () => { + const m = SortedMap({ a: 'a', b: 'b', c: 'c' }); + const r = m.map(value => value.toUpperCase()); + expect(r.toObject()).toEqual({ a: 'A', b: 'B', c: 'C' }); + }); + + it('maps keys', () => { + const m = SortedMap({ a: 'a', b: 'b', c: 'c' }); + const r = m.mapKeys(key => key.toUpperCase()); + expect(r.toObject()).toEqual({ A: 'a', B: 'b', C: 'c' }); + }); + + it('filters values', () => { + const m = SortedMap({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }); + const r = m.filter(value => value % 2 === 1); + expect(r.toObject()).toEqual({ a: 1, c: 3, e: 5 }); + }); + + it('filterNots values', () => { + const m = SortedMap({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }); + const r = m.filterNot(value => value % 2 === 1); + expect(r.toObject()).toEqual({ b: 2, d: 4, f: 6 }); + }); + + it('derives keys', () => { + const v = SortedMap({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }); + expect(v.keySeq().toArray()).toEqual(['a', 'b', 'c', 'd', 'e', 'f']); + }); + + it('flips keys and values', () => { + const v = SortedMap({ a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }); + expect(v.flip().toObject()).toEqual({ + 1: 'a', + 2: 'b', + 3: 'c', + 4: 'd', + 5: 'e', + 6: 'f', + }); + }); + + it('can convert to a list', () => { + const m = SortedMap({ a: 1, b: 2, c: 3 }); + const v = m.toList(); + const k = m.keySeq().toList(); + expect(v.size).toBe(3); + expect(k.size).toBe(3); + expect(v.get(1)).toBe(2); + expect(k.get(1)).toBe('b'); + }); + + check.it( + 'works like an object', + { maxSize: 50 }, + [gen.object(gen.JSONPrimitive)], + obj => { + let map = SortedMap(obj); + Object.keys(obj).forEach(key => { + expect(map.get(key)).toBe(obj[key]); + expect(map.has(key)).toBe(true); + }); + Object.keys(obj).forEach(key => { + expect(map.get(key)).toBe(obj[key]); + expect(map.has(key)).toBe(true); + map = map.remove(key); + expect(map.get(key)).toBe(undefined); + expect(map.has(key)).toBe(false); + }); + } + ); + + check.it('sets', { maxSize: 5000 }, [gen.posInt], len => { + let map = SortedMap(); + for (let ii = 0; ii < len; ii++) { + expect(map.size).toBe(ii); + map = map.set('' + ii, ii); + } + expect(map.size).toBe(len); + expect(is(map.toSet(), Range(0, len).toSet())).toBe(true); + }); + + check.it('has and get', { maxSize: 5000 }, [gen.posInt], len => { + const map = Range(0, len) + .toKeyedSeq() + .mapKeys(x => '' + x) + .toSortedMap(); + for (let ii = 0; ii < len; ii++) { + expect(map.get('' + ii)).toBe(ii); + expect(map.has('' + ii)).toBe(true); + } + }); + + check.it('deletes', { maxSize: 5000 }, [gen.posInt], len => { + let map = Range(0, len).toSortedMap(); + for (let ii = 0; ii < len; ii++) { + expect(map.size).toBe(len - ii); + map = map.remove(ii); + } + expect(map.size).toBe(0); + expect(map.toObject()).toEqual({}); + }); + + check.it('deletes from transient', { maxSize: 5000 }, [gen.posInt], len => { + const map = Range(0, len).toSortedMap().asMutable(); + for (let ii = 0; ii < len; ii++) { + expect(map.size).toBe(len - ii); + map.remove(ii); + } + expect(map.size).toBe(0); + expect(map.toObject()).toEqual({}); + }); + + check.it('iterates through all entries', [gen.posInt], len => { + const v = Range(0, len).toSortedMap(); + const a = v.toArray(); + const iter = v.entries(); + for (let ii = 0; ii < len; ii++) { + delete a[iter.next().value[0]]; + } + expect(a).toEqual(new Array(len)); + }); + + it('allows chained mutations', () => { + const m1 = SortedMap(); + const m2 = m1.set('a', 1); + const m3 = m2.withMutations(m => m.set('b', 2).set('c', 3)); + const m4 = m3.set('d', 4); + + expect(m1.toObject()).toEqual({}); + expect(m2.toObject()).toEqual({ a: 1 }); + expect(m3.toObject()).toEqual({ a: 1, b: 2, c: 3 }); + expect(m4.toObject()).toEqual({ a: 1, b: 2, c: 3, d: 4 }); + }); + + it('chained mutations does not result in new empty map instance', () => { + const v1 = SortedMap({ x: 1 }); + const v2 = v1.withMutations(v => v.set('y', 2).delete('x').delete('y')); + expect(v2).toBe(SortedMap()); + }); + + it('expresses value equality with unordered sequences', () => { + const m1 = SortedMap({ A: 1, B: 2, C: 3 }); + const m2 = SortedMap({ C: 3, B: 2, A: 1 }); + expect(is(m1, m2)).toBe(true); + }); + + it('deletes all the provided keys', () => { + const NOT_SET = undefined; + const m1 = SortedMap({ A: 1, B: 2, C: 3 }); + const m2 = m1.deleteAll(['A', 'B']); + expect(m2.get('A')).toBe(NOT_SET); + expect(m2.get('B')).toBe(NOT_SET); + expect(m2.get('C')).toBe(3); + expect(m2.size).toBe(1); + }); + + it('remains unchanged when no keys are provided', () => { + const m1 = SortedMap({ A: 1, B: 2, C: 3 }); + const m2 = m1.deleteAll([]); + expect(m1).toBe(m2); + }); + + it('uses toString on keys and values', () => { + class A extends Record({ x: null as number | null }) { + toString() { + return this.x; + } + } + + const r = new A({ x: 2 }); + const map = SortedMap([[r, r]]); + expect(map.toString()).toEqual('SortedMap { 2: 2 }'); + }); + + it('builds correct seq in function from', () => { + const size = 10000; + const data = Range(0, size).map(v => [v, 2 * v]); + const s = new SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); + + expect(s.toSeq().size).toBe(size); + + for (let n = 10; n < 100; n += 13) { + for (let i = 0; i < size; i += 17) { + const limit = i + n < size ? i + n : size; + const seq = s.from(i).takeWhile((v, k) => k < limit); + const l1 = seq.keySeq().toList(); + const l2 = Range(i, limit).toList(); + expect(is(l1, l2)).toEqual(true); + } + } + }); + + it('builds correct seq in function from backwards', () => { + const size = 10000; + const data = Range(0, size).map(v => [v, 2 * v]); + const s = new SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); + + expect(s.toSeq().size).toBe(size); + + for (let n = 10; n < 100; n += 13) { + for (let i = 0; i < size; i += 17) { + const limit = i + 1 >= n ? i + 1 - n : 0; + const seq = s.from(i, true).takeWhile((v, k) => k >= limit); + const l1 = seq.keySeq().toList(); + const l2 = Range(i, limit - 1, -1).toList(); + // tslint:disable-next-line:no-console + // console.log(l1, l2); + expect(is(l1, l2)).toEqual(true); + } + } + }); +}); diff --git a/__tests__/SortedSet.ts b/__tests__/SortedSet.ts new file mode 100644 index 0000000000..c2599db7a9 --- /dev/null +++ b/__tests__/SortedSet.ts @@ -0,0 +1,396 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/// + +declare var Symbol: any; +import { + is, + List, + Map, + OrderedSet, + Range, + Seq, + Set, + SortedMap, + SortedSet, +} from '../'; + +declare function expect(val: any): ExpectWithIs; + +interface ExpectWithIs extends Expect { + is(expected: any): void; + not: ExpectWithIs; +} + +jasmine.addMatchers({ + is() { + return { + compare(actual, expected) { + const passed = is(actual, expected); + return { + pass: passed, + message: + 'Expected ' + + actual + + (passed ? '' : ' not') + + ' to equal ' + + expected, + }; + }, + }; + }, +}); + +describe('SortedSet', () => { + it('accepts array of values', () => { + const s = SortedSet([1, 2, 3]); + expect(s.has(1)).toBe(true); + expect(s.has(2)).toBe(true); + expect(s.has(3)).toBe(true); + expect(s.has(4)).toBe(false); + }); + + it('accepts array-like of values', () => { + const s = SortedSet({ length: 3, 2: 3 } as any); + expect(s.size).toBe(2); + expect(s.has(undefined)).toBe(true); + expect(s.has(3)).toBe(true); + expect(s.has(2)).toBe(false); + }); + + it('accepts string, an array-like collection', () => { + const s = SortedSet('abc'); + expect(s.size).toBe(3); + expect(s.has('a')).toBe(true); + expect(s.has('b')).toBe(true); + expect(s.has('c')).toBe(true); + expect(s.has('abc')).toBe(false); + }); + + it('accepts sequence of values', () => { + const seq = Seq([1, 2, 3]); + const s = SortedSet(seq); + expect(s.has(1)).toBe(true); + expect(s.has(2)).toBe(true); + expect(s.has(3)).toBe(true); + expect(s.has(4)).toBe(false); + }); + + it('accepts a keyed Seq as a set of entries', () => { + const seq = Seq({ a: null, b: null, c: null }).flip(); + const s = SortedSet(seq); + expect(s.toArray()).toEqual([ + [null, 'a'], + [null, 'b'], + [null, 'c'], + ]); + // Explicitly getting the values sequence + const s2 = SortedSet(seq.valueSeq()); + expect(s2.toArray()).toEqual(['a', 'b', 'c']); + // toSet() does this for you. + const v3 = seq.toSortedSet(); + expect(v3.toArray()).toEqual(['a', 'b', 'c']); + }); + + it('accepts object keys', () => { + const s = SortedSet.fromKeys({ a: null, b: null, c: null }); + expect(s.has('a')).toBe(true); + expect(s.has('b')).toBe(true); + expect(s.has('c')).toBe(true); + expect(s.has('d')).toBe(false); + }); + + it('accepts sequence keys', () => { + const seq = Seq({ a: null, b: null, c: null }); + const s = SortedSet.fromKeys(seq); + expect(s.has('a')).toBe(true); + expect(s.has('b')).toBe(true); + expect(s.has('c')).toBe(true); + expect(s.has('d')).toBe(false); + }); + + it('accepts explicit values', () => { + const s = SortedSet.of(1, 2, 3); + expect(s.has(1)).toBe(true); + expect(s.has(2)).toBe(true); + expect(s.has(3)).toBe(true); + expect(s.has(4)).toBe(false); + }); + + it('converts back to JS array', () => { + const s = SortedSet.of(1, 2, 3); + expect(s.toArray()).toEqual([1, 2, 3]); + }); + + it('converts back to JS object', () => { + const s = SortedSet.of('a', 'b', 'c'); + expect(s.toObject()).toEqual({ a: 'a', b: 'b', c: 'c' }); + }); + + it('unions an unknown collection of SortedSets', () => { + const abc = SortedSet(['a', 'b', 'c']); + const cat = SortedSet(['c', 'a', 't']); + expect(Set.union([abc, cat]).toArray()).toEqual(['a', 'c', 't', 'b']); + expect(Set.union([abc])).toEqual(Set(['a', 'b', 'c'])); + expect(Set.union([])).toBe(Set()); + }); + + it('intersects an unknown collection of SortedSets', () => { + const abc = SortedSet(['a', 'b', 'c']); + const cat = SortedSet(['c', 'a', 't']); + expect(Set.intersect([abc, cat]).toArray()).toEqual(['a', 'c']); + }); + + it('iterates values', () => { + const s = SortedSet.of(1, 2, 3); + const iterator = jest.genMockFunction(); + s.forEach(iterator); + expect(iterator.mock.calls).toEqual([ + [1, 1, s], + [2, 2, s], + [3, 3, s], + ]); + }); + + it('unions two sets', () => { + const s1 = SortedSet.of('a', 'b', 'c'); + const s2 = SortedSet.of('d', 'b', 'wow'); + const s3 = s1.union(s2); + expect(s3.toArray()).toEqual(['a', 'b', 'c', 'd', 'wow']); + }); + + it('returns self when union results in no-op', () => { + const s1 = SortedSet.of('a', 'b', 'c'); + const s2 = SortedSet.of('c', 'a'); + const s3 = s1.union(s2); + expect(s3).toBe(s1); + }); + + it('returns arg when union results in no-op', () => { + const s1 = SortedSet(); + const s2 = SortedSet.of('a', 'b', 'c'); + const s3 = s1.union(s2); + expect(s3).toBe(s2); + }); + + it('unions a set and another collection and returns a set', () => { + const s1 = SortedSet([1, 2, 3]); + const emptySortedSet = SortedSet(); + const l = List([1, 2, 3]); + const s2 = s1.union(l); + const s3 = emptySortedSet.union(l); + const o = OrderedSet([1, 2, 3]); + const s4 = s1.union(o); + const s5 = emptySortedSet.union(o); + expect(Set.isSet(s2)).toBe(true); + expect(Set.isSet(s3)).toBe(true); + expect(Set.isSet(s4) && !OrderedSet.isOrderedSet(s4)).toBe(true); + expect(Set.isSet(s5) && !OrderedSet.isOrderedSet(s5)).toBe(true); + }); + + it('is persistent to adds', () => { + const s1 = SortedSet(); + const s2 = s1.add('a'); + const s3 = s2.add('b'); + const s4 = s3.add('c'); + const s5 = s4.add('b'); + expect(s1.size).toBe(0); + expect(s2.size).toBe(1); + expect(s3.size).toBe(2); + expect(s4.size).toBe(3); + expect(s5.size).toBe(3); + }); + + it('is persistent to deletes', () => { + const s1 = SortedSet(); + const s2 = s1.add('a'); + const s3 = s2.add('b'); + const s4 = s3.add('c'); + const s5 = s4.remove('b'); + expect(s1.size).toBe(0); + expect(s2.size).toBe(1); + expect(s3.size).toBe(2); + expect(s4.size).toBe(3); + expect(s5.size).toBe(2); + expect(s3.has('b')).toBe(true); + expect(s5.has('b')).toBe(false); + }); + + it('deletes down to empty set', () => { + const s = SortedSet.of('A').remove('A'); + expect(s).toEqual(SortedSet()); + }); + + it('unions multiple sets', () => { + const s = SortedSet.of('A', 'B', 'C').union( + SortedSet.of('C', 'D', 'E'), + SortedSet.of('D', 'B', 'F') + ); + expect(s).is(SortedSet.of('A', 'B', 'C', 'D', 'E', 'F')); + }); + + it('intersects multiple sets', () => { + const s = SortedSet.of('A', 'B', 'C').intersect( + SortedSet.of('B', 'C', 'D'), + SortedSet.of('A', 'C', 'E') + ); + expect(s).is(SortedSet.of('C')); + }); + + it('diffs multiple sets', () => { + const s = SortedSet.of('A', 'B', 'C').subtract( + SortedSet.of('C', 'D', 'E'), + SortedSet.of('D', 'B', 'F') + ); + expect(s).is(SortedSet.of('A')); + }); + + it('expresses value equality with set sequences', () => { + const s1 = SortedSet.of('A', 'B', 'C'); + expect(s1.equals(null)).toBe(false); + + const s2 = SortedSet.of('C', 'B', 'A'); + expect(s1 === s2).toBe(false); + expect(is(s1, s2)).toBe(true); + expect(s1.equals(s2)).toBe(true); + + // SortedMap and SortedSet are not the same (keyed vs unkeyed) + const v1 = SortedMap({ A: 'A', C: 'C', B: 'B' }); + expect(is(s1, v1)).toBe(false); + }); + + it('can use union in a withMutation', () => { + const js = SortedSet() + .withMutations(set => { + set.union(['a']); + set.add('b'); + }) + .toJS(); + expect(js).toEqual(['a', 'b']); + }); + + it('can determine if an array is a subset', () => { + const s = SortedSet.of('A', 'B', 'C'); + expect(s.isSuperset(['B', 'C'])).toBe(true); + expect(s.isSuperset(['B', 'C', 'D'])).toBe(false); + }); + + describe('accepts Symbol as entry #579', () => { + if (typeof Symbol !== 'function') { + Symbol = function (key) { + return { key, __proto__: Symbol }; + }; + Symbol.toString = function () { + return 'Symbol(' + (this.key || '') + ')'; + }; + } + + it('operates on small number of symbols, preserving set uniqueness', () => { + const a = Symbol(); + const b = Symbol(); + const c = Symbol(); + + const symbolSet = SortedSet([a, b, c, a, b, c, a, b, c, a, b, c]); + expect(symbolSet.size).toBe(1); + expect(symbolSet.has(b)).toBe(true); + expect(symbolSet.get(c)).toEqual(c); + }); + + it('operates on a large number of symbols, maintaining obj uniqueness', () => { + const manySymbols = [ + Symbol('a'), + Symbol('b'), + Symbol('c'), + Symbol('a'), + Symbol('b'), + Symbol('c'), + Symbol('a'), + Symbol('b'), + Symbol('c'), + Symbol('a'), + Symbol('b'), + Symbol('c'), + ]; + + const symbolSet = SortedSet(manySymbols); + expect(symbolSet.size).toBe(3); + expect(symbolSet.has(manySymbols[10])).toBe(true); + expect(symbolSet.get(manySymbols[10])).toEqual(manySymbols[10]); + }); + }); + + it('can use intersect after add or union in a withMutation', () => { + const set = SortedSet(['a', 'd']).withMutations(s => { + s.add('b'); + s.union(['c']); + s.intersect(['b', 'c', 'd']); + }); + expect(set.toArray()).toEqual(['b', 'c', 'd']); + }); + + it('can count entries that satisfy a predicate', () => { + const set = SortedSet([1, 2, 3, 4, 5]); + expect(set.size).toEqual(5); + expect(set.count()).toEqual(5); + expect(set.count(x => x % 2 === 0)).toEqual(2); + expect(set.count(x => true)).toEqual(5); + }); + + it('works with the `new` operator #3', () => { + const s = new SortedSet([1, 2, 3]); + expect(s.has(1)).toBe(true); + expect(s.has(2)).toBe(true); + expect(s.has(3)).toBe(true); + expect(s.has(4)).toBe(false); + }); + + it('builds correct seq in function from', () => { + const size = 10000; + const data = Range(0, size); + const s = new SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); + + expect(s.toSeq().size).toBe(size); + + for (let n = 10; n < 100; n += 13) { + for (let i = 0; i < size; i += 17) { + const limit = i + n < size ? i + n : size; + const seq = s.from(i).takeWhile(k => k < limit); + const l1 = seq.toList(); + const l2 = Range(i, limit).toList(); + expect(is(l1, l2)).toEqual(true); + } + } + }); + + it('builds correct seq in function from backwards', () => { + const size = 10000; + const data = Range(0, size); + const s = new SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); + + expect(s.toSeq().size).toBe(size); + + for (let n = 10; n < 100; n += 13) { + for (let i = 0; i < size; i += 17) { + const limit = i + 1 >= n ? i + 1 - n : 0; + const seq = s.from(i, true).takeWhile(k => k >= limit); + const l1 = seq.toList(); + const l2 = Range(i, limit - 1, -1).toList(); + // tslint:disable-next-line:no-console + // console.log(l1, l2); + expect(is(l1, l2)).toEqual(true); + } + } + }); +}); diff --git a/src/CollectionImpl.js b/src/CollectionImpl.js index bc33af022a..416bdb151d 100644 --- a/src/CollectionImpl.js +++ b/src/CollectionImpl.js @@ -36,6 +36,8 @@ import quoteString from './utils/quoteString'; import { toJS } from './toJS'; import { Map } from './Map'; import { OrderedMap } from './OrderedMap'; +import { SortedMap } from './SortedMap'; +import { SortedSet } from './SortedSet'; import { List } from './List'; import { Set } from './Set'; import { OrderedSet } from './OrderedSet'; @@ -120,6 +122,20 @@ mixin(Collection, { return OrderedMap(this.toKeyedSeq()); }, + toSortedMap(comparator, options) { + // Use Late Binding here to solve the circular dependency. + return SortedMap(this.toKeyedSeq(), comparator, options); + }, + + toSortedSet(comparator, options) { + // Use Late Binding here to solve the circular dependency. + return SortedSet( + isKeyed(this) ? this.valueSeq() : this, + comparator, + options + ); + }, + toOrderedSet() { // Use Late Binding here to solve the circular dependency. return OrderedSet(isKeyed(this) ? this.valueSeq() : this); diff --git a/src/Immutable.js b/src/Immutable.js index 223cb32c65..4fc835555e 100644 --- a/src/Immutable.js +++ b/src/Immutable.js @@ -1,11 +1,13 @@ import { Seq } from './Seq'; import { OrderedMap } from './OrderedMap'; +import { SortedMap } from './SortedMap'; import { List } from './List'; import { Map } from './Map'; import { Stack } from './Stack'; import { OrderedSet } from './OrderedSet'; import { PairSorting } from './PairSorting'; import { Set } from './Set'; +import { SortedSet } from './SortedSet'; import { Record } from './Record'; import { Range } from './Range'; import { Repeat } from './Repeat'; @@ -21,14 +23,17 @@ import { isKeyed } from './predicates/isKeyed'; import { isIndexed } from './predicates/isIndexed'; import { isAssociative } from './predicates/isAssociative'; import { isOrdered } from './predicates/isOrdered'; +import { isSorted } from './predicates/isSorted'; import { isValueObject } from './predicates/isValueObject'; import { isSeq } from './predicates/isSeq'; import { isList } from './predicates/isList'; import { isMap } from './predicates/isMap'; import { isOrderedMap } from './predicates/isOrderedMap'; +import { isSortedMap } from './predicates/isSortedMap'; import { isStack } from './predicates/isStack'; import { isSet } from './predicates/isSet'; import { isOrderedSet } from './predicates/isOrderedSet'; +import { isSortedSet } from './predicates/isSortedSet'; import { isRecord } from './predicates/isRecord'; import { Collection } from './CollectionImpl'; @@ -59,10 +64,12 @@ export default { Seq: Seq, Map: Map, OrderedMap: OrderedMap, + SortedMap: SortedMap, List: List, Stack: Stack, Set: Set, OrderedSet: OrderedSet, + SortedSet: SortedSet, PairSorting: PairSorting, Record: Record, @@ -79,15 +86,18 @@ export default { isIndexed: isIndexed, isAssociative: isAssociative, isOrdered: isOrdered, + isSorted: isSorted, isValueObject: isValueObject, isPlainObject: isPlainObject, isSeq: isSeq, isList: isList, isMap: isMap, isOrderedMap: isOrderedMap, + isSortedMap: isSortedMap, isStack: isStack, isSet: isSet, isOrderedSet: isOrderedSet, + isSortedSet: isSortedSet, isRecord: isRecord, get: get, @@ -116,10 +126,12 @@ export { Seq, Map, OrderedMap, + SortedMap, List, Stack, Set, OrderedSet, + SortedSet, PairSorting, Record, Range, @@ -133,12 +145,14 @@ export { isIndexed, isAssociative, isOrdered, + isSorted, isPlainObject, isValueObject, isSeq, isList, isMap, isOrderedMap, + isSortedMap, isStack, isSet, isOrderedSet, diff --git a/src/List.js b/src/List.js index e512bb90db..0c80a96cf3 100644 --- a/src/List.js +++ b/src/List.js @@ -6,6 +6,7 @@ import { OwnerID, MakeRef, SetRef, + GetRef, wrapIndex, wholeSlice, resolveBegin, @@ -446,7 +447,7 @@ function updateList(list, index, value) { ); } - if (!didAlter.value) { + if (!GetRef(didAlter)) { return list; } diff --git a/src/Map.js b/src/Map.js index 61af7c0a7d..ae2573f9c3 100644 --- a/src/Map.js +++ b/src/Map.js @@ -2,6 +2,7 @@ import { is } from './is'; import { Collection, KeyedCollection } from './Collection'; import { IS_MAP_SYMBOL, isMap } from './predicates/isMap'; import { isOrdered } from './predicates/isOrdered'; +import { isSorted } from './predicates/isSorted'; import { DELETE, SHIFT, @@ -11,6 +12,7 @@ import { OwnerID, MakeRef, SetRef, + GetRef, } from './TrieUtils'; import { hash } from './Hash'; import { Iterator, iteratorValue, iteratorDone } from './Iterator'; @@ -39,7 +41,7 @@ export class Map extends KeyedCollection { // eslint-disable-next-line no-constructor-return return value === undefined || value === null ? emptyMap() - : isMap(value) && !isOrdered(value) + : isMap(value) && !isOrdered(value) && !isSorted(value) ? value : emptyMap().withMutations(map => { const iter = KeyedCollection(value); @@ -651,10 +653,10 @@ function updateMap(map, k, v) { didChangeSize, didAlter ); - if (!didAlter.value) { + if (!GetRef(didAlter)) { return map; } - newSize = map.size + (didChangeSize.value ? (v === NOT_SET ? -1 : 1) : 0); + newSize = map.size + (GetRef(didChangeSize) ? (v === NOT_SET ? -1 : 1) : 0); } if (map.__ownerID) { map.size = newSize; diff --git a/src/Set.js b/src/Set.js index 7b2b3bd569..f79939ca68 100644 --- a/src/Set.js +++ b/src/Set.js @@ -1,5 +1,6 @@ import { Collection, SetCollection, KeyedCollection } from './Collection'; import { isOrdered } from './predicates/isOrdered'; +import { isSorted } from './predicates/isSorted'; import { IS_SET_SYMBOL, isSet } from './predicates/isSet'; import { emptyMap } from './Map'; import { DELETE } from './TrieUtils'; @@ -18,7 +19,7 @@ export class Set extends SetCollection { // eslint-disable-next-line no-constructor-return return value === undefined || value === null ? emptySet() - : isSet(value) && !isOrdered(value) + : isSet(value) && !isOrdered(value) && !isSorted(value) ? value : emptySet().withMutations(set => { const iter = SetCollection(value); diff --git a/src/SortedMap.js b/src/SortedMap.js new file mode 100644 index 0000000000..f56c460043 --- /dev/null +++ b/src/SortedMap.js @@ -0,0 +1,523 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { is } from './is'; +import { KeyedCollection } from './Collection'; +import { IS_SORTED_SYMBOL } from './predicates/isSorted'; +import { isSortedMap } from './predicates/isSortedMap'; +import { DELETE, NOT_SET, MakeRef, GetRef } from './TrieUtils'; +import assertNotInfinite from './utils/assertNotInfinite'; +import { sortFactory } from './Operations'; +import { Map } from './Map'; +import { KeyedSeq } from './Seq'; +import { SortedMapBtreeNodeFactory } from './SortedMapBtreeNode'; + +export class SortedMap extends Map { + // @pragma Construction + + constructor(value, comparator, options) { + if (!comparator) { + if (this instanceof SortedMap) { + comparator = this.getComparator(); + } + if (!comparator) { + comparator = SortedMap.defaultComparator; + } + } + if (!options) { + if (this instanceof SortedMap) { + options = this.getOptions(); + } + if (!options) { + options = SortedMap.defaultOptions; + } + } + + return value === null || value === undefined + ? emptySortedMap(comparator, options) + : isSortedMap(value) && + value.getComparator() === comparator && + value.getOptions() === options + ? value + : emptySortedMap(comparator, options).withMutations(map => { + map.pack(value); + }); + } + + static of(...keyValues) { + return emptySortedMap().withMutations(map => { + for (let i = 0; i < keyValues.length; i += 2) { + if (i + 1 >= keyValues.length) { + throw new Error('Missing value for key: ' + keyValues[i]); + } + map.set(keyValues[i], keyValues[i + 1]); + } + }); + } + + toString() { + return this.__toString('SortedMap {', '}'); + } + + // @pragma Access + + getComparator() { + return this._comparator; + } + + getOptions() { + return this._options; + } + + get(k, notSetValue) { + return this._root ? this._root.get(k, notSetValue) : notSetValue; + } + + entryAt(index) { + return this._root + ? this._root.entryAt(index) + : new Error('index is out of bounds'); + } + + keyAt(index) { + return this._root + ? this._root.keyAt(index) + : new Error('index is out of bounds'); + } + + valueAt(index) { + return this._root + ? this._root.valueAt(index) + : new Error('index is out of bounds'); + } + + // @pragma Modification + + clear() { + if (this.size === 0) { + return this; + } + if (this.__ownerID) { + this.size = 0; + this._root = null; + this.__altered = true; + return this; + } + return emptySortedMap(this._comparator, this._options); + } + + pack(value) { + let collection; + if (value === undefined) { + collection = this; + } else { + // Sort and deduplicate the entries + let index = 0; + const entries = KeyedCollection(value) + .map((v, k) => [k, v, index++]) + .valueSeq() + .toArray(); + if (entries.length === 0) { + if (this.__ownerID) { + this._root = undefined; + (this.size = 0), (this.__altered = true); + return this; + } + return emptySortedMap(this._comparator, this._options); + } + entries.sort((a, b) => this._comparator(a[0], b[0]) || a[2] - b[2]); + const result = []; + for (let i = 0, stop = entries.length - 1; i < stop; i++) { + const entry = entries[i]; + const nextEntry = entries[i + 1]; + if (this._comparator(entry[0], nextEntry[0]) < 0) { + const newEntry = [entry[0], entry[1]]; + result.push(newEntry); + } + } + // push the last ownerID + const entry = entries[entries.length - 1]; + const newEntry = [entry[0], entry[1]]; + result.push(newEntry); + collection = KeyedSeq(result); + } + assertNotInfinite(collection.size); + + const newSize = collection.size; + const newRoot = this._factory + .createPacker() + .pack(this._comparator, this._options, this.__ownerID, collection); + + if (this.__ownerID) { + this._root = newRoot; + (this.size = newSize), (this.__altered = true); + return this; + } + return newRoot + ? makeSortedMap(this._comparator, this._options, newSize, newRoot) + : emptySortedMap(this._comparator, this._options); + } + + set(k, v) { + return updateMap(this, k, v); + } + + remove(k) { + return updateMap(this, k, NOT_SET); + } + + fastRemove(k) { + return updateMap(this, k, NOT_SET, true); + } + + // @pragma Composition + + sort(comparator) { + return SortedMap(this, comparator, this.getOptions()); + } + + sortBy(mapper, comparator) { + return SortedMap( + sortFactory(this, comparator, mapper), + comparator, + this.getOptions() + ); + } + + // @pragma Mutability + + __iterator(type, reverse) { + return this._factory.createIterator(this, type, reverse); + } + + __ensureOwner(ownerID) { + if (ownerID === this.__ownerID) { + return this; + } + if (!ownerID) { + if (this.size === 0) { + return emptySortedMap(this._comparator, this._options); + } + this.__ownerID = ownerID; + this.__altered = false; + return this; + } + return makeSortedMap( + this._comparator, + this._options, + this.size, + this._root, + ownerID + ); + } + + checkConsistency(printFlag) { + if (this._root) { + if (!(this.size > 0)) { + return 1; + } + return this._root.checkConsistency(printFlag); + } else if (!(this.size === 0)) { + return 2; + } + + let n = 0; + let prevKey; + this.keySeq().forEach(key => { + if (n && !(this._comparator(prevKey, key) < 0)) { + return 3; + } + prevKey = key; + n++; + }); + + if (this.size !== n) { + return 4; + } + + return 0; + } + + print(maxDepth) { + let header = 'SORTED MAP: size=' + this.size; + if (this._options) { + header = header + ', options=' + JSON.stringify(this._options); + } + // eslint-disable-next-line + console.log(header); + if (this._root) { + this._root.print(1, maxDepth); + } + return this; + } + + from(key, backwards) { + const self = this; + const sequence = Object.create(KeyedSeq).prototype; + sequence.__iterateUncached = function (fn, reverse) { + if (!self._root) { + return 0; + } + + let iterations = 0; + if (backwards) { + self._root.iterateFromBackwards( + key, + entry => { + iterations++; + return fn(entry[1], entry[0], this); + }, + reverse + ); + } else { + self._root.iterateFrom( + key, + entry => { + iterations++; + return fn(entry[1], entry[0], this); + }, + reverse + ); + } + + return iterations; + }; + + return sequence; + } + + fromIndex(index, backwards) { + const self = this; + const sequence = Object.create(KeyedSeq).prototype; + sequence.__iterateUncached = function (fn, reverse) { + if (reverse) { + throw new Error('fromIndex: reverse mode not supported'); + } + + if (!self._root) { + return 0; + } + + let iterations = 0; + if (backwards) { + self._root.iterateFromIndexBackwards( + index, + entry => { + iterations++; + return fn(entry[1], entry[0], this); + }, + reverse + ); + } else { + self._root.iterateFromIndex( + index, + entry => { + iterations++; + return fn(entry[1], entry[0], this); + }, + reverse + ); + } + + return iterations; + }; + + return sequence; + } +} + +SortedMap.isSortedMap = isSortedMap; + +SortedMap.defaultComparator = defaultComparator; +SortedMap.defaultOptions = { + type: 'btree', +}; + +export const SortedMapPrototype = SortedMap.prototype; +SortedMapPrototype[IS_SORTED_SYMBOL] = true; +SortedMapPrototype[DELETE] = SortedMapPrototype.remove; +SortedMapPrototype.removeIn = SortedMapPrototype.deleteIn; +SortedMapPrototype.removeAll = SortedMapPrototype.deleteAll; + +function makeSortedMap(comparator, options, size, root, ownerID) { + const map = Object.create(SortedMapPrototype); + map._comparator = comparator || SortedMap.defaultComparator; + map._options = options || SortedMap.defaultOptions; + map.size = size; + map._root = root; + map._factory = SortedMap.getFactory(map._options); + map.__ownerID = ownerID; + map.__altered = false; + + if (map._options.btreeOrder && map._options.btreeOrder < 3) { + throw new Error( + 'SortedMap: minimum value of options.btreeOrder is 3, but got: ' + + map._options.btreeOrder + ); + } + + if (!map._factory) { + throw new Error('SortedMap type not supported: ' + map._options.type); + } + + return map; +} + +let DEFAULT_EMPTY_MAP; +export function emptySortedMap(comparator, options) { + if ( + comparator === SortedMap.defaultComparator && + options === SortedMap.defaultOptions + ) { + return ( + DEFAULT_EMPTY_MAP || + (DEFAULT_EMPTY_MAP = makeSortedMap( + SortedMap.defaultComparator, + SortedMap.defaultOptions, + 0 + )) + ); + } + return makeSortedMap(comparator, options, 0); +} + +function updateMap(map, k, v, fast) { + const remove = v === NOT_SET; + const root = map._root; + let newRoot; + let newSize; + if (!root) { + if (remove) { + return map; + } + newSize = 1; + const entries = [[k, v]]; + newRoot = map._factory.createNode( + map._comparator, + map._options, + map.__ownerID, + entries + ); + } else { + const didChangeSize = MakeRef(); + const didAlter = MakeRef(); + + if (remove) { + if (fast) { + newRoot = map._root.fastRemove( + map.__ownerID, + k, + didChangeSize, + didAlter + ); + } else { + newRoot = map._root.remove(map.__ownerID, k, didChangeSize, didAlter); + } + } else { + newRoot = map._root.upsert(map.__ownerID, k, v, didChangeSize, didAlter); + } + if (!GetRef(didAlter)) { + return map; + } + newSize = map.size + (GetRef(didChangeSize) ? (remove ? -1 : 1) : 0); + if (newSize === 0) { + newRoot = undefined; + } + } + if (map.__ownerID) { + map.size = newSize; + map._root = newRoot; + map.__altered = true; + return map; + } + return newRoot + ? makeSortedMap(map._comparator, map._options, newSize, newRoot) + : emptySortedMap(map._comparator, map._options); +} + +export function defaultComparator(a, b) { + if (is(a, b)) { + return 0; + } + + const ta = typeof a; + const tb = typeof b; + + if (ta !== tb) { + return ta < tb ? -1 : 1; + } + + switch (ta) { + case 'undefined': + // we should not get here and is above should take care of this case + break; + case 'object': + // Take care of null cases then convert objects to strings + if (a === null) { + return 1; + } + if (b === null) { + return -1; + } + a = a.toString(); + b = b.toString(); + break; + case 'boolean': + // default comparisons work + break; + case 'number': + // take care of NaN + if (is(a, NaN)) { + return 1; + } + if (is(b, NaN)) { + return -1; + } + // for all other cases the + // default comparisons work + break; + case 'string': + // default comparisons work + break; + case 'symbol': + // convert symbols to strings + a = a.toString(); + b = b.toString(); + break; + case 'function': + // convert functions to strings + a = a.toString(); + b = b.toString(); + break; + default: + // we should not get here as all types are covered + break; + } + + return a < b ? -1 : a > b ? 1 : 0; +} + +// +// Register all the factories +// +SortedMap.getFactory = function (options) { + const type = + options && options.type ? options.type : SortedMap.defaultOptions.type; + + return SortedMap.factories[type]; +}; + +SortedMap.factories = { + btree: new SortedMapBtreeNodeFactory(), +}; diff --git a/src/SortedMapBtreeNode.js b/src/SortedMapBtreeNode.js new file mode 100644 index 0000000000..bb17421a18 --- /dev/null +++ b/src/SortedMapBtreeNode.js @@ -0,0 +1,2526 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable no-else-return */ + +import { NOT_SET, MakeRef, SetRef, GetRef } from './TrieUtils'; +import { Iterator, iteratorValue, iteratorDone } from './Iterator'; +import { + SortedMapNode, + SortedMapPacker, + SortedMapNodeFactory, +} from './SortedMapNode'; +import { KeyedCollection } from './Collection'; +import assertNotInfinite from './utils/assertNotInfinite'; + +const DEFAULT_TYPE = 'btree'; +const DEFAULT_BTREE_ORDER = 33; + +// #pragma Trie Nodes + +class SortedMapBtreeNode extends SortedMapNode { + constructor(comparator, options, ownerID, entries, nodes) { + super(comparator, options, ownerID); + + this.entries = entries; + this.nodes = nodes; + + this.btreeOrder = + options && options.btreeOrder ? options.btreeOrder : DEFAULT_BTREE_ORDER; + this.btreeNodeSplitSize = Math.floor((this.btreeOrder - 1) / 2); + + this._calcSize(); + return this; + } + + _calcSize() { + this.size = 0; + for (let i = 0; i < this.entries.length; i++) { + if (this.entries[i][1] !== NOT_SET) { + this.size++; + } + } + if (this.nodes) { + for (let i = 0; i < this.nodes.length; i++) { + this.size += this.nodes[i].size; + } + } + } + + getComparator() { + return this.comparator; + } + + get(key, notSetValue) { + const entries = this.entries; + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, key, didMatch); + if (GetRef(didMatch)) { + const value = entries[idx][1]; + return value === NOT_SET ? notSetValue : value; + } + + const nodes = this.nodes; + if (nodes) { + const value = nodes[idx].get(key, notSetValue); + return value === NOT_SET ? notSetValue : value; + } + return notSetValue; + } + + entryAt(index) { + const didMatch = MakeRef(); + const subIndex = MakeRef(); + const idx = this.indexSearch(index, didMatch, subIndex); + if (GetRef(didMatch)) { + const entry = this.entries[idx]; + const key = entry[0]; + const value = entry[1]; + return [key, value]; + } + + return this.nodes[idx].entryAt(subIndex.value); + } + + keyAt(index) { + const didMatch = MakeRef(); + const subIndex = MakeRef(); + const idx = this.indexSearch(index, didMatch, subIndex); + if (GetRef(didMatch)) { + const entry = this.entries[idx]; + const key = entry[0]; + return key; + } + + return this.nodes[idx].keyAt(subIndex.value); + } + + valueAt(index) { + const didMatch = MakeRef(); + const subIndex = MakeRef(); + const idx = this.indexSearch(index, didMatch, subIndex); + if (GetRef(didMatch)) { + const entry = this.entries[idx]; + const value = entry[1]; + return value; + } + + return this.nodes[idx].valueAt(subIndex.value); + } + + // Returns first key in this subtree + firstKey() { + const nodes = this.nodes; + if (nodes) { + return nodes[0].firstKey(); + } + + const entries = this.entries; + return entries[0][0]; + } + + // Returns last key in this subtree + lastKey() { + const nodes = this.nodes; + if (nodes) { + return nodes[nodes.length - 1].lastKey(); + } + + const entries = this.entries; + return entries[entries.length - 1][0]; + } + + upsert(ownerID, key, value, didChangeSize, didAlter, outKvn) { + const ret = this._upsert( + ownerID, + key, + value, + didChangeSize, + didAlter, + outKvn + ); + + if (this === ret && GetRef(didChangeSize)) { + this.size++; + } + + return ret; + } + + // + // outKvn is out array with values [[key, value], node] i.e. [entry, node] + // which can be consumed or returned by this operation + // + _upsert(ownerID, key, value, didChangeSize, didAlter, outKvn) { + if (!outKvn) { + // This must be a root case called from SortedMap + const subKvn = []; + + let newRoot = this.upsert( + ownerID, + key, + value, + didChangeSize, + didAlter, + subKvn + ); + + if (subKvn[0]) { + // Make a new root node + const entries = [subKvn[0]]; + const nodes = [newRoot, subKvn[1]]; + newRoot = new SortedMapBtreeNode( + this.comparator, + this.options, + ownerID, + entries, + nodes + ); + } + + return newRoot; + } + + const entries = this.entries; + + // Search keys + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, key, didMatch); + const exists = GetRef(didMatch); + + const nodes = this.nodes; + const canEdit = ownerID && ownerID === this.ownerID; + let newEntries; + let newNodes; + + if (exists) { + // Updating entries + + if (entries[idx][1] === value) { + // + // OPERATION: NONE, same value, no need to update + // + return this; + } else { + // + // OPERATION: UPDATE entry value in entries + // + const entry = [key, value]; + + SetRef(didAlter); + // Updating previously REMOVED ENTRY + if (entries[idx][1] === NOT_SET) { + SetRef(didChangeSize); + } + newEntries = setIn(entries, idx, entry, canEdit); + newNodes = clone(nodes, canEdit); + } + } else { + // Inserting into entries or upserting nodes + + // eslint-disable-next-line no-lonely-if + if (nodes) { + // + // RECURSIVE: UPSERT node recursively + // + const subKvn = []; + + const updatedNode = nodes[idx].upsert( + ownerID, + key, + value, + didChangeSize, + didAlter, + subKvn + ); + + if (GetRef(didAlter)) { + if (subKvn[0]) { + // + // Insert subKvn into this node + // + if (entries.length >= this.btreeOrder - 1) { + return this.splitNode( + idx, + updatedNode, + subKvn, + outKvn, + ownerID, + canEdit + ); + } else { + // + // Insert subKvn into entries and nodes + // + newEntries = spliceIn(entries, idx, subKvn[0], canEdit); + newNodes = spliceIn(nodes, idx + 1, subKvn[1], canEdit); + newNodes[idx] = updatedNode; + } + } else { + // + // No splitting, just setIn the updated subNode + // + newEntries = clone(entries, canEdit); + newNodes = setIn(nodes, idx, updatedNode, canEdit); + } + } else { + // Nothing changed + return this; + } + } else { + // Leaf node + // Insert new entry into entries + const entry = [key, value]; + + SetRef(didAlter); + SetRef(didChangeSize); + + if (entries.length >= this.btreeOrder - 1) { + return this.splitLeaf(idx, entry, outKvn, ownerID, canEdit); + } else { + // + // OPERATION: INSERT new entry into entries + // + newEntries = spliceIn(entries, idx, entry, canEdit); + } + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); + } + + fastRemove(ownerID, key, didChangeSize, didAlter) { + const ret = this._fastRemove(ownerID, key, didChangeSize, didAlter); + + if (this === ret && GetRef(didChangeSize)) { + this.size--; + } + + return ret; + } + + // this version of remove doesn't do any rebalancing + // it just sets the value in an entry to NOT_SET + // this method would be preferable when removing large bulk + // of entres from mutable SortedMap followed by pack() + _fastRemove(ownerID, key, didChangeSize, didAlter) { + const entries = this.entries; + + // Search keys + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, key, didMatch); + const exists = GetRef(didMatch); + + const nodes = this.nodes; + const canEdit = ownerID && ownerID === this.ownerID; + let newEntries; + let newNodes; + + if (exists) { + // Remove entry from entries + if (entries[idx][1] === NOT_SET) { + // the entry has been technically deleted already + return this; + } + SetRef(didAlter); + SetRef(didChangeSize); + const newEntry = [key, NOT_SET]; + newEntries = setIn(entries, idx, newEntry, canEdit); + newNodes = clone(nodes, canEdit); + } else { + // Remove from node + + // eslint-disable-next-line no-lonely-if + if (nodes) { + // RECURSIVE: REMOVE from node recursively + const updatedNode = nodes[idx].fastRemove( + ownerID, + key, + didChangeSize, + didAlter + ); + + if (GetRef(didAlter)) { + // + // No splitting, just setIn the updated subNode + // + newEntries = clone(entries, canEdit); + newNodes = setIn(nodes, idx, updatedNode, canEdit); + } else { + // Nothing changed + return this; + } + } else { + // OPERATION: NONE, key to be removed doesn't exist + return this; + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); + } + + remove(ownerID, key, didChangeSize, didAlter, parent, parentIdx, outKvn) { + const ret = this._remove( + ownerID, + key, + didChangeSize, + didAlter, + parent, + parentIdx, + outKvn + ); + + if (this === ret && GetRef(didChangeSize)) { + this.size--; + } + + return ret; + } + + // + // outKvn is an output array with the following format + // + // [updatedEntry, updatedNode, updatedNodeIsLeft: boolean] + // + // The returned values can be: + // - undefined - means the referenced item didn't change + // - NOT_SET - means the referenced node was merged and has to be removed + // - any other - the referenced item has to be updated with this value + // outKvn[2] is boolean value indicating if node referenced in outKvnp[1] + // is left (true) or right (false) + // + _remove(ownerID, key, didChangeSize, didAlter, parent, parentIdx, outKvn) { + const entries = this.entries; + + // Search keys + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, key, didMatch); + const exists = GetRef(didMatch); + + const nodes = this.nodes; + const canEdit = ownerID && ownerID === this.ownerID; + let newEntries; + let newNodes; + + if (exists) { + // Remove entry from entries + if (nodes) { + // OPERATION: MOVE some entries from neighbors or MERGE with a neighbor + if (entries[idx][1] === NOT_SET) { + // the entry has been technically deleted already + return this; + } + // WORKAROUND: so far let's do the workaround and just update + // the entry in place with NOT_SET + SetRef(didAlter); + SetRef(didChangeSize); + const newEntry = [key, NOT_SET]; + newEntries = setIn(entries, idx, newEntry, canEdit); + newNodes = clone(nodes, canEdit); + } else { + // + // OPERATION: REMOVE entry from the LEAF + // + if (entries[idx][1] === NOT_SET) { + // the entry has been technically deleted already + return this; + } + SetRef(didAlter); + SetRef(didChangeSize); + if (entries.length <= this.btreeNodeSplitSize && parent) { + // we don't have enough items in this leaf to just remove the entry + // we should try borrow an entry from a neighbour or merge this mode with a neighbour + // as a workaround we can just update a value in the entry to NOT_SET + return this.consolidateLeaf( + ownerID, + idx, + parent, + parentIdx, + canEdit, + outKvn + ); + } + // it's ok to physically remove from the LEAF and no moves are needed + // as the node will meet all the consistency rules + newEntries = spliceOut(entries, idx, canEdit); + } + } else { + // Remove from node + + // eslint-disable-next-line no-lonely-if + if (nodes) { + // RECURSIVE: REMOVE from node recursively + const subKvn = [undefined, undefined, undefined]; + const updatedNode = nodes[idx].remove( + ownerID, + key, + didChangeSize, + didAlter, + this, + idx, + subKvn + ); + + if (GetRef(didAlter)) { + // take care of subKvn + return this.spliceNode( + ownerID, + idx, + updatedNode, + parent, + parentIdx, + canEdit, + subKvn, + outKvn + ); + } else { + // Nothing changed + return this; + } + } else { + // OPERATION: NONE, key to be removed doesn't exist in this map + return this; + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); + } + + makeNewNode(newEntries, newNodes, ownerID, canEdit) { + if (newEntries.length === 0) { + if (newNodes) { + // Root node is turning into a leaf + return newNodes[0]; + } else { + // Root node leaf is turning into empty + return; + } + } + + if (canEdit) { + this.entries = newEntries; + this.nodes = newNodes; + this._calcSize(); + return this; + } + return new SortedMapBtreeNode( + this.comparator, + this.options, + ownerID, + newEntries, + newNodes + ); + } + + print(level, maxDepth) { + function w(s) { + process.stdout.write(s); + } + + if (maxDepth && level >= maxDepth) { + return; + } + + const nodes = this.nodes; + const entries = this.entries; + + w(indent(level)); + w('SIZE=' + this.size + '\n'); + if (nodes) { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + w(indent(level)); + if (!node || !(node instanceof SortedMapNode)) { + w( + '+ CORRUPT NODE[' + + i + + '] (L' + + level + + ') ' + + JSON.stringify(node) + + '\n' + ); + } else { + if (node.nodes) { + w('+ NODE[' + i + '] (L' + level + ')\n'); + } else { + w('+ LEAF[' + i + '] (L' + level + ')\n'); + } + node.print(level + 1, maxDepth); + } + if (i < entries.length) { + w(indent(level)); + const entry = entries[i]; + if (!entry) { + w('- CORRUPT ENTRY[' + i + ']: ' + JSON.stringify(entry) + '\n'); + } else if (entry[1] === NOT_SET) { + w('- REMOVED ENTRY[' + i + ']: ' + JSON.stringify(entry[0]) + '\n'); + } else { + w('- ENTRY[' + i + ']: ' + JSON.stringify(entry[0]) + '\n'); + } + } + } + } else { + for (let i = 0; i < entries.length; i++) { + w(indent(level)); + const entry = entries[i]; + if (!entry) { + w('- CORRUPT ENTRY[' + i + ']: ' + JSON.stringify(entry) + '\n'); + } else if (entry[1] === NOT_SET) { + w('- REMOVED ENTRY[' + i + ']: ' + JSON.stringify(entry[0]) + '\n'); + } else { + w('- ENTRY[' + i + ']: ' + JSON.stringify(entry[0]) + '\n'); + } + } + } + } + + checkConsistency(printFlag, level, n, leafLevel) { + function w(f) { + if (printFlag) { + const s = f(); + if (s !== undefined) { + process.stdout.write(indent(level)); + process.stdout.write(s); + } + } + } + + if (!level) { + level = 0; + } + if (!n) { + n = 0; + } + if (!leafLevel) { + leafLevel = [undefined]; + } + + if (this.nodes) { + w(() => '+ Checking NODE[' + n + '] (L' + level + ')\n'); + } else { + w(() => '+ Checking LEAF[' + n + '] (L' + level + ')\n'); + if (leafLevel[0] === undefined) { + leafLevel[0] = level; + } else if (leafLevel[0] !== level) { + failed(112, 'leaves are not on the same level'); + } + } + + function failed(code, msg) { + const s = 'Consistency Check Failed with error code ' + code + ': ' + msg; + if (printFlag) { + w(() => s + '\n'); + return code; + } + + throw new Error(s); + } + + const entries = this.entries; + const nodes = this.nodes; + + if (!entries) { + return failed(101, 'empty entries in a node'); + } + + if (!(entries.length > 0 && entries.length < this.btreeOrder)) { + return failed( + 102, + 'entries length is out of range from 0 to (btreeOrder-1)' + ); + } + + if (level > 0 && !(this.btreeNodeSplitSize <= entries.length)) { + return failed(103, 'entries length is shorter than btreeNodeSplitSize'); + } + + if (nodes && !(nodes.length === entries.length + 1)) { + return failed(104, 'nodes length out of sync with entries length'); + } + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + + if (!entry) return failed(105, 'empty entry'); + + if (!(typeof entry === 'object' && entry instanceof Array)) + return failed(106, 'entry is not Array'); + + if (!(entry.length === 2)) return failed(107, 'entry is not Array[2]'); + + if (entry[1] === NOT_SET) { + w( + () => + ' - Checking REMOVED ENTRY[' + + i + + ']: ' + + JSON.stringify(entry[0]) + + '\n' + ); + if (!nodes) { + failed(113, 'NOT_SET values are not allowed in leaves'); + } + } else { + w( + () => + ' - Checking ENTRY[' + + i + + ']: ' + + JSON.stringify(entry[0]) + + '\n' + ); + } + } + + // Check if all the keys are sorted + for (let i = 0; i < entries.length - 1; i++) { + if (!(this.comparator(entries[i][0], entries[i + 1][0]) < 0)) { + return failed(108, 'the entries are not sorted'); + } + } + + if (nodes) + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if (!node || !(node instanceof SortedMapNode)) + return failed(109, 'empty or corrupt node'); + + // Check the node recursively + const code = node.checkConsistency(printFlag, level + 1, i, leafLevel); + + if (code !== 0) { + return code; + } + + if ( + i > 0 && + !(this.comparator(entries[i - 1][0], node.firstKey()) < 0) + ) { + return failed(110, 'the entry and right node not sorted'); + } + + if ( + i < entries.length && + !(this.comparator(node.lastKey(), entries[i][0]) < 0) + ) { + return failed(111, 'the entry and left node not sorted'); + } + } + + return 0; + } +} // class + +// #pragma Iterators + +SortedMapBtreeNode.prototype.iterate = function (fn, reverse) { + const entries = this.entries; + const nodes = this.nodes; + + if (nodes) { + for (let ii = 0, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const node = nodes[reverse ? maxIndex + 1 - ii : ii]; + if (node.iterate(fn, reverse) === false) { + return false; + } + const entry = entries[reverse ? maxIndex - ii : ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + + // Iterate through the remaining last node + const node = nodes[reverse ? 0 : nodes.length - 1]; + if (node.iterate(fn, reverse) === false) { + return false; + } + } else { + for (let ii = 0, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const entry = entries[reverse ? maxIndex - ii : ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + } + return true; +}; + +SortedMapBtreeNode.prototype.iterateFrom = function (from, fn, reverse) { + if (reverse) { + return this.iterate(entry => { + if (this.comparator(from, entry[0]) <= 0) { + return fn(entry); + } + return true; + }, reverse); + } + + const entries = this.entries; + const nodes = this.nodes; + + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, from, didMatch); + + if (nodes) { + for (let ii = idx, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const node = nodes[ii]; + if (ii === idx && !GetRef(didMatch)) { + if (node.iterateFrom(from, fn, reverse) === false) { + return false; + } + } else if (ii > idx) { + if (node.iterate(fn, reverse) === false) { + return false; + } + } + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + + // Iterate through the remaining last node + const node = nodes[nodes.length - 1]; + if (idx === nodes.length - 1) { + if (node.iterateFrom(from, fn, reverse) === false) { + return false; + } + } else if (node.iterate(fn, reverse) === false) { + return false; + } + } else { + for (let ii = idx, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + } + return true; +}; + +SortedMapBtreeNode.prototype.iterateFromBackwards = function ( + from, + fn, + reverse +) { + if (reverse) { + return this.iterate(entry => { + if (this.comparator(entry[0], from) <= 0) { + return fn(entry); + } + return true; + }, false); + } + + const entries = this.entries; + const nodes = this.nodes; + + const didMatch = MakeRef(); + const idx = binarySearch(this.comparator, entries, from, didMatch); + + if (nodes) { + for (let ii = idx; ii >= 0; ii--) { + if (ii < idx || GetRef(didMatch)) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + const node = nodes[ii]; + if (ii === idx && !GetRef(didMatch)) { + if (node.iterateFromBackwards(from, fn, reverse) === false) { + return false; + } + } else if (node.iterate(fn, true) === false) { + return false; + } + } + } else { + for (let ii = GetRef(didMatch) ? idx : idx - 1; ii >= 0; ii--) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + } + return true; +}; + +SortedMapBtreeNode.prototype.iterateFromIndex = function (index, fn) { + const entries = this.entries; + const nodes = this.nodes; + + const didMatch = MakeRef(); + const subIndex = MakeRef(); + const idx = this.indexSearch(index, didMatch, subIndex); + + if (nodes) { + for (let ii = idx, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const node = nodes[ii]; + if (ii === idx && !GetRef(didMatch)) { + if (node.iterateFromIndex(subIndex.value, fn) === false) { + return false; + } + } else if (ii > idx) { + if (node.iterate(fn, false) === false) { + return false; + } + } + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + + // Iterate through the remaining last node + const node = nodes[nodes.length - 1]; + if (idx === nodes.length - 1) { + if (node.iterateFromIndex(subIndex.value, fn) === false) { + return false; + } + } else if (node.iterate(fn, false) === false) { + return false; + } + } else { + for (let ii = idx, maxIndex = entries.length - 1; ii <= maxIndex; ii++) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + } + return true; +}; + +SortedMapBtreeNode.prototype.iterateFromIndexBackwards = function (index, fn) { + const entries = this.entries; + const nodes = this.nodes; + + const didMatch = MakeRef(); + const subIndex = MakeRef(); + const idx = this.indexSearch(index, didMatch, subIndex); + + if (nodes) { + for (let ii = idx; ii >= 0; ii--) { + if (ii < idx || GetRef(didMatch)) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + const node = nodes[ii]; + if (ii === idx && !GetRef(didMatch)) { + if (node.iterateFromIndexBackwards(subIndex.value, fn) === false) { + return false; + } + } else if (node.iterate(fn, true) === false) { + return false; + } + } + } else { + for (let ii = GetRef(didMatch) ? idx : idx - 1; ii >= 0; ii--) { + const entry = entries[ii]; + if (entry[1] === NOT_SET) { + continue; + } + if (fn(entry) === false) { + return false; + } + } + } + return true; +}; + +class SortedMapBtreeNodeIterator extends Iterator { + constructor(map, type, reverse) { + this._type = type; + this._reverse = reverse; + this._stack = map._root && mapIteratorFrame(map._root); + } + + next() { + const type = this._type; + let stack = this._stack; + while (stack) { + const node = stack.node; + let index = stack.index++; + if (node.nodes) { + const maxIndex = node.entries.length + node.nodes.length - 1; + if (index <= maxIndex) { + if (index % 2 === 0) { + index /= 2; + const subNode = + node.nodes[this._reverse ? node.nodes.length - 1 - index : index]; + if (subNode) { + stack = this._stack = mapIteratorFrame(subNode, stack); + } + continue; + } else { + index = (index - 1) / 2; + const entry = + node.entries[ + this._reverse ? node.entries.length - 1 - index : index + ]; + if (entry[1] === NOT_SET) { + continue; + } + return mapIteratorValue(type, entry); + } + } + } else { + // node.entries + const maxIndex = node.entries.length - 1; + if (index <= maxIndex) { + const entry = node.entries[this._reverse ? maxIndex - index : index]; + if (entry[1] === NOT_SET) { + continue; + } + return mapIteratorValue(type, entry); + } + } + stack = this._stack = this._stack.__prev; + } + return iteratorDone(); + } +} + +function mapIteratorValue(type, entry) { + return iteratorValue(type, entry[0], entry[1]); +} + +function mapIteratorFrame(node, prev) { + return { + node: node, + index: 0, + __prev: prev, + }; +} + +// +// Array manipulation algorithms +// + +function allocArray(n) { + const a = new Array(n); + return a; +} + +const _indentStr = new Array(120).join(' '); + +function indent(level) { + let indentCnt = 4 * level; + if (indentCnt > _indentStr.length) { + indentCnt = _indentStr.length; + } + return _indentStr.substring(0, indentCnt); +} + +function clone(array, canEdit) { + if (array === undefined) { + return array; + } + if (canEdit) { + return array; + } + + const newLen = array.length; + const newArray = allocArray(newLen); + for (let ii = 0; ii < newLen; ii++) { + newArray[ii] = array[ii]; + } + return newArray; +} + +function setIn(array, idx, val, canEdit) { + if (canEdit) { + array[idx] = val; + return array; + } + + const newLen = array.length; + const newArray = allocArray(newLen); + for (let ii = 0; ii < idx; ii++) { + newArray[ii] = array[ii]; + } + newArray[idx] = val; + for (let ii = idx + 1; ii < newLen; ii++) { + newArray[ii] = array[ii]; + } + return newArray; +} + +function spliceIn(array, idx, val, canEdit) { + const newLen = array.length + 1; + + if (canEdit) { + // Have to shift items going backwards + for (let ii = newLen - 1, stop = idx + 1; ii >= stop; ii--) { + array[ii] = array[ii - 1]; + } + array[idx] = val; + return array; + } + + const newArray = allocArray(newLen); + for (let ii = 0; ii < idx; ii++) { + newArray[ii] = array[ii]; + } + newArray[idx] = val; + for (let ii = idx + 1; ii < newLen; ii++) { + newArray[ii] = array[ii - 1]; + } + return newArray; +} + +// eslint-disable-next-line no-unused-vars +function spliceInN(array, idx, n, valArray, canEdit) { + const newLen = array.length + n; + + if (canEdit) { + // Have to shift items going backwards + for (let ii = newLen - 1, stop = idx + n; ii >= stop; ii--) { + array[ii] = array[ii - n]; + } + for (let ii = 0; ii < n; ii++) { + array[idx + ii] = valArray[ii]; + } + return array; + } + + const newArray = allocArray(newLen); + for (let ii = 0; ii < idx; ii++) { + newArray[ii] = array[ii]; + } + for (let ii = 0; ii < n; ii++) { + newArray[idx + ii] = valArray[ii]; + } + for (let ii = idx + n; ii < newLen; ii++) { + newArray[ii] = array[ii - n]; + } + return newArray; +} + +function spliceOut(array, idx, canEdit) { + const newLen = array.length - 1; + + if (canEdit) { + for (let ii = idx; ii < newLen; ii++) { + array[ii] = array[ii + 1]; + } + array.length = newLen; + return array; + } + + const newArray = allocArray(newLen); + for (let ii = 0; ii < idx; ii++) { + newArray[ii] = array[ii]; + } + for (let ii = idx; ii < newLen; ii++) { + newArray[ii] = array[ii + 1]; + } + return newArray; +} + +function spliceOutN(array, idx, n, canEdit) { + const newLen = array.length - n; + + if (canEdit) { + for (let ii = idx; ii < newLen; ii++) { + array[ii] = array[ii + n]; + } + array.length = newLen; + return array; + } + + const newArray = allocArray(newLen); + for (let ii = 0; ii < idx; ii++) { + newArray[ii] = array[ii]; + } + for (let ii = idx; ii < newLen; ii++) { + newArray[ii] = array[ii + n]; + } + return newArray; +} + +// +// Example: spliceOutShiftRightN(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3, 2, canEdit) +// +// removes item at index=3 ('d') and moves all remaining items in an awway right by 2 positions +// +// Result: [?, ?, 'a', 'b', 'c', 'f', 'g'] +// +function spliceOutShiftRightN(array, idx, rightN, canEdit) { + const newLen = array.length - 1 + rightN; + let newArray; + + if (canEdit) { + array.length = newLen; + newArray = array; + } else { + newArray = allocArray(newLen); + } + + for (let ii = newLen - 1, stop = idx + rightN; ii >= stop; ii--) { + newArray[ii] = array[ii - rightN + 1]; + } + for (let ii = idx + rightN - 1; ii >= rightN; ii--) { + newArray[ii] = array[ii - rightN]; + } + return newArray; +} + +// +// First: setIn(array, setInIdx, setInValue) +// Then: spliceOut(array, spliceOutIdx) +// Optimized, eliminating redundant copying +// Equivalent of setInSpliceOutN(array, setInIdx, setInValue, spliceOutIdx, 1, canEdit) +// +// Example: setInSpliceOut(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3, 'D', 1, canEdit) +// +// Result: ['a', 'c', 'D', 'f', 'g'] +// +function setInSpliceOut(array, setInIdx, setInValue, spliceOutIdx, canEdit) { + const newArray = spliceOut(array, spliceOutIdx, canEdit); + + // Now we can edit regardless of canEdit + if (setInIdx < spliceOutIdx) { + newArray[setInIdx] = setInValue; + } else if (setInIdx > spliceOutIdx) { + newArray[setInIdx - 1] = setInValue; + } + + return newArray; +} + +// +// First: setIn(array, setInIdx, setInValue) +// Then: spliceOut(array, spliceOutIdx) +// Optimized, eliminating redundant copying +// +// Example: setInSpliceOut(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3, 'D', 1, 2, canEdit) +// +// Result: ['a', 'D', 'f', 'g'] +// +// eslint-disable-next-line no-unused-vars +function setInSpliceOutN( + array, + setInIdx, + setInValue, + spliceOutIdx, + n, + canEdit +) { + const newArray = spliceOutN(array, spliceOutIdx, n, canEdit); + + // Now we can edit regardless of canEdit + if (setInIdx < spliceOutIdx) { + newArray[setInIdx] = setInValue; + } else if (setInIdx >= spliceOutIdx + n) { + newArray[setInIdx - n] = setInValue; + } + + return newArray; +} + +function binarySearch(comparator, entries, key, didMatch) { + let first = 0; + let range = entries.length; + + while (range > 0) { + const half = Math.floor(range / 2); + const entry = entries[first + half]; + const entryKey = entry[0]; + const cmp = comparator(key, entryKey); + if (cmp === 0) { + SetRef(didMatch); + return first + half; + } + if (cmp > 0) { + first += half + 1; + range -= half + 1; + } else { + range = half; + } + } + return first; +} + +SortedMapBtreeNode.prototype.indexSearch = function ( + index, + didMatch, + subIndex +) { + if (index < 0 || index >= this.size) { + throw new Error( + 'BtreeNode.indexSearch: index is out of bounds: ' + + index + + ' vs ' + + this.size + ); + } + const entries = this.entries; + const nodes = this.nodes; + + for (let i = 0; i < entries.length; i++) { + if (nodes) { + const node = nodes[i]; + if (index < node.size) { + subIndex.value = index; + return i; + } + index -= node.size; + } + + const entry = entries[i]; + if (entry[1] !== NOT_SET) { + if (index === 0) { + SetRef(didMatch); + return i; + } + index--; + } + } + + if (nodes) { + const node = nodes[nodes.length - 1]; + if (index < node.size) { + subIndex.value = index; + return nodes.length - 1; + } + } + + throw new Error('BtreeNode.indexSearch: inconsistent node size'); +}; + +// +// Node Split algorithms +// +SortedMapBtreeNode.prototype.splitNode = function ( + idx, + updatedNode, + subKvn, + outKvn, + ownerID, + canEdit +) { + const entries = this.entries; + const nodes = this.nodes; + const medianIdx = this.btreeNodeSplitSize; + + let newEntries; + let newNodes; + + if (idx < medianIdx) { + const rightEntries = entries.slice(medianIdx, entries.length); + const rightNodes = nodes.slice(medianIdx, nodes.length); + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries, + rightNodes + ); + + outKvn[0] = entries[medianIdx - 1]; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + nodes.length = medianIdx + 1; + + // shift the items right to make room for returned Kvn + // and updatedNode (has to go backwards) + for (let i = medianIdx - 1; i >= idx + 1; i--) { + entries[i] = entries[i - 1]; + nodes[i + 1] = nodes[i]; + } + + // place returned Kvn and updated node into entries and nodes + entries[idx] = subKvn[0]; + nodes[idx] = updatedNode; + nodes[idx + 1] = subKvn[1]; + newEntries = entries; + newNodes = nodes; + } else { + // allocate new arrays for entries and nodes + newEntries = allocArray(medianIdx); + newNodes = allocArray(medianIdx + 1); + + // copy the items before idx into new arrays + for (let i = 0; i < idx; i++) { + newEntries[i] = entries[i]; + newNodes[i] = nodes[i]; + } + + // place returned Kvn and updated node into new arrays + newEntries[idx] = subKvn[0]; + newNodes[idx] = updatedNode; + newNodes[idx + 1] = subKvn[1]; + + // copy remaining items after idx into new arrays + for (let i = idx + 1; i < medianIdx; i++) { + newEntries[i] = entries[i - 1]; + newNodes[i + 1] = nodes[i]; + } + } + } else if (idx === medianIdx) { + // allocate the arrays for right node + const rightEntries = allocArray(entries.length - medianIdx); + const rightNodes = allocArray(nodes.length - medianIdx); + + // place subKvn to the beginning of right node arrays + rightEntries[0] = entries[medianIdx]; + rightNodes[0] = subKvn[1]; + + // copy the remaining items into the right node arrays + for (let i = 1, len = rightEntries.length; i < len; i++) { + rightEntries[i] = entries[medianIdx + i]; + rightNodes[i] = nodes[medianIdx + i]; + } + // copy the last node item into rightNodes + rightNodes[rightNodes.length - 1] = nodes[nodes.length - 1]; + + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries, + rightNodes + ); + + outKvn[0] = subKvn[0]; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + nodes.length = medianIdx + 1; + nodes[idx] = updatedNode; + newEntries = entries; + newNodes = nodes; + } else { + // allocate new arrays for entries and nodes + newEntries = allocArray(medianIdx); + newNodes = allocArray(medianIdx + 1); + + // copy the items before idx into new arrays + for (let i = 0; i < medianIdx; i++) { + newEntries[i] = entries[i]; + newNodes[i] = nodes[i]; + } + + // place returned Kvn and updated node into new node arrays + newNodes[idx] = updatedNode; + } + } else { + // idx > medianIdx + + // allocate the arrays for right node + const rightEntries = allocArray(entries.length - medianIdx); + const rightNodes = allocArray(nodes.length - medianIdx); + + // copy the items into the beginning of right node arrays + const idx0 = medianIdx + 1; + const rightIdx = idx - idx0; + for (let i = 0, len = rightIdx; i < len; i++) { + rightEntries[i] = entries[idx0 + i]; + rightNodes[i] = nodes[idx0 + i]; + } + + // place subKvn to the middle right node arrays + rightEntries[rightIdx] = subKvn[0]; + rightNodes[rightIdx] = updatedNode; + rightNodes[rightIdx + 1] = subKvn[1]; + + // copy the remaining items into the right node arrays + for (let i = rightIdx + 1, len = rightEntries.length; i < len; i++) { + rightEntries[i] = entries[medianIdx + i]; + rightNodes[i + 1] = nodes[medianIdx + i + 1]; + } + + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries, + rightNodes + ); + + outKvn[0] = entries[medianIdx]; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + nodes.length = medianIdx + 1; + newEntries = entries; + newNodes = nodes; + } else { + // allocate new arrays for entries and nodes + newEntries = entries.slice(0, medianIdx); + newNodes = nodes.slice(0, medianIdx + 1); + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); +}; + +SortedMapBtreeNode.prototype.splitLeaf = function ( + idx, + entry, + outKvn, + ownerID, + canEdit +) { + const entries = this.entries; + const medianIdx = this.btreeNodeSplitSize; + + let newEntries; + let newNodes; + + if (idx < medianIdx) { + const rightEntries = entries.slice(medianIdx, entries.length); + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries + ); + + outKvn[0] = entries[medianIdx - 1]; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + + // shift the items right to make room for returned Kvn + // and updatedNode (has to go backwards) + for (let i = medianIdx - 1; i >= idx + 1; i--) { + entries[i] = entries[i - 1]; + } + + // place returned Kvn and updated node into entries and nodes + entries[idx] = entry; + newEntries = entries; + } else { + // allocate new arrays for entries and nodes + newEntries = allocArray(medianIdx); + + // copy the items before idx into new arrays + for (let i = 0; i < idx; i++) { + newEntries[i] = entries[i]; + } + + // place returned Kvn and updated node into new arrays + newEntries[idx] = entry; + + // copy remaining items after idx into new arrays + for (let i = idx + 1; i < medianIdx; i++) { + newEntries[i] = entries[i - 1]; + } + } + } else if (idx === medianIdx) { + // allocate the arrays for right node + const rightEntries = allocArray(entries.length - medianIdx); + + // place subKvn to the beginning of right node arrays + rightEntries[0] = entries[medianIdx]; + + // copy the remaining items into the right node arrays + for (let i = 1, len = rightEntries.length; i < len; i++) { + rightEntries[i] = entries[medianIdx + i]; + } + + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries + ); + + outKvn[0] = entry; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + newEntries = entries; + } else { + // allocate new arrays for entries + newEntries = allocArray(medianIdx); + + // copy the items before idx into new arrays + for (let i = 0; i < medianIdx; i++) { + newEntries[i] = entries[i]; + } + } + } else { + // idx > medianIdx + + // allocate the arrays for right node + const rightEntries = allocArray(entries.length - medianIdx); + + // copy the items into the beginning of right node arrays + const idx0 = medianIdx + 1; + const rightIdx = idx - idx0; + for (let i = 0, len = rightIdx; i < len; i++) { + rightEntries[i] = entries[idx0 + i]; + } + + // place subKvn to the middle right node arrays + rightEntries[rightIdx] = entry; + + // copy the remaining items into the right node arrays + for (let i = rightIdx + 1, len = rightEntries.length; i < len; i++) { + rightEntries[i] = entries[medianIdx + i]; + } + + const rightNode = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + rightEntries + ); + + outKvn[0] = entries[medianIdx]; + outKvn[1] = rightNode; + + if (canEdit) { + // truncate existing entries and nodes + entries.length = medianIdx; + newEntries = entries; + } else { + // allocate new arrays for entries and nodes + newEntries = entries.slice(0, medianIdx); + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); +}; + +SortedMapBtreeNode.prototype.spliceNode = function ( + ownerID, + idx, + updatedNode, + parent, + parentIdx, + canEdit, + subKvn, + outKvn +) { + const entries = this.entries; + const nodes = this.nodes; + + let newEntries; + let newNodes; + + const updatedEntry = subKvn[0]; + const updatedNeighbor = subKvn[1]; + const updatedNeighborIsLeft = subKvn[2]; + + if (updatedNeighbor === NOT_SET) { + // + // Removing entry and node + // + if (entries.length <= this.btreeNodeSplitSize && parent) { + // Not enough room, consolidate this node + if (updatedNeighborIsLeft) { + // remove left node in newNodes + return this.consolidateNode( + ownerID, + idx, + updatedNode, + idx - 1, + idx - 1, + parent, + parentIdx, + canEdit, + outKvn + ); + } else { + // remove right node in newNodes + return this.consolidateNode( + ownerID, + idx, + updatedNode, + idx, + idx + 1, + parent, + parentIdx, + canEdit, + outKvn + ); + } + } else { + // eslint-disable-next-line no-lonely-if + if (updatedNeighborIsLeft) { + // update left node in newNodes + newNodes = setInSpliceOut(nodes, idx, updatedNode, idx - 1, canEdit); + newEntries = spliceOut(entries, idx - 1, canEdit); + } else { + // update right node in newNodes + newNodes = setInSpliceOut(nodes, idx, updatedNode, idx + 1, canEdit); + newEntries = spliceOut(entries, idx, canEdit); + } + } + } else { + // + // Updating entry and node + // + newNodes = setIn(nodes, idx, updatedNode, canEdit); + if (updatedNeighbor) { + if (updatedNeighborIsLeft) { + // update left node in newNodes + newNodes[idx - 1] = updatedNeighbor; + newEntries = setIn(entries, idx - 1, updatedEntry, canEdit); + } else { + // update right node in newNodes + newNodes[idx + 1] = updatedNeighbor; + newEntries = setIn(entries, idx, updatedEntry, canEdit); + } + } else if (updatedEntry) { + newEntries = setIn(entries, idx, updatedEntry, canEdit); + } else { + newEntries = clone(entries, canEdit); + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); +}; + +// +// We updating node at position idx, removing the entry at position removeEntryIdx, +// and removing a neighbor node at position removeNodeIdx +// (either on the left or right side of updated node). We know that we already have +// a minimum number of allowed entries in the node, so we have to either +// move some entries from a neighbor or merge with neighbour +// +SortedMapBtreeNode.prototype.consolidateNode = function ( + ownerID, + idx, + updatedNode, + removeEntryIdx, + removeNodeIdx, + parent, + parentIdx, + canEdit, + outKvn +) { + const entries = this.entries; + const nodes = this.nodes; + + const parentEntries = parent.entries; + const parentNodes = parent.nodes; + + // + // Decide if we are going to move entries or merge + // and with which neighbor we are going to proceed + // + let donorNode; + let mergeNode; + let leftNode; + let rightNode; + if (parentIdx === 0) { + // Only right node can be a host within a scope of this parent + rightNode = parentNodes[parentIdx + 1]; + mergeNode = donorNode = rightNode; + } else if (parentIdx === parentNodes.length - 1) { + // Only left node can be a host within a scope of this parent + leftNode = parentNodes[parentIdx - 1]; + mergeNode = donorNode = leftNode; + } else { + // Both left and right node could be a potential donor + leftNode = parentNodes[parentIdx - 1]; + rightNode = parentNodes[parentIdx + 1]; + const leftAvail = + (leftNode.entries.length - this.btreeNodeSplitSize + 1) / 2; + const rightAvail = + (rightNode.entries.length - this.btreeNodeSplitSize + 1) / 2; + if (leftAvail >= rightAvail) { + donorNode = leftNode; + mergeNode = rightNode; + } else { + donorNode = rightNode; + mergeNode = leftNode; + } + } + + let newEntries; + let newNodes; + + // + // Move from the LEFT node + // + function moveFromLeftNode(node, n, merge) { + // allocate newEntries extended by n + newEntries = spliceOutShiftRightN(entries, removeEntryIdx, n, canEdit); + newNodes = spliceOutShiftRightN(nodes, removeNodeIdx, n, canEdit); + + // now set the updatedNode, adjust the index according to the shift above + const uIdx = idx < removeNodeIdx ? idx + n : idx + n - 1; + newNodes[uIdx] = updatedNode; + + // Then move an item from the parent node into newEntries + let i = n - 1; + newEntries[i] = parentEntries[parentIdx - 1]; + + // And move rightest node from the neighbor into newNodes + newNodes[i--] = node.nodes[node.nodes.length - 1]; + + // Then copy the items from the node + let j; + for (j = node.entries.length - 1; i >= 0; i--, j--) { + newEntries[i] = node.entries[j]; + newNodes[i] = node.nodes[j]; + } + + if (merge) { + outKvn[1] = NOT_SET; + } else { + // Last, copy the remaining entry from node to parent + outKvn[0] = node.entries[j]; + + // Make a copy of donor's node without donated entries + const newNodeEntries = spliceOutN( + node.entries, + node.entries.length - n, + n, + canEdit + ); + const newNodeNodes = spliceOutN( + node.nodes, + node.nodes.length - n, + n, + canEdit + ); + + outKvn[1] = node.makeNewNode( + newNodeEntries, + newNodeNodes, + ownerID, + canEdit + ); + } + outKvn[2] = true; + } + + // + // Move from the right node + // + function moveFromRightNode(node, n, merge) { + newEntries = spliceOut(entries, removeEntryIdx, canEdit); + newNodes = spliceOut(nodes, removeNodeIdx, canEdit); + + // Expand new entries + let j = newEntries.length; + newEntries.length += n; + newNodes.length += n; + + // now set the updatedNode, adjust the index according to the shift above + const uIdx = idx < removeNodeIdx ? idx : idx - 1; + newNodes[uIdx] = updatedNode; + + // Then move an item from the parent node into newEntries + newEntries[j++] = parentEntries[parentIdx]; + + // Also copy the first item in right neighbor into newNodes + newNodes[j] = node.nodes[0]; + + // Then copy the items from the node + for (let i = 0, iLimit = n - 1; i < iLimit; i++) { + newEntries[j + i] = node.entries[i]; + newNodes[j + i + 1] = node.nodes[i + 1]; + } + + if (merge) { + outKvn[1] = NOT_SET; + } else { + // Last, copy the remaining item from node to parent + outKvn[0] = node.entries[n - 1]; + + // Make a copy of donor's node without donated entries + const newNodeEntries = spliceOutN(node.entries, 0, n, canEdit); + const newNodeNodes = spliceOutN(node.nodes, 0, n, canEdit); + + outKvn[1] = node.makeNewNode( + newNodeEntries, + newNodeNodes, + ownerID, + canEdit + ); + } + outKvn[2] = false; + } + + const donorAvail = Math.floor( + (donorNode.entries.length - this.btreeNodeSplitSize + 1) / 2 + ); + if (donorAvail > 0) { + // + // OPERATION: MOVE + // + // move donorAvail entries from donorNode to this leaf through parentNodes + if (donorNode === leftNode) { + moveFromLeftNode(donorNode, donorAvail); + } else { + moveFromRightNode(donorNode, donorAvail); + } + } else { + // + // OPERATION: MERGE + // + // neither neighbour has enough entries to donate + // we gotta merge this node with mergeNode which has fewer entries available + // eslint-disable-next-line no-lonely-if + if (mergeNode === leftNode) { + // Merge with the left node + moveFromLeftNode(mergeNode, mergeNode.entries.length + 1, true); + } else { + // Merge with the right node + moveFromRightNode(mergeNode, mergeNode.entries.length + 1, true); + } + } + + return this.makeNewNode(newEntries, newNodes, ownerID, canEdit); +}; + +// We are eliminating the entry at position idx and we know that we already +// have a minimum number of allowed entries in the node, so we have to either +// move some entries from a neighbor or merge with neighbour +SortedMapBtreeNode.prototype.consolidateLeaf = function ( + ownerID, + idx, + parent, + parentIdx, + canEdit, + outKvn +) { + const entries = this.entries; + const parentEntries = parent.entries; + const parentNodes = parent.nodes; + + // + // Decide if we are going to move entries or merge + // and with which neighbor we are going to proceed + // + let donorNode; + let leftNode; + // eslint-disable-next-line no-unused-vars + let rightNode; + if (parentIdx === 0) { + // Only right node can be a host within a scope of this parent + rightNode = parentNodes[parentIdx + 1]; + donorNode = rightNode; + } else if (parentIdx === parentNodes.length - 1) { + // Only left node can be a host within a scope of this parent + leftNode = parentNodes[parentIdx - 1]; + donorNode = leftNode; + } else { + // Both left and right node could be a potential donor + leftNode = parentNodes[parentIdx - 1]; + rightNode = parentNodes[parentIdx + 1]; + const leftAvail = leftNode.entries.length - this.btreeNodeSplitSize; + const rightAvail = rightNode.entries.length - this.btreeNodeSplitSize; + if (leftAvail >= rightAvail) { + donorNode = leftNode; + } else { + donorNode = rightNode; + } + } + + let newEntries; + // + // Move from the LEFT node + // + // n - is the number of entries added to the target node + // + function moveFromLeftNode(node, n, merge) { + // allocate newEntries extended by n + newEntries = spliceOutShiftRightN(entries, idx, n, canEdit); + + // m is number of entries to be moved from donor node + let m = n; + if (!parentNotSet) { + // Move an item from the parent node into newEntries + newEntries[n - 1] = parentEntry; + m--; + } + + // Then copy the items from the node + for (let i = 0; i < m; i++) { + newEntries[i] = node.entries[node.entries.length - m + i]; + } + + if (merge) { + outKvn[1] = NOT_SET; + } else { + // Last, copy the remaining item from node to parent + m++; + outKvn[0] = node.entries[node.entries.length - m]; + + // Make a copy of donor's node without donated entries + const newNodeEntries = spliceOutN( + node.entries, + node.entries.length - m, + m, + canEdit + ); + + outKvn[1] = node.makeNewNode(newNodeEntries, undefined, ownerID, canEdit); + } + outKvn[2] = true; + } + + // + // Move from the right node + // + // n - is the number of entries added to the target node + // + function moveFromRightNode(node, n, merge) { + newEntries = spliceOut(entries, idx, canEdit); + // Expand new entries + let j = newEntries.length; + newEntries.length += n; + + // m is number of entries to be moved from donor node + let m = n; + if (!parentNotSet) { + // Move an item from the parent node into newEntries + newEntries[j++] = parentEntry; + m--; + } + + // Then copy the items from the node + for (let i = 0; i < m; i++) { + newEntries[j + i] = node.entries[i]; + } + + if (merge) { + outKvn[1] = NOT_SET; + } else { + // Last, copy the remaining item from node to parent + outKvn[0] = node.entries[m++]; + + // Make a copy of donor's node without donated entries + const newNodeEntries = spliceOutN(node.entries, 0, m, canEdit); + + outKvn[1] = node.makeNewNode(newNodeEntries, undefined, ownerID, canEdit); + } + outKvn[2] = false; + } + + const parentEntry = + donorNode === leftNode + ? parentEntries[parentIdx - 1] + : parentEntries[parentIdx]; + const parentNotSet = parentEntry[1] === NOT_SET; + const parentAdj = parentNotSet ? 1 : 0; + const donorAvail = + donorNode.entries.length - this.btreeNodeSplitSize - parentAdj; + if (donorAvail > 0) { + // + // OPERATION: MOVE + // + // move donorAvail entries from donorNode to this leaf through parentNodes + const n = Math.floor((donorAvail + 1) / 2); + if (donorNode === leftNode) { + moveFromLeftNode(donorNode, n); + } else { + moveFromRightNode(donorNode, n); + } + } else { + // + // OPERATION: MERGE + // + // neither neighbour has enough entries to donate + // we gotta merge this node with donorNode + const n = donorNode.entries.length + 1 - parentAdj; + if (donorNode === leftNode) { + // Merge with the left node + moveFromLeftNode(donorNode, n, true); + } else { + // Merge with the right node + moveFromRightNode(donorNode, n, true); + } + } + + return this.makeNewNode(newEntries, undefined, ownerID, canEdit); +}; + +class SortedMapBtreeNodePacker extends SortedMapPacker { + calcPlanCnt(order, height) { + if (height < 1 || height > 20) { + throw new Error('Height is out of supported limit'); + } + + // The recursive algorithm would be: + // + // if(height <= 1) { + // return order - 1; + // } + // return order * this.calcPlanCnt(order, height - 1) + (order - 1); + + let n = order - 1; + + for (let h = 1; h < height; h++) { + n = n * order + (order - 1); + } + + return n; + } + + prepareCachedPlan(order, n) { + const key = order.toString() + ' ' + n.toString(); + + const cachedPlan = SortedMapBtreeNodePacker.cache[key]; + + if (cachedPlan) { + return cachedPlan; + } + + const plan = this.preparePlan(order, n); + this.verifyPlan(plan); + + if ( + order < 100 && + n <= 100 && + n >= order && + SortedMapBtreeNodePacker.cacheSize < 500 + ) { + SortedMapBtreeNodePacker.cache[key] = plan; + SortedMapBtreeNodePacker.cacheSize++; + } + + return plan; + } + + preparePlan(order, n) { + // + // First determine height of the tree we are building + // + const order1 = order - 1; + let height = 1; + let maxEntriesCnt = order1; + let maxEntriesCnt1; + while (maxEntriesCnt < n) { + maxEntriesCnt1 = maxEntriesCnt; + maxEntriesCnt = maxEntriesCnt * order + order1; + height++; + } + + if (maxEntriesCnt === n) { + // Exact match for the full tree + return { + op: 'build', + full: true, + height: height, + order: order, + repeat: 1, + total: n, + }; + } + + if (height === 1) { + return { + op: 'build', + full: false, + height: height, + order: order, + repeat: 1, + total: n, + }; + } + + // + // Number of entries in subtrees of (height - 1) + // + const planCnt1 = maxEntriesCnt1; + + // + // Then determine the root order + // + const rootOrder = 1 + Math.floor(n / (planCnt1 + 1)); + + if (rootOrder < 2) { + throw new Error( + 'Something is wrong, the rootOrder is expected to be >= 2' + ); + } + + if (rootOrder * planCnt1 + (rootOrder - 1) === n) { + const repeat = rootOrder; + const repPlan = []; + const total = repeat * planCnt1 + repeat - 1; + repPlan.push({ + op: 'build', + full: true, + height: height - 1, + order: order, + repeat: rootOrder, + total: total, + }); + return { + op: 'assemble', + height: height, + order: order, + total: total, + items: repPlan, + }; + } + + // We have to adjust last two subtrees + const plan = []; + + if (rootOrder > 2) { + const repeat = rootOrder - 2; + const total = repeat * planCnt1 + repeat - 1; + const build = { + op: 'build', + full: true, + height: height - 1, + order: order, + repeat: repeat, + total: total, + }; + plan.push(build); + n -= total; + n--; + } + + // Find feasible plan for 2 subtrees and n entries + n--; // 1 more entry will be in between the two subtrees + const n2 = Math.floor(n / 2); + if (n - n2 > 0) { + plan.push(this.prepareCachedPlan(order, n - n2)); + } + if (n2 > 0) { + plan.push(this.prepareCachedPlan(order, n2)); + } + + let total = 0; + const ilen = plan.length; + for (let i = 0; i < ilen; i++) { + total += plan[i].total; + } + total += plan.length - 1; + + return { + op: 'assemble', + height: height, + order: order, + total: total, + items: plan, + }; + } + + verifyPlan(plan, level) { + function failed(msg) { + throw new Error(msg); + } + + if (level === undefined) { + level = 0; + } + + if (plan.op === 'assemble') { + let cnt = 0; + + const ilen = plan.items.length; + for (let i = 0; i < ilen; i++) { + const pl = plan.items[i]; + cnt += pl.total; + if (pl.op === 'build') { + if (!(pl.order >= pl.repeat)) { + failed( + 'Plan verification test failed: pl.order >= pl.repeat: ' + + JSON.stringify(pl) + ); + } + } + if (!(plan.height === pl.height + 1)) { + failed('Plan verification test failed: plan.height === pl.height+1'); + } + this.verifyPlan(pl, level + 1); + } + cnt += plan.items.length - 1; + if (!(plan.total === cnt)) { + failed('Count mismatch: ' + plan.total + ' vs ' + cnt); + } + } else if (plan.op === 'build') { + // Verify plan consistency + const ec = this.calcPlanCnt(plan.order, plan.height); + if (plan.full) { + const cnt = ec * plan.repeat + plan.repeat - 1; + if (!(plan.total === cnt)) { + failed('Plan verification test failed: plan.total === ec'); + } + } else { + if (!(plan.height === 1)) { + failed( + 'Plan verification test failed: expected height 1, got instead ' + + plan.height + ); + } + if (!(plan.total < ec)) { + failed('Plan verification test failed: plan.total < ec'); + } + const halfSize = Math.floor((plan.order - 1) / 2); + if (level > 0 && !(plan.total >= halfSize)) { + failed( + 'Plan verification test failed: plan.total >= halfSize: ' + + plan.total + + ', ' + + halfSize + ); + } + } + } else { + failed('Plan verification test failed: invalid op: ' + plan.op); + } + } + + // Pack the map according to the plan + // Return a new root + // + // Sample Plan: + // { + // "op": "assemble", + // "height": 2, + // "order": 7, + // "total": 10, + // "items": [ + // { + // "op": "build", + // "full": false, + // "height": 1, + // "order": 7, + // "repeat": 1, + // "total": 5 + // }, + // { + // "op": "build", + // "full": false, + // "height": 1, + // "order": 7, + // "repeat": 1, + // "total": 4 + // } + // ] + // } + // + + runPlan(plan, iter) { + function failed(msg) { + msg = 'Packing Plan is corrupt: ' + msg; + throw new Error(msg); + } + + if (plan.op === 'assemble') { + const ilen = plan.items.length; + for (let i = 0; i < ilen; i++) { + if (i > 0) { + this.populate(iter, 1); + } + this.runPlan(plan.items[i], iter); + } + } else if (plan.op === 'build') { + const n = (plan.total - plan.repeat + 1) / plan.repeat; + for (let i = 0; i < plan.repeat; i++) { + if (i > 0) { + this.populate(iter, 1); + } + this.populate(iter, n); + } + } else { + failed('invalid op: ' + plan.op); + } + this.flush(plan.height); + } + + flushLevel(level) { + this.prepareLevel(level + 1); + const node = this.stack[level]; + node._calcSize(); + this.addNode(level + 1, node); + this.stack[level] = undefined; + } + + flush(height) { + for (let i = 0; i < height; i++) { + const level = i; + if (this.stack[level]) { + // flush this level + this.flushLevel(level); + // next entry goes to parent + } + } + this.stackLevel = height; + } + + populate(iter, n) { + for (let i = 0; i < n; i++) { + const next = iter.next(); + this.entriesCnt++; + if (next.done) { + throw new Error( + 'unexpected end of iterator at ' + + this.entriesCnt + + ' vs ' + + iter.size + ); + } + const entry = next.value; + + const level = this.stackLevel; + this.prepareLevel(level); + this.addEntry(level, entry); + + if (level > 0) { + // Node - go populate the subtree now + this.stackLevel = 0; + } else if (this.stackIndices[level] === this.order - 1) { + // Leaf - we have filled all entries + // flush the leaf + this.flushLevel(level); + // next entry goes to parent + this.stackLevel++; + } + } + } + + addEntry(level, entry) { + this.stack[level].entries[this.stackIndices[level]++] = entry; + } + + addNode(level, node) { + this.stack[level].nodes[this.stackIndices[level]] = node; + + if (this.stackIndices[level] === this.order - 1) { + // we filled the whole node + // flush the node + this.flushLevel(level); + // next entry goes to parent + this.stackLevel++; + } + } + + prepareLevel(level) { + if (!this.stack[level]) { + const entries = allocArray(this.order - 1); + entries.length = 0; + let nodes; + if (level > 0) { + nodes = allocArray(this.order); + nodes.length = 0; + } + this.stack[level] = new SortedMapBtreeNode( + this.comparator, + this.options, + this.ownerID, + entries, + nodes + ); + this.stackIndices[level] = 0; + } + } + + finish() { + const level = this.stackLevel; + if (level >= this.stack.length) { + return undefined; + } + return this.stack[level].nodes[0]; + } + + // Will pack seq and store it in the map + pack(comparator, options, ownerID, collection) { + if (options && options.type && options.type !== DEFAULT_TYPE) { + throw new Error('Unsuported type by btree factory: ' + options.type); + } + + this.order = + options && options.btreeOrder ? options.btreeOrder : DEFAULT_BTREE_ORDER; + + const kc = KeyedCollection(collection); + assertNotInfinite(kc.size); + + const plan = this.preparePlan(this.order, kc.size); + + this.comparator = comparator; + this.options = options; + this.ownerID = ownerID; + this.stack = []; + this.stackIndices = []; + this.stackLevel = 0; + this.entriesCnt = 0; + + const iter = kc.entries(); + this.runPlan(plan, iter); + + if (!iter.next().done) { + throw new Error('iterator did not end when expected'); + } + + return this.finish(); + } +} + +SortedMapBtreeNodePacker.cache = {}; +SortedMapBtreeNodePacker.cacheSize = 0; + +export class SortedMapBtreeNodeFactory extends SortedMapNodeFactory { + createNode(comparator, options, ownerID, entries, nodes) { + return new SortedMapBtreeNode(comparator, options, ownerID, entries, nodes); + } + + createPacker() { + return new SortedMapBtreeNodePacker(); + } + + createIterator(map, type, reverse) { + return new SortedMapBtreeNodeIterator(map, type, reverse); + } +} diff --git a/src/SortedMapNode.js b/src/SortedMapNode.js new file mode 100644 index 0000000000..1825c0699f --- /dev/null +++ b/src/SortedMapNode.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export class SortedMapNode { + constructor(comparator, options, ownerID) { + this.comparator = comparator; + this.options = options; + this.ownerID = ownerID; + } + + getComparator() {} + // eslint-disable-next-line no-unused-vars + get(key, notSetValue) {} + // eslint-disable-next-line no-unused-vars + upsert(ownerID, key, value, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars + remove(ownerID, key, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars + fastRemove(ownerID, key, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars + iterate(fn, reverse) {} + // eslint-disable-next-line no-unused-vars + print(level, maxDepth) {} + // eslint-disable-next-line no-unused-vars + checkConsistency(printFlag) {} +} + +export class SortedMapPacker { + // eslint-disable-next-line no-unused-vars + pack(comparator, options, ownerID, collection) {} +} + +export class SortedMapNodeFactory { + // eslint-disable-next-line no-unused-vars + createNode(comparator, options, ownerID, entries, nodes) {} + createPacker() {} + // eslint-disable-next-line no-unused-vars + createIterator(map, type, reverse) {} +} diff --git a/src/SortedSet.js b/src/SortedSet.js new file mode 100644 index 0000000000..e000ed5b13 --- /dev/null +++ b/src/SortedSet.js @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2017, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * Original source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { SetCollection, KeyedCollection } from './Collection'; +import { IS_SORTED_SYMBOL } from './predicates/isSorted'; +import { isSortedSet } from './predicates/isSortedSet'; +import { Set } from './Set'; +import { SortedMap, emptySortedMap } from './SortedMap'; +import { mapFactory } from './Operations'; + +export class SortedSet extends Set { + // @pragma Construction + + constructor(value, comparator, options) { + if (!comparator) { + if (this instanceof SortedSet) { + comparator = this._map && this.getComparator(); + } + if (!comparator) { + comparator = SortedSet.defaultComparator; + } + } + if (!options) { + if (this instanceof SortedSet) { + options = this._map && this.getOptions(); + } + if (!options) { + options = SortedSet.defaultOptions; + } + } + return value === null || value === undefined + ? emptySortedSet(comparator, options) + : isSortedSet(value) && + value.getComparator() === comparator && + value.getOptions() === options + ? value + : emptySortedSet(comparator, options).withMutations(set => { + set.pack(value); + }); + } + + static of(/*...values*/) { + return this(arguments); + } + + static fromKeys(value) { + return this(KeyedCollection(value).keySeq()); + } + + toString() { + return this.__toString('SortedSet {', '}'); + } + + // @pragma Access + + getComparator() { + return this._map.getComparator(); + } + + getOptions() { + return this._map.getOptions(); + } + + // @pragma Modification + + pack(value) { + const seq = + value === undefined + ? undefined + : SetCollection(value) + .toKeyedSeq() + .mapKeys((k, v) => v); + return updateSortedSet(this, this._map.pack(seq)); + } + + from(value, backwards) { + return this._map.from(value, backwards).toSetSeq(); + } + + fromIndex(index, backwards) { + return this._map.fromIndex(index, backwards).toSetSeq(); + } + + sort(comparator) { + // Late binding + return SortedSet(this, comparator, this.getOptions()); + } + + sortBy(mapper, comparator) { + // Late binding + return SortedSet(mapFactory(this, mapper), comparator, this.getOptions()); + } + + __ensureOwner(ownerID) { + if (ownerID === this.__ownerID) { + return this; + } + const newMap = this._map.__ensureOwner(ownerID); + if (!ownerID) { + if (this.size === 0) { + return this.__empty(); + } + this.__ownerID = ownerID; + this._map = newMap; + return this; + } + return this.__make(newMap, ownerID); + } +} + +SortedSet.isSortedSet = isSortedSet; + +SortedSet.defaultComparator = SortedMap.defaultComparator; +SortedSet.defaultOptions = SortedMap.defaultOptions; + +const SortedSetPrototype = SortedSet.prototype; +SortedSetPrototype[IS_SORTED_SYMBOL] = true; + +SortedSetPrototype.__empty = function () { + return emptySortedSet(this.getComparator(), this.getOptions()); +}; +SortedSetPrototype.__make = makeSortedSet; + +function updateSortedSet(set, newMap) { + if (set.__ownerID) { + set.size = newMap.size; + set._map = newMap; + return set; + } + return newMap === set._map + ? set + : newMap.size === 0 + ? set.__empty() + : set.__make(newMap); +} + +function makeSortedSet(map, ownerID) { + const set = Object.create(SortedSetPrototype); + set.size = map ? map.size : 0; + set._map = map; + set.__ownerID = ownerID; + return set; +} + +function emptySortedSet(comparator, options) { + return makeSortedSet(emptySortedMap(comparator, options)); +} diff --git a/src/TrieUtils.js b/src/TrieUtils.js index ded7991184..0599b62334 100644 --- a/src/TrieUtils.js +++ b/src/TrieUtils.js @@ -21,6 +21,10 @@ export function SetRef(ref) { } } +export function GetRef(ref) { + return ref.value; +} + // A function which returns a value representing an "owner" for transient writes // to tries. The return value will only ever equal itself, and will not equal // the return of any subsequent call of this function. diff --git a/src/predicates/isSorted copy.js b/src/predicates/isSorted copy.js new file mode 100644 index 0000000000..b622153bc0 --- /dev/null +++ b/src/predicates/isSorted copy.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017-present, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const IS_SORTED_SYMBOL = '@@__IMMUTABLE_SORTED__@@'; + +export function isSorted(maybeSorted) { + return Boolean(maybeSorted && maybeSorted[IS_SORTED_SYMBOL]); +} diff --git a/src/predicates/isSorted.js b/src/predicates/isSorted.js new file mode 100644 index 0000000000..b622153bc0 --- /dev/null +++ b/src/predicates/isSorted.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017-present, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const IS_SORTED_SYMBOL = '@@__IMMUTABLE_SORTED__@@'; + +export function isSorted(maybeSorted) { + return Boolean(maybeSorted && maybeSorted[IS_SORTED_SYMBOL]); +} diff --git a/src/predicates/isSortedMap.js b/src/predicates/isSortedMap.js new file mode 100644 index 0000000000..d2b6ea03fc --- /dev/null +++ b/src/predicates/isSortedMap.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2017-present, Applitopia, Inc. + * + * Modified source code is licensed under the MIT-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { isMap } from './isMap'; +import { isSorted } from './isSorted'; + +export function isSortedMap(maybeSortedMap) { + return isMap(maybeSortedMap) && isSorted(maybeSortedMap); +} diff --git a/src/predicates/isSortedSet.js b/src/predicates/isSortedSet.js new file mode 100644 index 0000000000..647b1e0838 --- /dev/null +++ b/src/predicates/isSortedSet.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { isSet } from './isSet'; +import { isSorted } from './isSorted'; + +export function isSortedSet(maybeSortedSet) { + return isSet(maybeSortedSet) && isSorted(maybeSortedSet); +} diff --git a/src/utils/deepEqual.js b/src/utils/deepEqual.js index 01b25aaecf..f273ce9741 100644 --- a/src/utils/deepEqual.js +++ b/src/utils/deepEqual.js @@ -5,6 +5,7 @@ import { isKeyed } from '../predicates/isKeyed'; import { isIndexed } from '../predicates/isIndexed'; import { isAssociative } from '../predicates/isAssociative'; import { isOrdered } from '../predicates/isOrdered'; +import { isSorted } from '../predicates/isSorted'; export default function deepEqual(a, b) { if (a === b) { @@ -19,7 +20,8 @@ export default function deepEqual(a, b) { a.__hash !== b.__hash) || isKeyed(a) !== isKeyed(b) || isIndexed(a) !== isIndexed(b) || - isOrdered(a) !== isOrdered(b) + isOrdered(a) !== isOrdered(b) || + isSorted(a) !== isSorted(b) ) { return false; } diff --git a/type-definitions/immutable.d.ts b/type-definitions/immutable.d.ts index dc2b24547c..faa56ec33e 100644 --- a/type-definitions/immutable.d.ts +++ b/type-definitions/immutable.d.ts @@ -1506,6 +1506,413 @@ declare namespace Immutable { flip(): Map; } + /** + * A type of Map that keeps its entries (their keys) sorted by a comparator. + * The current implementation is using a classic [B-Tree](https://en.wikipedia.org/wiki/B-tree) memory structure + * with O(N) space requirements and O(log N) get, set, and delete operations. + * + * ```js + * > const { SortedMap } = require('immutable-sorted'); + * + * > const map1=SortedMap([['orange','orange'], ['apple','red'], ['banana','yellow']]); + * SortedMap { "apple": "red", "banana": "yellow", "orange": "orange" } + * + * > const map2=map1.set('mango', 'yellow/orange'); + * SortedMap { "apple": "red", "banana": "yellow", "mango": "yellow/orange", "orange": "orange" } + * + * > const map3=map2.delete('banana'); + * SortedMap { "apple": "red", "mango": "yellow/orange", "orange": "orange" } + * ``` + * + * Using a custom comparator: + * ```js + * > const reverseCmp=(a,b)=>(a>b?-1:a const map4=SortedMap(map1, reverseCmp); + * SortedMap { "orange": "orange", "banana": "yellow", "apple": "red" } + * + * > const map5=map4.set('mango', 'yellow/orange'); + * SortedMap { "orange": "orange", "mango": "yellow/orange", "banana": "yellow", "apple": "red" } + * + * > const map6=map5.delete('banana'); + * SortedMap { "orange": "orange", "mango": "yellow/orange", "apple": "red" } + * ``` + * + * When iterating a `SortedMap`, the order of entries is guaranteed to be the same + * as the sorted order of keys determined by a comparator. + * + * Map keys and values may be of any type. Equality of keys is determined + * by comparator returning 0 value. In case of a custom comparator the equality + * may be redefined to have a different meaning than `Immutable.is`. + * + * Many real use cases will be about storing the whole objects in `SortedMap`. + * That will usually be meaningful only when custom comparator is defined. + * + * Let's consider the following example with city objects as keys and their co-ordinates as values: + * ```js + * > const { SortedMap, Seq, fromJS } = require('immutable-sorted'); + * // Have an array of city objects + * > const cities=[ + * [{state: 'MA', city: 'Boston'}, ['42°21′N','71°04′W']], + * [{city: 'Miami', state: 'FL'},['25°47′N','80°13′W']], + * [{city: 'Seattle', state: 'WA'},['47°37′N','122°20′W']], + * [{city: 'Phoenix', state: 'AZ'},['33°27′N','112°04′W']]]; + * // Make a seq that converts cities and their co-ordinates from JS into immutable objects + * > const citiesSeq=Seq.Keyed(cities).mapKeys((v)=>fromJS(v)).map((v)=>fromJS(v)); + * Seq { + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ], + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ] } + * // Create a default SortedMap + * > const map1=SortedMap(citiesSeq); + * SortedMap { + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ], + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ] } + * ``` + * When relying on `defaultComparator`, like in example above, the objects get sorted + * by their string representations from `toString()` method. This is usually not what + * the application designers want. In our case it makes more sense to sort by the city name, + * than the whole string representation. + * + * Let's create a custom comparator: + * ```js + * // Define a general comparator + * > const cmp=(a,b)=>(a>b?1:a let citiesCmp=(a,b)=>cmp(a.get('city'), b.get('city')); + * // Create a SortedSet with custom comparator + * > const map2=SortedMap(citiesSeq, citiesCmp); + * SortedMap { + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } + * ``` + * The custom comparator that we have created seems to work as expected. Now let's add + * into the collection another city of Phoenix, this time from state Illinois. + * ```js + * > const map3=map2.set(fromJS({city: 'Phoenix', state: 'IL'}), fromJS(['41°36′N','87°37′W'])); + * SortedMap { + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "IL" }: List [ "41°36′N", "87°37′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } + * ``` + * The Phoenix, AZ had been replaced with Phoenix, IL. This is because of the way + * the custom comparator is defined. It determines equality by comparing city names only, + * therefore Phoenix, AZ and Phoenix, IL are equal according to this comparator. + * Let's try to extend the comparator to compare the city name first and if they + * match then determine the result by comparing the state. + * ```js + * // Define more complex custom comparator + * > citiesCmp=(a,b)=>cmp(a.get('city'), b.get('city'))||cmp(a.get('state'), b.get('state')); + * // Create a new SortedMap with new custom comparator + * > const map4=SortedMap(map2, citiesCmp); + * SortedMap { + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } + * // map4 looks the same as map2, now let's add the conflicting Phoenix, IL to map4 + * > const map5=map4.set(fromJS({city: 'Phoenix', state: 'IL'}), fromJS(['41°36′N','87°37′W'])); + * SortedMap { + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ], + * Map { "city": "Phoenix", "state": "IL" }: List [ "41°36′N", "87°37′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } + * ``` + * The custom comparator behaves as expected. Now let's swap the order of commands + * in the comparator and sort by state first and by city name second. + * ```js + * > const stateCitiesCmp=(a,b)=>cmp(a.get('state'), b.get('state'))||cmp(a.get('city'), b.get('city')); + * > const map6=SortedMap(map5, stateCitiesCmp); + * SortedMap { + * Map { "city": "Phoenix", "state": "AZ" }: List [ "33°27′N", "112°04′W" ], + * Map { "city": "Miami", "state": "FL" }: List [ "25°47′N", "80°13′W" ], + * Map { "city": "Phoenix", "state": "IL" }: List [ "41°36′N", "87°37′W" ], + * Map { "state": "MA", "city": "Boston" }: List [ "42°21′N", "71°04′W" ], + * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } + * ``` + */ + export module SortedMap { + /** + * True if the provided value is a SortedMap + * + * ```js + * > const { SortedMap, SortedSet, List, Map, Set } = require('immutable-sorted'); + * > SortedMap.isSortedMap(SortedMap()); + * true + * > SortedMap.isSortedMap(SortedSet()); + * false + * > SortedMap.isSortedMap(Map()); + * false + * > SortedMap.isSortedMap(Set()); + * false + * > SortedMap.isSortedMap(List()); + * false + * ``` + */ + function isSortedMap(maybeSortedMap: any): boolean; + + /** + * Creates a new SortedMap from alternating keys and values + * + * ```js + * > const { SortedMap } = require('immutable-sorted') + * > let sortedMap=SortedMap.of('orange','orange', 'apple','red', 'banana','yellow'); + * SortedMap { "apple": "red", "banana": "yellow", "orange": "orange" } + * ``` + * + * @deprecated Use SortedMap([ [ 'k', 'v' ] ]) or SortedMap({ k: 'v' }) + */ + function of(...keyValues: Array): SortedMap; + } + + /** + * Creates a new immutable `SortedMap` containing the entries of the provided + * collection-like. The keys will be sorted by the provided comparator. + * + * The comparator is a function that takes two arguments (a, b) and + * returns 0 if a and b are equal, returns positive number when a > b + * and returns negative number if a < b. + * + * If comparator is undefined, the `defaultComparator` will be applied. + * This comparator is different than the default comparator used by `Collection.sort` + * because more stringent comparison rules have to be applied to support all the types + * and marginal values like NaN or Infinity. + * + * The `defaultComparator` first determines equality by calling `Immutable.is`, + * then compares the types of both values. If both values are of the same type, + * then it compares the values by using the javascript comparison operators > and <, + * but before that the objects, functions, and symbols are converted to string. + * + * The internal data structures will be created according to the given options. If options + * are undefined then `defaultOptions` will be applied. + * + * The default options are: + * ```js + * {type: 'btree', btreeOrder: 33} + * ``` + * Currently the only type implemented is `btree`. It is a classic [B-Tree](https://en.wikipedia.org/wiki/B-tree) + * structure with keys and values not only in leaves but also in internal nodes. + * The option `btreeOrder` is defined as the maximum number of children that any internal nodes can have + * and also implies maximum number of entries (key/value pairs) in any node which is (`btreeOrder` - 1). + * The `btreeOrder` option can be changed in a constructor and can be any integer greater or equal 3. + * + * There are many ways to quickly and conveniently create a `SortedMap` by calling a constructor. + * Below are some examples. + * + * Create a `SortedMap` from any array of [K,V]: + * ```js + * > const { SortedMap } = require('immutable-sorted'); + * > let a=SortedMap([['a','A'], ['c','C'], ['z','Z'], ['u','U'], ['b','B']]); + * SortedMap { "a": "A", "b": "B", "c": "C", "u": "U", "z": "Z" } + * ``` + * + * From a keyed sequence: + * ```js + * > const { SortedMap, SortedSet, List, Map, Seq, Set } = require('immutable-sorted'); + * + * > let seq=Seq({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * Seq { "x": "X", "c": "B", "m": "M", "anylabel": "K", "f": "F" } + * + * > let b=SortedMap(seq); + * SortedMap { "anylabel": "K", "c": "B", "f": "F", "m": "M", "x": "X" } + * ``` + * + * From other collections (List, Range, Set, Map): + * ```js + * > const { SortedMap, List, Map, Range, Seq, Set } = require('immutable-sorted'); + * > let list=List(['orange', 'apple', 'banana']); + * List [ "orange", "apple", "banana" ] + * > let c=SortedMap(list.toKeyedSeq()); + * SortedMap { 0: "orange", 1: "apple", 2: "banana" } + * + * > c=SortedMap(list.toKeyedSeq().flip()); + * SortedMap { "apple": 1, "banana": 2, "orange": 0 } + * + * > let range=Range(30, 0, 5); + * Range [ 30...0 by -5 ] + * > c=SortedMap(range.toKeyedSeq()); + * SortedMap { 0: 30, 1: 25, 2: 20, 3: 15, 4: 10, 5: 5 } + * > c=SortedMap(range.toKeyedSeq().flip()); + * SortedMap { 5: 5, 10: 4, 15: 3, 20: 2, 25: 1, 30: 0 } + * + * > let set=Set(['orange', 'apple', 'banana']); + * Set { "orange", "apple", "banana" } + * > c=SortedMap(set.toKeyedSeq()); + * SortedMap { "apple": "apple", "banana": "banana", "orange": "orange" } + * + * > let map=Map({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * Map { "x": "X", "c": "B", "m": "M", "anylabel": "K", "f": "F" } + * > c=SortedMap(map); + * SortedMap { "anylabel": "K", "c": "B", "f": "F", "m": "M", "x": "X" } + * ``` + * + * Use a custom comparator (reverse order): + * ```js + * > let reverseComparator=(a, b)=>(a > b ? -1 : a < b ? 1 : 0); + * > let d=SortedMap(list.toKeyedSeq().flip(), reverseComparator); + * SortedMap { "orange": 0, "banana": 2, "apple": 1 } + * + * // Create an empty map with custom comparator + * > d=SortedMap(undefined, reverseComparator); + * ``` + * + * Change the `btreeOrder` option: + * ```js + * > let options={type: 'btree', btreeOrder: 17}; + * > let e=SortedMap(list.toKeyedSeq().flip(), reverseComparator, options); + * SortedMap { "orange": 0, "banana": 2, "apple": 1 } + * + * // Create an empty set with default comparator and custom options + * > e=SortedMap(undefined, undefined, options); + * ``` + * + * You can also conveniently create a `SortedMap` within a chain of sequence operations: + * ```js + * > seq.toSortedMap(); + * SortedMap { "anylabel": "K", "c": "B", "f": "F", "m": "M", "x": "X" } + * + * > seq.toSortedMap(reverseComparator); + * SortedMap { "x": "X", "m": "M", "f": "F", "c": "B", "anylabel": "K" } + * ``` + */ + export function SortedMap( + collection?: Iterable<[K, V]>, + comparator?: (a: K, b: K) => number, + options?: Object + ): SortedMap; + export function SortedMap( + collection: Iterable>, + comparator?: (a: T, b: T) => number, + options?: Object + ): SortedMap; + export function SortedMap( + obj: { [key: string]: V }, + comparator?: (a: string, b: string) => number, + options?: Object + ): SortedMap; + + export interface SortedMap extends Map { + /** + * The number of entries (key/value pairs) in this SortedMap. + */ + readonly size: number; + + // Persistent changes + + /** + * Returns a new `SortedMap` containing the entries of the provided + * collection-like. The entries will be organized in an optimized tree structure + * with internal nodes and leaves defragmented as much as possible + * while keeping all the consistency rules enforced. + * + * If the collection argument is undefined then the current content + * of this `SortedMap` will be reorganized into an optimized tree structure. + * + * The pack procedure is actually called from the `SortedMap` constructor + * as it is usually faster than a series of set operations. It is recommended + * to explicitly call this method after a large batch of update or delete operations + * to release a portion of the allocated memory and to speed up the consequent get + * operations. + */ + pack(collection?: Iterable<[K, V]>): this; + + // Deep persistent changes + + // Transient changes + + // Reading values + + /** + * Returns a sequence representing a portion of this sorted map starting with a specific key + * up to the last entry in the sorted map. + * If the optional parameter backwards is set to true, the returned sequence will + * list the entries backwards, starting with key down to the first entry in the sorted map, + * Example: + * ```js + * > const abc = SortedMap([["A", "a"], ["B", "b"], ["C", "c"], ["D", "d"], ["E", "e"], ["F", "f"], ["G", "g"], ["H", "h"], ["I", "i"], ["J", "j"], ["K", "k"], ["L", "l"], ["M", "m"], ["N", "n"], ["O", "o"], ["P", "p"], ["Q", "q"], ["R", "r"], ["S", "s"], ["T", "t"], ["U", "u"], ["V", "v"], ["W", "w"], ["X", "x"], ["Y", "y"], ["Z", "z"]]); + * + * > abc.from("R"); + * Seq { "R": "r", "S": "s", "T": "t", "U": "u", "V": "v", "W": "w", "X": "x", "Y": "y", "Z": "z" } + * + * > abc.from("R", true); + * Seq { "R": "r", "Q": "q", "P": "p", "O": "o", "N": "n", "M": "m", "L": "l", "K": "k", "J": "j", "I": "i", "H": "h", "G": "g", "F": "f", "E": "e", "D": "d", "C": "c", "B": "b", "A": "a" } + * ``` + * + * The method from() can be efficiently combined with take() to retrieve the desired number of values or with takeWhile() to retrieve a specific range: + * ```js + * > abc.from("R").take(5); + * Seq { "R": "r", "S": "s", "T": "t", "U": "u", "V": "v" } + * + * > abc.from("R").takeWhile((v, k) => k < "W"); + * Seq { "R": "r", "S": "s", "T": "t", "U": "u", "V": "v" } + * + * > abc.from("R", true).take(5); + * Seq { "R": "r", "Q": "q", "P": "p", "O": "o", "N": "n" } + * + * > abc.from("R", true).takeWhile((v, k) => k > "K"); + * Seq { "R": "r", "Q": "q", "P": "p", "O": "o", "N": "n", "M": "m", "L": "l" } + * ``` + */ + from(key: K, backwards?: boolean): Seq; + + /** + * Returns a sequence representing a portion of this sorted map starting from numeric index position, + * as if the collection was an array. + * If the optional parameter backwards is set to true, the returned sequence will + * list the entries backwards. + * + * The method is optimized to quickly find the n-th entry inside the b-tree structure + * by checking the computed sizes of underlying nodes. + * Even though the algorithm is not as fast as working with a native array, + * it is faster by orders of magnitude than walking through the first n elements + * of unindexed collection to just skip them. The access time is O(log N). + * Example: + * ```js + * > const abc = SortedMap([["A", "a"], ["B", "b"], ["C", "c"], ["D", "d"], ["E", "e"], ["F", "f"], ["G", "g"], ["H", "h"], ["I", "i"], ["J", "j"], ["K", "k"], ["L", "l"], ["M", "m"], ["N", "n"], ["O", "o"], ["P", "p"], ["Q", "q"], ["R", "r"], ["S", "s"], ["T", "t"], ["U", "u"], ["V", "v"], ["W", "w"], ["X", "x"], ["Y", "y"], ["Z", "z"]]); + * + * > abc.fromIndex(4).take(5); + * Seq { "E": "e", "F": "f", "G": "g", "H": "h", "I": "i" } + * + * > abc.fromIndex(4, true).take(5); + * Seq { "E": "e", "D": "d", "C": "c", "B": "b", "A": "a" } + * ``` + */ + fromIndex(index: number, backwards?: boolean): Seq; + + /** + * Prints out the internal Btree structure of the SortedMap. + * Keeps printing the nodes recursively until `maxDepth` level is reached. + * + * ```js + * const { SortedMap } = require('immutable-sorted') + * const aSortedMap = Range(0, 8).toSortedMap(undefined, {btreeOrder: 4}); + * sortedMap.print(); + * + * + LEAF[0] (L0) + * - ENTRY[0]: 0 + * - ENTRY[1]: 1 + * - ENTRY[0]: 2 + * + LEAF[1] (L0) + * - ENTRY[0]: 3 + * - ENTRY[1]: 4 + * - ENTRY[1]: 5 + * + LEAF[2] (L0) + * - ENTRY[0]: 6 + * - ENTRY[1]: 7 + * + * ``` + * + */ + print(maxDepth?: number): this; + } + /** * A type of Map that has the additional guarantee that the iteration order of * entries will be the order in which they were set(). @@ -1749,6 +2156,397 @@ declare namespace Immutable { function union(sets: Iterable>): Set; } + /** + * A type of Set that keeps its values sorted by a comparator. + * The current implementation is using a classic [B-Tree](https://en.wikipedia.org/wiki/B-tree) memory structure + * with O(N) space requirements and O(log N) get, add, and delete operations. + * + * ```js + * > const { SortedSet } = require('immutable-sorted'); + * + * > const set1=SortedSet(['orange', 'apple', 'banana']); + * SortedSet { "apple", "banana", "orange" } + * + * > const set2=set1.add('mango'); + * SortedSet { "apple", "banana", "mango", "orange" } + * + * > const set3=set2.delete('banana'); + * SortedSet { "apple", "mango", "orange" } + * + * ``` + * + * Using a custom comparator: + * ```js + * > const reverseCmp=(a,b)=>(a>b?-1:a const set4=SortedSet(set1, reverseCmp); + * SortedSet { "orange", "banana", "apple" } + * + * > const set5=set4.add('mango'); + * SortedSet { "orange", "mango", "banana", "apple" } + * + * > const set6=set5.delete('banana'); + * SortedSet { "orange", "mango", "apple" } + * ``` + * + * When iterating a `SortedSet`, the entries will be (value, value) pairs. Iteration + * order of a SortedSet is determined by a comparator. + * + * Set values, like Map keys, may be of any type. Equality is determined + * by comparator returning 0 value. In case of a custom comparator the equality + * may be redefined to have a different meaning than `Immutable.is`. + * + * Many real use cases will be about storing the whole objects in `SortedSet`. + * That will usually be meaningful only when custom comparator is defined. + * + * Let's consider the following example with city objects: + * ```js + * > const { SortedSet, Seq, fromJS } = require('immutable-sorted'); + * // Have an array of city objects + * > const cities=[ + * {state: 'MA', city: 'Boston'}, + * {city: 'Miami', state: 'FL'}, + * {city: 'Seattle', state: 'WA'}, + * {city: 'Phoenix', state: 'AZ'}]; + * // Make a seq that converts cities from JS into immutable objects + * > const citiesSeq=Seq(cities).map((v)=>fromJS(v)); + * // Create a default SortedSet + * > const set1=SortedSet(citiesSeq); + * SortedSet { + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "AZ" }, + * Map { "city": "Seattle", "state": "WA" }, + * Map { "state": "MA", "city": "Boston" } } + * ``` + * When relying on `defaultComparator`, like in example above, the objects get sorted + * by their string representations from `toString()` method. This is usually not what + * the application designers want. In our case it makes more sense to sort by the city name, + * than the whole string representation. + * + * Let's create a custom comparator: + * ```js + * // Define a general comparator + * > const cmp=(a,b)=>(a>b?1:a let citiesCmp=(a,b)=>cmp(a.get('city'), b.get('city')); + * // Create a SortedSet with custom comparator + * > const set2=SortedSet(citiesSeq, citiesCmp); + * SortedSet { + * Map { "state": "MA", "city": "Boston" }, + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "AZ" }, + * Map { "city": "Seattle", "state": "WA" } } + * ``` + * The custom comparator that we have created seems to work as expected. Now let's add + * into the collection another city of Phoenix, this time from state Illinois. + * ```js + * > const set3=set2.add(fromJS({city: 'Phoenix', state: 'IL'})); + * SortedSet { + * Map { "state": "MA", "city": "Boston" }, + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "IL" }, + * Map { "city": "Seattle", "state": "WA" } } + * ``` + * The Phoenix, AZ had been replaced with Phoenix, IL. This is because of the way + * the custom comparator is defined. It determines equality by comparing city names only, + * therefore Phoenix, AZ and Phoenix, IL are equal according to this comparator. + * Let's try to extend the comparator to compare the city name first and if they + * match then determine the result by comparing the state. + * ```js + * // Define more complex custom comparator + * > citiesCmp=(a,b)=>cmp(a.get('city'), b.get('city'))||cmp(a.get('state'), b.get('state')); + * // Create a new SortedSet with new custom comparator + * > const set4=SortedSet(set2, citiesCmp); + * SortedSet { + * Map { "state": "MA", "city": "Boston" }, + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "AZ" }, + * Map { "city": "Seattle", "state": "WA" } } + * // set4 looks the same as set2, now let's add the conflicting Phoenix, IL to set4 + * > const set5=set4.add(fromJS({city: 'Phoenix', state: 'IL'})); + * SortedSet { + * Map { "state": "MA", "city": "Boston" }, + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "AZ" }, + * Map { "city": "Phoenix", "state": "IL" }, + * Map { "city": "Seattle", "state": "WA" } } + * ``` + * The custom comparator behaves as expected. Now let's swap the order of commands + * in the comparator and sort by state first and by city name second. + * ```js + * > const stateCitiesCmp=(a,b)=>cmp(a.get('state'), b.get('state'))||cmp(a.get('city'), b.get('city')); + * > const set6=SortedSet(set5, stateCitiesCmp); + * SortedSet { + * Map { "city": "Phoenix", "state": "AZ" }, + * Map { "city": "Miami", "state": "FL" }, + * Map { "city": "Phoenix", "state": "IL" }, + * Map { "state": "MA", "city": "Boston" }, + * Map { "city": "Seattle", "state": "WA" } } + * ``` + */ + export module SortedSet { + /** + * True if the provided value is a `SortedSet`. + * + * ```js + * > const { SortedMap, SortedSet, List, Map, Set } = require('immutable-sorted'); + * > SortedSet.isSortedSet(SortedSet()); + * true + * > SortedSet.isSortedSet(SortedMap()); + * false + * > SortedSet.isSortedSet(Set()); + * false + * > SortedSet.isSortedSet(Map()); + * false + * > SortedSet.isSortedSet(List()); + * false + * ``` + */ + function isSortedSet(maybeSortedSet: any): boolean; + + /** + * Creates a new `SortedSet` containing `values`. + * + * ```js + * > const { SortedMap } = require('immutable-sorted'); + * > let sortedSet=SortedSet.of("orange", "apple", "banana"); + * SortedSet { "apple", "banana", "orange" } + * ``` + */ + function of(...values: Array): SortedSet; + + /** + * `SortedSet.fromKeys()` creates a new immutable `SortedSet` containing + * the keys from this Collection or JavaScript Object. + * + * ```js + * > const { SortedMap, Map } = require('immutable-sorted'); + * + * > let map=Map({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * > let sortedSet=SortedSet.fromKeys(map); + * SortedSet { "anylabel", "c", "f", "m", "x" } + * + * > sortedSet=SortedSet.fromKeys({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * SortedSet { "anylabel", "c", "f", "m", "x" } + * ``` + */ + function fromKeys(iter: Collection): SortedSet; + function fromKeys(obj: { [key: string]: any }): SortedSet; + } + + /** + * Creates a new immutable `SortedSet` containing the values of the provided + * collection-like. The values will be sorted by the provided comparator. + * + * The comparator is a function that takes two arguments (a, b) and + * returns 0 if a and b are equal, returns positive number when a > b + * and returns negative number if a < b. + * + * If comparator is undefined, the `defaultComparator` will be applied. + * This comparator is different than the default comparator used by `Collection.sort` + * because more stringent comparison rules have to be applied to support all the types + * and marginal values like NaN or Infinity. + * + * The `defaultComparator` first determines equality by calling `Immutable.is`, + * then compares the types of both values. If both values are of the same type, + * then it compares the values by using the javascript comparison operators > and <, + * but before that the objects, functions, and symbols are converted to string. + * + * The internal data structures will be created according to the given options. If options + * are undefined then `defaultOptions` will be applied. + * + * The default options are: + * ```js + * {type: 'btree', btreeOrder: 33} + * ``` + * Currently the only type implemented is `btree`. It is a classic [B-Tree](https://en.wikipedia.org/wiki/B-tree) + * structure with keys and values not only in leaves but also in internal nodes. + * The option `btreeOrder` is defined as the maximum number of children that any internal nodes can have + * and also implies maximum number of entries (key/value pairs) in any node which is (`btreeOrder` - 1). + * The `btreeOrder` option can be changed in a constructor and can be any integer greater or equal 3. + * + * There are many ways to quickly and conveniently create a `SortedSet` by calling a constructor. + * Below are some examples. + * + * Create a `SortedSet` from any array: + * ```js + * > const { SortedMap } = require('immutable-sorted'); + * > let a=SortedSet(['a', 'c', 'z', 'u', 'b', 'q', 'd']); + * SortedSet { "a", "b", "c", "d", "q", "u", "z" } + * ``` + * + * From a sequence of values: + * ```js + * > const { SortedMap, SortedSet, List, Map, Seq, Set } = require('immutable-sorted'); + * + * > let seq=Seq({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * Seq { "x": "X", "c": "B", "m": "M", "anylabel": "K", "f": "F" } + * + * > let b=SortedSet(seq.keySeq()); + * SortedSet { "anylabel", "c", "f", "m", "x" } + * + * > let c=SortedSet(seq.valueSeq()); + * SortedSet { "B", "F", "K", "M", "X" } + * ``` + * + * From a string: + * ```js + * > let d=SortedSet('abcdefg'); + * SortedSet { "a", "b", "c", "d", "e", "f", "g" } + * + * > let e=SortedSet('gefdcba'); + * SortedSet { "a", "b", "c", "d", "e", "f", "g" } + * + * > e=SortedSet("hello"); + * SortedSet { "e", "h", "l", "o" } + * ``` + * + * From other collections (List, Range, Set, Map): + * ```js + * > let list=List(['orange', 'apple', 'banana']); + * List [ "orange", "apple", "banana" ] + * > let f=SortedSet(list); + * SortedSet { "apple", "banana", "orange" } + * + * > let range=Range(30, 0, 5); + * Range [ 30...0 by -5 ] + * > f=SortedSet(range); + * SortedSet { 5, 10, 15, 20, 25, 30 } + * + * > let set=Set(['orange', 'apple', 'banana']); + * Set { "orange", "apple", "banana" } + * > f=SortedSet(set); + * SortedSet { "apple", "banana", "orange" } + * + * > let map=Map({x:'X', c:'B', m:'M', anylabel:'K', f:'F'}); + * Map { "x": "X", "c": "B", "m": "M", "anylabel": "K", "f": "F" } + * > f=SortedSet(map.keySeq()); + * SortedSet { "anylabel", "c", "f", "m", "x" } + * ``` + * + * Use a custom comparator (reverse order): + * ```js + * > let reverseComparator=(a, b)=>(a > b ? -1 : a < b ? 1 : 0); + * > let g=SortedSet(list, reverseComparator); + * SortedSet { "orange", "banana", "apple" } + * + * // Create an empty set with custom comparator + * > g=SortedSet(undefined, reverseComparator); + * ``` + * + * Change the `btreeOrder` option: + * ```js + * > let options={type: 'btree', btreeOrder: 17}; + * > let h=SortedSet(list, reverseComparator, options); + * SortedSet { "orange", "banana", "apple" } + * + * // Create an empty set with default comparator and custom options + * > h=SortedSet(undefined, undefined, options); + * ``` + * + * You can also conveniently create a `SortedSet` within a chain of sequence operations: + * ```js + * > seq.keySeq().toSortedSet(); + * SortedSet { "anylabel", "c", "f", "m", "x" } + * + * > seq.valueSeq().toSortedSet(); + * SortedSet { "B", "F", "K", "M", "X" } + * + * > seq.valueSeq().toSortedSet(reverseComparator); + * SortedSet { "X", "M", "K", "F", "B" } + * ``` + */ + export function SortedSet( + collection?: Iterable, + comparator?: (a: T, b: T) => number, + options?: Object + ): SortedSet; + + export interface SortedSet extends Set { + /** + * The number of items in this SortedSet. + */ + readonly size: number; + + // Persistent changes + + /** + * Returns a new `SortedSet` containing the values of the provided + * collection-like. The values will be organized in an optimized tree structure + * with internal nodes and leaves defragmented as much as possible + * while keeping all the consistency rules enforced. + * + * If the collection argument is undefined then the current content + * of this `SortedSet` will be reorganized into an optimized tree structure. + * + * The pack procedure is actually called from the `SortedSet` constructor + * as it is usually faster than a series of add operations. It is recommended + * to explicitly call this method after a large batch of add or delete operations + * to release a portion of the allocated memory and to speed up the consequent get + * operations. + */ + pack(collection?: Iterable): this; + + /** + * Returns a sequence representing a subset of this sorted set starting with value + * up to the last element in the set. + * + * If the optional parameter backwards is set to true, the returned sequence will + * list the entries backwards, starting with value down to the first element in the set. + * + * Example: + * ```js + * const { SortedSet } = require('immutable-sorted'); + * + * const abc = SortedSet(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]); + * + * > abc.from("R"); + * Seq { "R", "S", "T", "U", "V", "W", "X", "Y", "Z" } + * + * > abc.from("R", true); + * Seq { "R", "Q", "P", "O", "N", "M", "L", "K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A" } + * ``` + * + * The method from() can be efficiently combined with take() to retrieve the desired number of values or with takeWhile() to retrieve a specific range: + * ```js + * > abc.from("R").take(5); + * Seq { "R", "S", "T", "U", "V" } + * + * > abc.from("R").takeWhile(s => s < "W"); + * Seq { "R", "S", "T", "U", "V" } + * + * > abc.from("R", true).take(5); + * Seq { "R", "Q", "P", "O", "N" } + * + * > abc.from("R", true).takeWhile(s => s > "K"); + * Seq { "R", "Q", "P", "O", "N", "M", "L" } + * ``` + */ + from(value: T, backwards?: boolean): Seq.Set; + + /** + * Returns a sequence representing a portion of this sorted set starting from numeric index position, + * as if the collection was an array. + * If the optional parameter backwards is set to true, the returned sequence will + * list the entries backwards. + * + * The method is optimized to quickly find the n-th entry inside the b-tree structure + * by checking the computed sizes of underlying nodes. + * Even though the algorithm is not as fast as working with a native array, + * it is faster by orders of magnitude than walking through the first n elements + * of unindexed collection to just skip them. The access time is O(log N). + * Example: + * ```js + * > abc.fromIndex(4).take(5); + * Seq { "E", "F", "G", "H", "I" } + * + * > abc.fromIndex(4, true).take(5); + * Seq { "E", "D", "C", "B", "A" } + * ``` + */ + fromIndex(index: number, backwards?: boolean): Seq.Set; + } + /** * Create a new immutable Set containing the values of the provided * collection-like. @@ -4327,6 +5125,18 @@ declare namespace Immutable { */ toOrderedMap(): OrderedMap; + /** + * Converts this Collection to a SortedMap, with entries sorted according to comparator + * If comparator is undefined then `defaultComparator` will be applied. + * + * Note: This is equivalent to `SortedMap(this.toKeyedSeq())`, but + * provided for convenience and to allow for chained expressions. + */ + toSortedMap( + comparator?: (a: K, b: K) => number, + options?: Object + ): SortedMap; + /** * Converts this Collection to a Set, discarding keys. Throws if values * are not hashable. @@ -4345,6 +5155,18 @@ declare namespace Immutable { */ toOrderedSet(): OrderedSet; + /** + * Converts this Collection to a SortedSet, maintaining the order of iteration and + * discarding keys. + * + * Note: This is equivalent to `SortedSet(this.valueSeq())`, but provided + * for convenience and to allow for chained expressions. + */ + toSortedSet( + comparator?: (a: V, b: V) => number, + options?: Object + ): SortedSet; + /** * Converts this Collection to a List, discarding keys. * @@ -5395,6 +6217,23 @@ declare namespace Immutable { */ function isOrdered(maybeOrdered: unknown): boolean; + /** + * True if `maybeSorted` is a Collection where iteration order is well + * defined by a comparator. + * + * + * ```js + * const { isSorted, Map, SortedMap, List, Set } = require('immutable'); + * isSorted([]); // false + * isSorted({}); // false + * isSorted(Map()); // false + * isSorted(SortedMap()); // true + * isSorted(List()); // true + * isSorted(Set()); // false + * ``` + */ + function isSorted(maybeSorted: any): boolean; + /** * True if `maybeValue` is a JavaScript Object which has *both* `equals()` * and `hashCode()` methods. @@ -5422,7 +6261,7 @@ declare namespace Immutable { /** * True if `maybeMap` is a Map. * - * Also true for OrderedMaps. + * Also true for OrderedMaps and SortedMaps. */ function isMap(maybeMap: unknown): maybeMap is Map; @@ -5441,7 +6280,7 @@ declare namespace Immutable { /** * True if `maybeSet` is a Set. * - * Also true for OrderedSets. + * Also true for OrderedSets and SortedSets. */ function isSet(maybeSet: unknown): maybeSet is Set; @@ -5452,6 +6291,13 @@ declare namespace Immutable { maybeOrderedSet: unknown ): maybeOrderedSet is OrderedSet; + /** + * True if `maybeSortedSet` is an SortedSet. + */ + export function isSortedSet( + maybeSortedSet: any + ): maybeSortedSet is SortedSet; + /** * True if `maybeRecord` is a Record. */ From b62ac0a08601860df33a69182d726950e40b8f0e Mon Sep 17 00:00:00 2001 From: Simon Accascina Date: Fri, 17 May 2024 15:56:37 +0200 Subject: [PATCH 2/2] make sorted set/map iml pass tests --- __tests__/SortedMap.ts | 17 +- __tests__/SortedSet.ts | 52 +----- src/SortedMap.js | 4 +- src/SortedMapBtreeNode.js | 1 + src/SortedMapNode.js | 9 + src/SortedSet.js | 2 + type-definitions/immutable.d.ts | 16 +- type-definitions/immutable.js.flow | 279 +++++++++++++++++++++++++++++ 8 files changed, 315 insertions(+), 65 deletions(-) diff --git a/__tests__/SortedMap.ts b/__tests__/SortedMap.ts index 9bf745116c..cd8b9d43c8 100644 --- a/__tests__/SortedMap.ts +++ b/__tests__/SortedMap.ts @@ -5,20 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -/** - * Copyright (c) 2014-present, Facebook, Inc. - * - * Original source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/// +import { is, List, Range, Record, Seq, SortedMap } from 'immutable'; import * as jasmineCheck from 'jasmine-check'; jasmineCheck.install(); -import { is, List, Range, Record, Seq, SortedMap } from '../'; - describe('SortedMap', () => { it('converts from object', () => { const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); @@ -120,7 +111,7 @@ describe('SortedMap', () => { it('iterates values', () => { const m = SortedMap({ a: 'A', b: 'B', c: 'C' }); - const iterator = jest.genMockFunction(); + const iterator = jest.fn(); m.forEach(iterator); expect(iterator.mock.calls).toEqual([ ['A', 'a', m], @@ -414,7 +405,7 @@ describe('SortedMap', () => { it('builds correct seq in function from', () => { const size = 10000; const data = Range(0, size).map(v => [v, 2 * v]); - const s = new SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); + const s = SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); expect(s.toSeq().size).toBe(size); @@ -432,7 +423,7 @@ describe('SortedMap', () => { it('builds correct seq in function from backwards', () => { const size = 10000; const data = Range(0, size).map(v => [v, 2 * v]); - const s = new SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); + const s = SortedMap(data, undefined, { type: 'btree', btreeOrder: 3 }); expect(s.toSeq().size).toBe(size); diff --git a/__tests__/SortedSet.ts b/__tests__/SortedSet.ts index c2599db7a9..80224066f2 100644 --- a/__tests__/SortedSet.ts +++ b/__tests__/SortedSet.ts @@ -5,16 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -/** - * Copyright (c) 2014-present, Facebook, Inc. - * - * Original source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/// - declare var Symbol: any; + import { is, List, @@ -25,33 +17,7 @@ import { Set, SortedMap, SortedSet, -} from '../'; - -declare function expect(val: any): ExpectWithIs; - -interface ExpectWithIs extends Expect { - is(expected: any): void; - not: ExpectWithIs; -} - -jasmine.addMatchers({ - is() { - return { - compare(actual, expected) { - const passed = is(actual, expected); - return { - pass: passed, - message: - 'Expected ' + - actual + - (passed ? '' : ' not') + - ' to equal ' + - expected, - }; - }, - }; - }, -}); +} from 'immutable'; describe('SortedSet', () => { it('accepts array of values', () => { @@ -155,7 +121,7 @@ describe('SortedSet', () => { it('iterates values', () => { const s = SortedSet.of(1, 2, 3); - const iterator = jest.genMockFunction(); + const iterator = jest.fn(); s.forEach(iterator); expect(iterator.mock.calls).toEqual([ [1, 1, s], @@ -238,7 +204,7 @@ describe('SortedSet', () => { SortedSet.of('C', 'D', 'E'), SortedSet.of('D', 'B', 'F') ); - expect(s).is(SortedSet.of('A', 'B', 'C', 'D', 'E', 'F')); + expect(is(s, SortedSet.of('A', 'B', 'C', 'D', 'E', 'F'))).toBeTruthy(); }); it('intersects multiple sets', () => { @@ -246,7 +212,7 @@ describe('SortedSet', () => { SortedSet.of('B', 'C', 'D'), SortedSet.of('A', 'C', 'E') ); - expect(s).is(SortedSet.of('C')); + expect(is(s, SortedSet.of('C'))).toBeTruthy(); }); it('diffs multiple sets', () => { @@ -254,7 +220,7 @@ describe('SortedSet', () => { SortedSet.of('C', 'D', 'E'), SortedSet.of('D', 'B', 'F') ); - expect(s).is(SortedSet.of('A')); + expect(is(s, SortedSet.of('A'))).toBeTruthy(); }); it('expresses value equality with set sequences', () => { @@ -349,7 +315,7 @@ describe('SortedSet', () => { }); it('works with the `new` operator #3', () => { - const s = new SortedSet([1, 2, 3]); + const s = SortedSet([1, 2, 3]); expect(s.has(1)).toBe(true); expect(s.has(2)).toBe(true); expect(s.has(3)).toBe(true); @@ -359,7 +325,7 @@ describe('SortedSet', () => { it('builds correct seq in function from', () => { const size = 10000; const data = Range(0, size); - const s = new SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); + const s = SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); expect(s.toSeq().size).toBe(size); @@ -377,7 +343,7 @@ describe('SortedSet', () => { it('builds correct seq in function from backwards', () => { const size = 10000; const data = Range(0, size); - const s = new SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); + const s = SortedSet(data, undefined, { type: 'btree', btreeOrder: 3 }); expect(s.toSeq().size).toBe(size); diff --git a/src/SortedMap.js b/src/SortedMap.js index f56c460043..89ba788134 100644 --- a/src/SortedMap.js +++ b/src/SortedMap.js @@ -44,6 +44,7 @@ export class SortedMap extends Map { } } + // eslint-disable-next-line no-constructor-return return value === null || value === undefined ? emptySortedMap(comparator, options) : isSortedMap(value) && @@ -228,7 +229,8 @@ export class SortedMap extends Map { return 1; } return this._root.checkConsistency(printFlag); - } else if (!(this.size === 0)) { + } + if (!(this.size === 0)) { return 2; } diff --git a/src/SortedMapBtreeNode.js b/src/SortedMapBtreeNode.js index bb17421a18..30c8d35606 100644 --- a/src/SortedMapBtreeNode.js +++ b/src/SortedMapBtreeNode.js @@ -41,6 +41,7 @@ class SortedMapBtreeNode extends SortedMapNode { this.btreeNodeSplitSize = Math.floor((this.btreeOrder - 1) / 2); this._calcSize(); + // eslint-disable-next-line no-constructor-return return this; } diff --git a/src/SortedMapNode.js b/src/SortedMapNode.js index 1825c0699f..d47563a7e6 100644 --- a/src/SortedMapNode.js +++ b/src/SortedMapNode.js @@ -20,18 +20,25 @@ export class SortedMapNode { } getComparator() {} + // eslint-disable-next-line no-unused-vars get(key, notSetValue) {} + // eslint-disable-next-line no-unused-vars upsert(ownerID, key, value, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars remove(ownerID, key, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars fastRemove(ownerID, key, didChangeSize, didAlter) {} + // eslint-disable-next-line no-unused-vars iterate(fn, reverse) {} + // eslint-disable-next-line no-unused-vars print(level, maxDepth) {} + // eslint-disable-next-line no-unused-vars checkConsistency(printFlag) {} } @@ -44,7 +51,9 @@ export class SortedMapPacker { export class SortedMapNodeFactory { // eslint-disable-next-line no-unused-vars createNode(comparator, options, ownerID, entries, nodes) {} + createPacker() {} + // eslint-disable-next-line no-unused-vars createIterator(map, type, reverse) {} } diff --git a/src/SortedSet.js b/src/SortedSet.js index e000ed5b13..7925064384 100644 --- a/src/SortedSet.js +++ b/src/SortedSet.js @@ -39,6 +39,8 @@ export class SortedSet extends Set { options = SortedSet.defaultOptions; } } + + // eslint-disable-next-line no-constructor-return return value === null || value === undefined ? emptySortedSet(comparator, options) : isSortedSet(value) && diff --git a/type-definitions/immutable.d.ts b/type-definitions/immutable.d.ts index faa56ec33e..379168ddb7 100644 --- a/type-definitions/immutable.d.ts +++ b/type-definitions/immutable.d.ts @@ -1638,7 +1638,7 @@ declare namespace Immutable { * Map { "city": "Seattle", "state": "WA" }: List [ "47°37′N", "122°20′W" ] } * ``` */ - export module SortedMap { + namespace SortedMap { /** * True if the provided value is a SortedMap * @@ -1785,17 +1785,17 @@ declare namespace Immutable { export function SortedMap( collection?: Iterable<[K, V]>, comparator?: (a: K, b: K) => number, - options?: Object + options?: object ): SortedMap; export function SortedMap( collection: Iterable>, comparator?: (a: T, b: T) => number, - options?: Object + options?: object ): SortedMap; export function SortedMap( obj: { [key: string]: V }, comparator?: (a: string, b: string) => number, - options?: Object + options?: object ): SortedMap; export interface SortedMap extends Map { @@ -2284,7 +2284,7 @@ declare namespace Immutable { * Map { "city": "Seattle", "state": "WA" } } * ``` */ - export module SortedSet { + namespace SortedSet { /** * True if the provided value is a `SortedSet`. * @@ -2459,7 +2459,7 @@ declare namespace Immutable { export function SortedSet( collection?: Iterable, comparator?: (a: T, b: T) => number, - options?: Object + options?: object ): SortedSet; export interface SortedSet extends Set { @@ -5134,7 +5134,7 @@ declare namespace Immutable { */ toSortedMap( comparator?: (a: K, b: K) => number, - options?: Object + options?: object ): SortedMap; /** @@ -5164,7 +5164,7 @@ declare namespace Immutable { */ toSortedSet( comparator?: (a: V, b: V) => number, - options?: Object + options?: object ): SortedSet; /** diff --git a/type-definitions/immutable.js.flow b/type-definitions/immutable.js.flow index 67a496f29c..1b459ac03f 100644 --- a/type-definitions/immutable.js.flow +++ b/type-definitions/immutable.js.flow @@ -119,8 +119,16 @@ declare class _Collection implements ValueObject { toObject(): { [key: string]: V }; toMap(): Map; toOrderedMap(): OrderedMap; + toSortedMap( + comparator?: (a: K, b: K) => number, + options?: SortedMapOptions + ): SortedMap; toSet(): Set; toOrderedSet(): OrderedSet; + toSortedSet( + comparator?: (a: V, b: V) => number, + options?: SortedSetOptions + ): SortedSet; toList(): List; toStack(): Stack; toSeq(): Seq; @@ -282,6 +290,10 @@ declare function isOrdered( ): boolean %checks(maybeOrdered instanceof IndexedCollection || maybeOrdered instanceof OrderedMap || maybeOrdered instanceof OrderedSet); +declare function isSorted( + maybeSorted: mixed +): boolean %checks(maybeSorted instanceof SortedMap || + maybeSorted instanceof SortedSet); declare function isValueObject(maybeValue: mixed): boolean; declare function isSeq(maybeSeq: any): boolean %checks(maybeSeq instanceof Seq); @@ -291,12 +303,18 @@ declare function isMap(maybeMap: any): boolean %checks(maybeMap instanceof Map); declare function isOrderedMap( maybeOrderedMap: any ): boolean %checks(maybeOrderedMap instanceof OrderedMap); +declare function isSortedMap( + maybeOrderedMap: any +): boolean %checks(maybeOrderedMap instanceof OrderedMap); declare function isStack(maybeStack: any): boolean %checks(maybeStack instanceof Stack); declare function isSet(maybeSet: any): boolean %checks(maybeSet instanceof Set); declare function isOrderedSet( maybeOrderedSet: any ): boolean %checks(maybeOrderedSet instanceof OrderedSet); +declare function isSortedSet( + maybeOrderedSet: any +): boolean %checks(maybeOrderedSet instanceof OrderedSet); declare function isRecord( maybeRecord: any ): boolean %checks(maybeRecord instanceof Record); @@ -1298,6 +1316,120 @@ declare class OrderedMap flatten(shallow?: boolean): OrderedMap; } +declare function isSortedMap( + maybeSortedMap: mixed +): boolean %checks(maybeSortedMap instanceof SortedMap); +declare type SortedMapOptions = {| type?: 'btree', btreeOrder?: number |}; +declare class SortedMap + extends Map + mixins UpdatableInCollection +{ + static ( + values?: Iterable<[K, V]> | PlainObjInput, + comparator?: (a: K, b: K) => number, + options?: SortedMapOptions + ): SortedMap; + + static isSortedMap: typeof isSortedMap; + + size: number; + + set(key: K_, value: V_): SortedMap; + delete(key: K): this; + remove(key: K): this; + clear(): this; + + update(updater: (value: this) => U): U; + update(key: K, updater: (value: V) => V_): SortedMap; + update( + key: K, + notSetValue: V_, + updater: (value: V) => V_ + ): SortedMap; + + merge( + ...collections: (Iterable<[K_, V_]> | PlainObjInput)[] + ): SortedMap; + concat( + ...collections: (Iterable<[K_, V_]> | PlainObjInput)[] + ): SortedMap; + + mergeWith( + merger: (oldVal: V, newVal: W, key: K) => X, + ...collections: (Iterable<[K_, W]> | PlainObjInput)[] + ): SortedMap; + + mergeDeep( + ...collections: (Iterable<[K_, V_]> | PlainObjInput)[] + ): SortedMap; + + mergeDeepWith( + merger: (oldVal: any, newVal: any, key: any) => mixed, + ...collections: (Iterable<[K_, V_]> | PlainObjInput)[] + ): Map; + + mergeIn( + keyPath: Iterable, + ...collections: (Iterable | PlainObjInput)[] + ): this; + mergeDeepIn( + keyPath: Iterable, + ...collections: (Iterable | PlainObjInput)[] + ): this; + + withMutations(mutator: (mutable: this) => mixed): this; + asMutable(): this; + wasAltered(): boolean; + asImmutable(): this; + + // Override specialized return types + + flip(): SortedMap; + + filter(predicate: typeof Boolean): SortedMap>; + filter( + predicate: (value: V, key: K, iter: this) => mixed, + context?: mixed + ): SortedMap; + + map( + mapper: (value: V, key: K, iter: this) => M, + context?: mixed + ): SortedMap; + + mapKeys( + mapper: (key: K, value: V, iter: this) => M, + context?: mixed + ): SortedMap; + + mapEntries( + mapper: (entry: [K, V], index: number, iter: this) => [KM, VM], + context?: mixed + ): SortedMap; + + flatMap( + mapper: (value: V, key: K, iter: this) => Iterable<[KM, VM]>, + context?: mixed + ): SortedMap; + + flatten(depth?: number): SortedMap; + flatten(shallow?: boolean): SortedMap; + + getComparator(): (a: K, b: K) => number; + + getOptions(): SortedMapOptions; + + pack(values?: Iterable<[K, V]> | PlainObjInput): this; + + from(key: K, backwards?: boolean): Seq; + + fromIndex(index: number, backwards?: boolean): Seq; + + checkConsistency(printFlag: boolean): number; + + print(maxDepth: number): this; +} + declare function isSet(maybeSet: mixed): boolean %checks(maybeSet instanceof Set); declare class Set<+T> extends SetCollection { @@ -1492,6 +1624,147 @@ declare class OrderedSet<+T> extends Set { ): OrderedSet; } +// Overrides except for `isSortedSet` are for specialized return types +declare function isSortedSet( + maybeSortedSet: mixed +): boolean %checks(maybeSortedSet instanceof SortedSet); +declare type SortedSetOptions = SortedMapOptions; +declare class SortedSet<+T> extends Set { + static (values?: Iterable): SortedSet; + + static of(...values: T[]): SortedSet; + static fromKeys( + values: Iterable<[T, mixed]> | PlainObjInput + ): SortedSet; + + static isSortedSet: typeof isSortedSet; + + size: number; + + add(value: U): SortedSet; + union(...collections: Iterable[]): SortedSet; + merge(...collections: Iterable[]): SortedSet; + concat(...collections: Iterable[]): SortedSet; + intersect(...collections: Iterable[]): SortedSet; + + filter(predicate: typeof Boolean): SortedSet<$NonMaybeType>; + filter( + predicate: (value: T, value: T, iter: this) => mixed, + context?: mixed + ): SortedSet; + + map( + mapper: (value: T, value: T, iter: this) => M, + context?: mixed + ): SortedSet; + + flatMap( + mapper: (value: T, value: T, iter: this) => Iterable, + context?: mixed + ): SortedSet; + + flatten(depth?: number): SortedSet; + flatten(shallow?: boolean): SortedSet; + + zip(a: Iterable, ..._: []): SortedSet<[T, A]>; + zip(a: Iterable, b: Iterable, ..._: []): SortedSet<[T, A, B]>; + zip( + a: Iterable, + b: Iterable, + c: Iterable, + ..._: [] + ): SortedSet<[T, A, B, C]>; + zip( + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + ..._: [] + ): SortedSet<[T, A, B, C, D]>; + zip( + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + e: Iterable, + ..._: [] + ): SortedSet<[T, A, B, C, D, E]>; + + zipAll(a: Iterable, ..._: []): SortedSet<[T | void, A | void]>; + zipAll( + a: Iterable, + b: Iterable, + ..._: [] + ): SortedSet<[T | void, A | void, B | void]>; + zipAll( + a: Iterable, + b: Iterable, + c: Iterable, + ..._: [] + ): SortedSet<[T | void, A | void, B | void, C | void]>; + zipAll( + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + ..._: [] + ): SortedSet<[T | void, A | void, B | void, C | void, D | void]>; + zipAll( + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + e: Iterable, + ..._: [] + ): SortedSet<[T | void, A | void, B | void, C | void, D | void, E | void]>; + + zipWith( + zipper: (value: T, a: A) => R, + a: Iterable, + ..._: [] + ): SortedSet; + zipWith( + zipper: (value: T, a: A, b: B) => R, + a: Iterable, + b: Iterable, + ..._: [] + ): SortedSet; + zipWith( + zipper: (value: T, a: A, b: B, c: C) => R, + a: Iterable, + b: Iterable, + c: Iterable, + ..._: [] + ): SortedSet; + zipWith( + zipper: (value: T, a: A, b: B, c: C, d: D) => R, + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + ..._: [] + ): SortedSet; + zipWith( + zipper: (value: T, a: A, b: B, c: C, d: D, e: E) => R, + a: Iterable, + b: Iterable, + c: Iterable, + d: Iterable, + e: Iterable, + ..._: [] + ): SortedSet; + + getComparator(): (a: T, b: T) => number; + + getOptions(): SortedSetOptions; + + pack(values?: Iterable): this; + + from(value: T, backwards?: boolean): Seq; + + fromIndex(index: number, backwards?: boolean): Seq; +} + declare function isStack( maybeStack: mixed ): boolean %checks(maybeStack instanceof Stack); @@ -2319,6 +2592,8 @@ export { Map, OrderedMap, OrderedSet, + SortedMap, + SortedSet, Range, Repeat, Record, @@ -2333,6 +2608,7 @@ export { isIndexed, isAssociative, isOrdered, + isSorted, isRecord, isValueObject, get, @@ -2359,6 +2635,8 @@ export default { Map, OrderedMap, OrderedSet, + SortedMap, + SortedSet, PairSorting, Range, Repeat, @@ -2376,6 +2654,7 @@ export default { isIndexed, isAssociative, isOrdered, + isSorted, isRecord, isValueObject,