diff --git a/src/mol-base/_spec/collections.spec.ts b/src/mol-base/_spec/collections.spec.ts index 4b93ce45a8476b9f82ff5a7b7e5157e5876f7349..89e956cd8266b505cb6dae62471818c3d9b3834c 100644 --- a/src/mol-base/_spec/collections.spec.ts +++ b/src/mol-base/_spec/collections.spec.ts @@ -10,6 +10,8 @@ import * as Sort from '../collections/sort' import OrderedSet from '../collections/ordered-set' import LinkedIndex from '../collections/linked-index' import EquivalenceClasses from '../collections/equivalence-classes' +import Interval from '../collections/interval' +import SortedArray from '../collections/sorted-array' function iteratorToArray<T>(it: Iterator<T>): T[] { const ret = []; @@ -125,6 +127,114 @@ describe('qsort-dual array', () => { test('shuffled', data, true); }) +describe('interval', () => { + function testI(name: string, a: Interval, b: Interval) { + it(name, () => expect(Interval.areEqual(a, b)).toBe(true)); + } + + function test(name: string, a: any, b: any) { + it(name, () => expect(a).toEqual(b)); + } + + const e = Interval.Empty; + const r05 = Interval.ofRange(0, 5); + const se05 = Interval.ofBounds(0, 5); + + test('size', Interval.size(e), 0); + test('size', Interval.size(r05), 6); + test('size', Interval.size(se05), 5); + + test('min/max', [Interval.min(e), Interval.max(e)], [0, -1]); + test('min/max', [Interval.min(r05), Interval.max(r05)], [0, 5]); + test('min/max', [Interval.min(se05), Interval.max(se05)], [0, 4]); + + test('start/end', [Interval.start(e), Interval.end(e)], [0, 0]); + test('start/end', [Interval.start(r05), Interval.end(r05)], [0, 6]); + test('start/end', [Interval.start(se05), Interval.end(se05)], [0, 5]); + + test('has', Interval.has(e, 5), false); + test('has', Interval.has(r05, 5), true); + test('has', Interval.has(r05, 6), false); + test('has', Interval.has(r05, -1), false); + test('has', Interval.has(se05, 5), false); + test('has', Interval.has(se05, 4), true); + + test('indexOf', Interval.indexOf(e, 5), -1); + test('indexOf', Interval.indexOf(r05, 5), 5); + test('indexOf', Interval.indexOf(r05, 6), -1); + + test('getAt', Interval.getAt(r05, 5), 5); + + test('areEqual', Interval.areEqual(r05, se05), false); + test('areIntersecting1', Interval.areIntersecting(r05, se05), true); + test('areIntersecting2', Interval.areIntersecting(r05, e), false); + test('areIntersecting3', Interval.areIntersecting(e, r05), false); + test('areIntersecting4', Interval.areIntersecting(e, e), true); + + test('areIntersecting5', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(-4, 3)), true); + test('areIntersecting6', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(-4, -3)), false); + test('areIntersecting7', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(1, 2)), true); + test('areIntersecting8', Interval.areIntersecting(Interval.ofRange(0, 5), Interval.ofRange(3, 6)), true); + + test('isSubInterval', Interval.isSubInterval(Interval.ofRange(0, 5), Interval.ofRange(3, 6)), false); + test('isSubInterval', Interval.isSubInterval(Interval.ofRange(0, 5), Interval.ofRange(3, 5)), true); + + testI('intersect', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(-4, 3)), Interval.ofRange(0, 3)); + testI('intersect1', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(1, 3)), Interval.ofRange(1, 3)); + testI('intersect2', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(3, 5)), Interval.ofRange(3, 5)); + testI('intersect3', Interval.intersect(Interval.ofRange(0, 5), Interval.ofRange(-4, -3)), Interval.Empty); + + test('predIndex1', Interval.findPredecessorIndex(r05, 5), 5); + test('predIndex2', Interval.findPredecessorIndex(r05, -1), 0); + test('predIndex3', Interval.findPredecessorIndex(r05, 6), 6); + test('predIndexInt', Interval.findPredecessorIndexInInterval(r05, 0, Interval.ofRange(2, 3)), 2); + test('predIndexInt1', Interval.findPredecessorIndexInInterval(r05, 4, Interval.ofRange(2, 3)), 4); + + testI('findRange', Interval.findRange(r05, 2, 3), Interval.ofRange(2, 3)); +}); + +describe('sortedArray', () => { + function testI(name: string, a: Interval, b: Interval) { + it(name, () => expect(Interval.areEqual(a, b)).toBe(true)); + } + + function test(name: string, a: any, b: any) { + it(name, () => expect(a).toEqual(b)); + } + + const a1234 = SortedArray.ofSortedArray([1, 2, 3, 4]); + const a2468 = SortedArray.ofSortedArray([2, 4, 6, 8]); + + test('size', SortedArray.size(a1234), 4); + + test('min/max', [SortedArray.min(a1234), SortedArray.max(a1234)], [1, 4]); + test('start/end', [SortedArray.start(a1234), SortedArray.end(a1234)], [1, 5]); + + test('has', SortedArray.has(a1234, 5), false); + test('has', SortedArray.has(a1234, 4), true); + + it('has-all', () => { + for (let i = 1; i <= 4; i++) expect(SortedArray.has(a1234, i)).toBe(true); + }); + + test('indexOf', SortedArray.indexOf(a2468, 5), -1); + test('indexOf', SortedArray.indexOf(a2468, 2), 0); + + test('getAt', SortedArray.getAt(a2468, 1), 4); + + test('areEqual', SortedArray.areEqual(a2468, a2468), true); + test('areEqual1', SortedArray.areEqual(a2468, SortedArray.create([4, 2, 8, 6])), true); + test('areEqual2', SortedArray.areEqual(a1234, a2468), false); + + test('predIndex1', SortedArray.findPredecessorIndex(a1234, 5), 4); + test('predIndex2', SortedArray.findPredecessorIndex(a1234, 2), 1); + test('predIndex3', SortedArray.findPredecessorIndex(a2468, 4), 1); + test('predIndex4', SortedArray.findPredecessorIndex(a2468, 3), 1); + test('predIndexInt', SortedArray.findPredecessorIndexInInterval(a1234, 0, Interval.ofRange(2, 3)), 2); + + testI('findRange', SortedArray.findRange(a2468, 2, 4), Interval.ofRange(0, 1)); +}); + describe('ordered set', () => { function ordSetToArray(set: OrderedSet) { const ret = []; diff --git a/src/mol-base/collections/impl/interval.ts b/src/mol-base/collections/impl/interval.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0290c22915b6942727b2d5cb32b487aec261789 --- /dev/null +++ b/src/mol-base/collections/impl/interval.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import IntTuple from '../int-tuple' + +export const Empty = IntTuple.Zero; +export function ofRange(min: number, max: number) { return max < min ? Empty : IntTuple.create(min, max + 1); } +export function ofBounds(min: number, max: number) { return max <= min ? Empty : IntTuple.create(min, max); } +export const is = IntTuple.is; + +export const start = IntTuple.fst; +export const end = IntTuple.snd; +export const min = IntTuple.fst; +export function max(i: IntTuple) { return IntTuple.snd(i) - 1; } +export function size(i: IntTuple) { return IntTuple.snd(i) - IntTuple.fst(i); } +export const hashCode = IntTuple.hashCode; + +export function has(int: IntTuple, v: number) { return IntTuple.fst(int) <= v && v < IntTuple.snd(int); } +export function indexOf(int: IntTuple, x: number) { const m = start(int); return x >= m && x < end(int) ? x - m : -1; } +export function getAt(int: IntTuple, i: number) { return IntTuple.fst(int) + i; } + +export const areEqual = IntTuple.areEqual; +export function areIntersecting(a: IntTuple, b: IntTuple) { + const sa = size(a), sb = size(b); + if (sa === 0 && sb === 0) return true; + return sa > 0 && sb > 0 && max(a) >= min(b) && min(a) <= max(b); +} +export function isSubInterval(a: IntTuple, b: IntTuple) { + if (!size(a)) return size(b) === 0; + if (!size(b)) return true; + return start(a) <= start(b) && end(a) >= end(b); +} + +export function findPredecessorIndex(int: IntTuple, v: number) { + const s = start(int); + if (v <= s) return 0; + const e = end(int); + if (v >= e) return e - s; + return v - s; +} + +export function findPredecessorIndexInInterval(int: IntTuple, x: number, bounds: IntTuple) { + const ret = findPredecessorIndex(int, x); + const s = start(bounds), e = end(bounds); + return ret <= s ? s : ret >= e ? e : ret; +} + +export function findRange(int: IntTuple, min: number, max: number) { + return ofBounds(findPredecessorIndex(int, min), findPredecessorIndex(int, max + 1)); +} + +export function intersect(a: IntTuple, b: IntTuple) { + if (!areIntersecting(a, b)) return Empty; + return ofBounds(Math.max(start(a), start(b)), Math.min(end(a), end(b))); +} \ No newline at end of file diff --git a/src/mol-base/collections/impl/sorted-array.ts b/src/mol-base/collections/impl/sorted-array.ts new file mode 100644 index 0000000000000000000000000000000000000000..134b1dbf3dd05afafec623a76fadf887e11059e0 --- /dev/null +++ b/src/mol-base/collections/impl/sorted-array.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { sortArray } from '../sort' +import { hash3, hash4 } from '../hash-functions' +import Interval from '../interval' + +type Nums = ArrayLike<number> + +export function ofSortedArray(xs: Nums) { + if (xs.length < 1) throw new Error('Sorted arrays must be non-empty.'); + return xs; +} +export function ofUnsortedArray(xs: Nums) { sortArray(xs); return xs; } +export function is(xs: any): xs is Nums { return xs && (xs instanceof Array || !!xs.buffer); } + +export function start(xs: Nums) { return xs[0]; } +export function end(xs: Nums) { return xs[xs.length - 1] + 1; } +export function min(xs: Nums) { return xs[0]; } +export function max(xs: Nums) { return xs[xs.length - 1]; } +export function size(xs: Nums) { return xs.length; } +export function hashCode(xs: Nums) { + // hash of tuple (size, min, max, mid) + const s = xs.length; + if (!s) return 0; + if (s > 2) return hash4(s, xs[0], xs[s - 1], xs[s << 1]); + return hash3(s, xs[0], xs[s - 1]); +} + +export function indexOf(xs: Nums, v: number) { + const l = xs.length; + return l === 0 ? -1 : xs[0] <= v && v <= xs[l - 1] ? binarySearchRange(xs, v, 0, l) : -1; +} +export function has(xs: Nums, v: number) { return indexOf(xs, v) >= 0; } + +export function getAt(xs: Nums, i: number) { return xs[i]; } + +export function areEqual(a: Nums, b: Nums) { + if (a === b) return true; + const aSize = a.length; + if (aSize !== b.length || a[0] !== b[0] || a[aSize - 1] !== b[aSize - 1]) return false; + for (let i = 0; i < aSize; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +export function findPredecessorIndex(xs: Nums, v: number) { + const len = xs.length; + if (v <= xs[0]) return 0; + if (v > xs[len - 1]) return len; + return binarySearchPredIndexRange(xs, v, 0, len); +} + +export function findPredecessorIndexInInterval(xs: Nums, v: number, bounds: Interval) { + const s = Interval.start(bounds), e = Interval.end(bounds); + if (v <= xs[s]) return s; + if (e > s && v > xs[e - 1]) return e; + return binarySearchPredIndexRange(xs, v, s, e); +} + +export function findRange(xs: Nums, min: number, max: number) { + return Interval.ofBounds(findPredecessorIndex(xs, min), findPredecessorIndex(xs, max + 1)); +} + +function binarySearchRange(xs: Nums, value: number, start: number, end: number) { + let min = start, max = end - 1; + while (min <= max) { + if (min + 11 > max) { + for (let i = min; i <= max; i++) { + if (value === xs[i]) return i; + } + return -1; + } + + const mid = (min + max) >> 1; + const v = xs[mid]; + if (value < v) max = mid - 1; + else if (value > v) min = mid + 1; + else return mid; + } + return -1; +} + +function binarySearchPredIndexRange(xs: Nums, value: number, start: number, end: number) { + let min = start, max = end - 1; + while (min < max) { + const mid = (min + max) >> 1; + const v = xs[mid]; + if (value < v) max = mid - 1; + else if (value > v) min = mid + 1; + else return mid; + } + if (min > max) return max + 1; + return xs[min] >= value ? min : min + 1; +} \ No newline at end of file diff --git a/src/mol-base/collections/interval.ts b/src/mol-base/collections/interval.ts index 722d48d395b56036bf5c21ff70c08bafdcbdb555..5736efc0d973341eb2ad3eca6818f613f770b1e6 100644 --- a/src/mol-base/collections/interval.ts +++ b/src/mol-base/collections/interval.ts @@ -4,44 +4,39 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import IntTuple from './int-tuple' +import * as Impl from './impl/interval' -/** Closed/Open inteval [a, b) (iterate as a <= i < b) */ namespace Interval { export const Empty: Interval = Impl.Empty as any; - // export const create: (min: number, max: number) => Interval = Impl.create as any; + /** Create interval [min, max] */ + export const ofRange: (min: number, max: number) => Interval = Impl.ofRange as any; + /** Create interval [min, max) */ + export const ofBounds: (start: number, end: number) => Interval = Impl.ofBounds as any; + export const is: (v: any) => v is Interval = Impl.is as any; - // export const indexOf: (set: Interval, x: number) => number = Impl.indexOf as any; - // export const getAt: (set: Interval, i: number) => number = Impl.getAt as any; + export const has: (interval: Interval, x: number) => boolean = Impl.has as any; + export const indexOf: (interval: Interval, x: number) => number = Impl.indexOf as any; + export const getAt: (interval: Interval, i: number) => number = Impl.getAt as any; - // export const start: (set: Interval) => number = Impl.start as any; - // export const end: (set: Interval) => number = Impl.end as any; + export const start: (interval: Interval) => number = Impl.start as any; + export const end: (interval: Interval) => number = Impl.end as any; + export const min: (interval: Interval) => number = Impl.min as any; + export const max: (interval: Interval) => number = Impl.max as any; + export const size: (interval: Interval) => number = Impl.size as any; + export const hashCode: (interval: Interval) => number = Impl.hashCode as any; - // export const min: (set: Interval) => number = Impl.min as any; - // export const max: (set: Interval) => number = Impl.max as any; - // export const size: (set: Interval) => number = Impl.size as any; - // export const hashCode: (set: Interval) => number = Impl.hashCode as any; + export const areEqual: (a: Interval, b: Interval) => boolean = Impl.areEqual as any; + export const areIntersecting: (a: Interval, b: Interval) => boolean = Impl.areIntersecting as any; + export const isSubInterval: (a: Interval, b: Interval) => boolean = Impl.isSubInterval as any; - // export const areEqual: (a: Interval, b: Interval) => boolean = Impl.areEqual as any; - // export const areIntersecting: (a: Interval, b: Interval) => boolean = Impl.areIntersecting as any; - // export const isSubset: (a: Interval, b: Interval) => boolean = Impl.isSubset as any; + export const findPredecessorIndex: (interval: Interval, x: number) => number = Impl.findPredecessorIndex as any; + export const findPredecessorIndexInInterval: (interval: Interval, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any; + export const findRange: (interval: Interval, min: number, max: number) => Interval = Impl.findRange as any; + export const intersect: (a: Interval, b: Interval) => Interval = Impl.intersect as any; } interface Interval { '@type': 'int-interval' } -export default Interval - -namespace Impl {export const Empty = IntTuple.Zero; - - export function create(min: number, max: number) { return max < min ? Empty : IntTuple.create(min, max + 1); } - - export const start = IntTuple.fst - export const end = IntTuple.snd - export const min = IntTuple.fst - export function max(i: IntTuple) { return IntTuple.snd(i) + 1; } - export function size(i: IntTuple) { return IntTuple.snd(i) - IntTuple.fst(i); } - - export function has(int: IntTuple, v: number) { return IntTuple.fst(int) <= v && v < IntTuple.snd(int); } -} \ No newline at end of file +export default Interval \ No newline at end of file diff --git a/src/mol-base/collections/sorted-array.ts b/src/mol-base/collections/sorted-array.ts new file mode 100644 index 0000000000000000000000000000000000000000..11c5c31abb79d483fa9cbf204ab8133c71522585 --- /dev/null +++ b/src/mol-base/collections/sorted-array.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as Impl from './impl/sorted-array' +import Interval from './interval' + +namespace SortedArray { + /** Create interval [min, max] */ + export const create: (xs: ArrayLike<number>) => SortedArray = Impl.ofUnsortedArray as any; + /** Create interval [min, max) */ + export const ofSortedArray: (xs: ArrayLike<number>) => SortedArray = Impl.ofSortedArray as any; + export const is: (v: any) => v is Interval = Impl.is as any; + + export const has: (interval: SortedArray, x: number) => boolean = Impl.has as any; + export const indexOf: (interval: SortedArray, x: number) => number = Impl.indexOf as any; + export const getAt: (interval: SortedArray, i: number) => number = Impl.getAt as any; + + export const start: (interval: SortedArray) => number = Impl.start as any; + export const end: (interval: SortedArray) => number = Impl.end as any; + export const min: (interval: SortedArray) => number = Impl.min as any; + export const max: (interval: SortedArray) => number = Impl.max as any; + export const size: (interval: SortedArray) => number = Impl.size as any; + export const hashCode: (interval: SortedArray) => number = Impl.hashCode as any; + + export const areEqual: (a: SortedArray, b: SortedArray) => boolean = Impl.areEqual as any; + + export const findPredecessorIndex: (interval: SortedArray, x: number) => number = Impl.findPredecessorIndex as any; + export const findPredecessorIndexInInterval: (interval: SortedArray, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any; + export const findRange: (interval: SortedArray, min: number, max: number) => Interval = Impl.findRange as any; +} + +interface SortedArray { '@type': 'int-sorted-array' } + +export default SortedArray \ No newline at end of file