├── packages ├── map │ ├── src │ │ ├── curried.ts │ │ ├── internals │ │ │ ├── index.ts │ │ │ └── map.ts │ │ ├── functions │ │ │ ├── index.ts │ │ │ ├── unwrap.ts │ │ │ └── stopgaps.ts │ │ └── index.ts │ ├── .npmignore │ ├── package.json │ ├── tests │ │ ├── get.ts │ │ ├── getSize.ts │ │ ├── has.ts │ │ ├── asMutable.ts │ │ ├── updateMap.ts │ │ ├── asImmutable.ts │ │ ├── update.ts │ │ ├── remove.ts │ │ └── set.ts │ └── README.md ├── set │ ├── .npmignore │ ├── src │ │ ├── internals │ │ │ ├── index.ts │ │ │ └── set.ts │ │ ├── functions │ │ │ ├── index.ts │ │ │ ├── unwrap.ts │ │ │ └── stopgaps.ts │ │ └── index.ts │ ├── tests │ │ └── index.ts │ ├── package.json │ └── README.md ├── list │ ├── .npmignore │ ├── visualiser │ │ ├── .babelrc │ │ ├── readme.md │ │ ├── home.ejs │ │ ├── webpack.config.js │ │ └── package.json │ ├── src │ │ ├── index.ts │ │ ├── functions │ │ │ ├── iterate.ts │ │ │ ├── hasIndex.ts │ │ │ ├── size.ts │ │ │ ├── empty.ts │ │ │ ├── thaw.ts │ │ │ ├── freeze.ts │ │ │ ├── get.ts │ │ │ ├── set.ts │ │ │ ├── equality.ts │ │ │ ├── index.ts │ │ │ ├── remove.ts │ │ │ ├── from.ts │ │ │ ├── insert.ts │ │ │ ├── unwrap.ts │ │ │ ├── update.ts │ │ │ ├── concat.ts │ │ │ ├── slice.ts │ │ │ ├── prepend.ts │ │ │ └── append.ts │ │ └── internals │ │ │ ├── index.ts │ │ │ ├── debug.ts │ │ │ └── common.ts │ ├── tests │ │ ├── functions │ │ │ ├── empty.ts │ │ │ ├── size.ts │ │ │ ├── from.ts │ │ │ ├── iterate.ts │ │ │ ├── hasIndex.ts │ │ │ ├── append.ts │ │ │ ├── prepend.ts │ │ │ ├── set.ts │ │ │ ├── mutability.ts │ │ │ ├── update.ts │ │ │ ├── insert.ts │ │ │ ├── composite.ts │ │ │ └── unwrap.ts │ │ └── internals │ │ │ └── traversal.ts │ └── package.json ├── red-black-tree │ ├── .npmignore │ ├── visualiser │ │ ├── .babelrc │ │ ├── app │ │ │ ├── core │ │ │ │ ├── palette.styl │ │ │ │ ├── renderer.js │ │ │ │ ├── navigation.js │ │ │ │ ├── styles.styl │ │ │ │ ├── versions.js │ │ │ │ ├── app.js │ │ │ │ └── console.js │ │ │ ├── data │ │ │ │ ├── index.js │ │ │ │ ├── styles.styl │ │ │ │ ├── data.js │ │ │ │ └── model.js │ │ │ └── index.js │ │ ├── index.html │ │ ├── webpack.config.js │ │ └── package.json │ ├── src │ │ ├── index.ts │ │ ├── internals │ │ │ ├── index.ts │ │ │ ├── debug.ts │ │ │ ├── node.ts │ │ │ ├── red-black-tree.ts │ │ │ └── iterator.ts │ │ ├── functions │ │ │ ├── index.ts │ │ │ ├── size.ts │ │ │ ├── freeze.ts │ │ │ ├── equality.ts │ │ │ ├── empty.ts │ │ │ ├── thaw.ts │ │ │ ├── set.ts │ │ │ ├── get.ts │ │ │ ├── last.ts │ │ │ ├── first.ts │ │ │ ├── from.ts │ │ │ └── update.ts │ │ └── curried.ts │ ├── package.json │ └── tests │ │ └── functions │ │ ├── empty.ts │ │ ├── size.ts │ │ ├── get.ts │ │ ├── first.ts │ │ ├── last.ts │ │ ├── unwrap.ts │ │ ├── mutability.ts │ │ ├── remove.ts │ │ ├── update.ts │ │ └── set.ts └── core │ ├── tests │ └── index.ts │ ├── .npmignore │ ├── src │ ├── index.ts │ ├── functions.ts │ ├── iterator.ts │ ├── ownership.ts │ ├── hash.ts │ ├── collection.ts │ └── array.ts │ ├── package.json │ └── README.md ├── src ├── index.ts ├── internals │ ├── index.ts │ └── common.ts └── functions │ ├── index.ts │ ├── get.ts │ ├── has.ts │ ├── set.ts │ ├── update.ts │ └── from.ts ├── tests └── index.ts ├── mocha.opts ├── .assets └── logo.png ├── .npmignore ├── .travis.yml ├── .gitignore ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.json ├── LICENSE ├── tslint.json ├── docs └── index.md ├── package.json └── gulpfile.js /packages/map/src/curried.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions'; -------------------------------------------------------------------------------- /src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; -------------------------------------------------------------------------------- /packages/map/.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | src/ 3 | tests/ -------------------------------------------------------------------------------- /packages/set/.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | src/ 3 | tests/ -------------------------------------------------------------------------------- /packages/map/src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './map'; -------------------------------------------------------------------------------- /packages/set/src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './set'; -------------------------------------------------------------------------------- /packages/list/.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | src/ 3 | tests/ 4 | visualiser/ -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | // placeholder so that Mocha doesn't throw an error -------------------------------------------------------------------------------- /packages/red-black-tree/.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | src/ 3 | tests/ 4 | visualiser/ -------------------------------------------------------------------------------- /packages/set/tests/index.ts: -------------------------------------------------------------------------------- 1 | // placeholder so that Mocha doesn't throw an error -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --require source-map-support/register 2 | --ui tdd 3 | ./.build/**/*.js -------------------------------------------------------------------------------- /packages/core/tests/index.ts: -------------------------------------------------------------------------------- 1 | // placeholder so that Mocha doesn't throw an error -------------------------------------------------------------------------------- /packages/list/visualiser/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-native-modules"] 3 | } -------------------------------------------------------------------------------- /.assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/collectable/master/.assets/logo.png -------------------------------------------------------------------------------- /packages/list/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions'; 2 | export {List} from './internals'; -------------------------------------------------------------------------------- /packages/map/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stopgaps'; 2 | export * from './unwrap'; -------------------------------------------------------------------------------- /packages/set/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stopgaps'; 2 | export * from './unwrap'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .devnotes/ 3 | .vscode/ 4 | src/ 5 | tests/ 6 | docs/ 7 | packages/ 8 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .devnotes/ 3 | .vscode/ 4 | src/ 5 | tests/ 6 | visualiser/ -------------------------------------------------------------------------------- /packages/map/src/index.ts: -------------------------------------------------------------------------------- 1 | export {HashMap as Map} from './internals'; 2 | export * from './functions'; -------------------------------------------------------------------------------- /packages/set/src/index.ts: -------------------------------------------------------------------------------- 1 | export {HashSet as Set} from './internals'; 2 | export * from './functions'; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | - 7 6 | script: npm run build-all 7 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es2015-modules-commonjs"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions'; 2 | export {RedBlackTreeEntry, Comparator} from './internals'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .docs/ 2 | .build/ 3 | .devnotes/ 4 | .vscode/settings.json 5 | .assets/working-files/ 6 | node_modules/ 7 | lib/ 8 | *.log -------------------------------------------------------------------------------- /src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './from'; 2 | export * from './get'; 3 | export * from './has'; 4 | export * from './set'; 5 | export * from './update'; -------------------------------------------------------------------------------- /packages/list/visualiser/readme.md: -------------------------------------------------------------------------------- 1 | This is a crude visualisation tool that I created to assist with development and 2 | testing. At this stage, it is not intended for general consumption. -------------------------------------------------------------------------------- /packages/list/src/functions/iterate.ts: -------------------------------------------------------------------------------- 1 | import {List, createIterator} from '../internals'; 2 | 3 | export function iterate(list: List): IterableIterator { 4 | return createIterator(list); 5 | } 6 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/palette.styl: -------------------------------------------------------------------------------- 1 | $palette-1 = #ec4857 2 | $palette-2 = #f0c670 3 | $palette-3 = #95b986 4 | $palette-3t = #f3f5f2 5 | $palette-4 = #2299a4 6 | $palette-5 = #18416c 7 | -------------------------------------------------------------------------------- /packages/list/src/functions/hasIndex.ts: -------------------------------------------------------------------------------- 1 | import {List, verifyIndex} from '../internals'; 2 | 3 | export function hasIndex(index: number, list: List): boolean { 4 | return verifyIndex(list._size, index) !== -1; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './array'; 2 | export * from './collection'; 3 | export * from './functions'; 4 | export * from './hash'; 5 | export * from './iterator'; 6 | export * from './ownership'; 7 | export * from './random'; -------------------------------------------------------------------------------- /packages/red-black-tree/src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './red-black-tree'; 2 | export * from './rebalance'; 3 | export * from './path'; 4 | export * from './node'; 5 | export * from './ops'; 6 | export * from './iterator'; 7 | export * from './find'; -------------------------------------------------------------------------------- /packages/list/src/functions/size.ts: -------------------------------------------------------------------------------- 1 | import {List} from '../internals'; 2 | 3 | export function size(list: List): number { 4 | return list._size; 5 | } 6 | 7 | export function isEmpty(list: List): boolean { 8 | return list._size === 0; 9 | } 10 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/data/index.js: -------------------------------------------------------------------------------- 1 | export {log, setCallback} from '../../../../../.build/packages/red-black-tree/src/internals/debug'; 2 | export {start, isInstanceOfVisualisedType} from './data'; 3 | export {createModel} from './model'; 4 | export {render} from './render'; 5 | -------------------------------------------------------------------------------- /packages/list/src/functions/empty.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from '@collectable/core'; 2 | import {List, createList} from '../internals'; 3 | 4 | var EMPTY: List|undefined; 5 | 6 | export function empty(): List { 7 | return isDefined(EMPTY) ? EMPTY : (EMPTY = createList(false)); 8 | } 9 | -------------------------------------------------------------------------------- /packages/list/tests/functions/empty.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty} from '../../src'; 3 | 4 | suite('[List]', () => { 5 | suite('empty()', () => { 6 | test('should have size 0', () => { 7 | const list = empty(); 8 | assert.strictEqual(list._size, 0); 9 | }); 10 | }); 11 | }); -------------------------------------------------------------------------------- /packages/list/src/internals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './capacity'; 3 | export * from './compact'; 4 | export * from './concat'; 5 | export * from './slice'; 6 | export * from './slot'; 7 | export * from './list'; 8 | export * from './traversal'; 9 | export * from './values'; 10 | export * from './view'; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/.git/objects/**": true 4 | }, 5 | "search.exclude": { 6 | "**/node_modules": true, 7 | "lib/": true 8 | }, 9 | "eslint.enable": false, 10 | "typescript.tsdk": "node_modules/typescript/lib", 11 | "editor.tabSize": 2 12 | } -------------------------------------------------------------------------------- /packages/list/src/functions/thaw.ts: -------------------------------------------------------------------------------- 1 | import {List, ensureMutable} from '../internals'; 2 | import {isMutable} from '@collectable/core'; 3 | 4 | export function thaw(list: List): List { 5 | return ensureMutable(list); 6 | } 7 | 8 | export function isThawed(list: List): boolean { 9 | return isMutable(list._owner); 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-w", "-p", "."], 8 | "showOutput": "silent", 9 | "isWatching": true, 10 | "problemMatcher": "$tsc-watch" 11 | } -------------------------------------------------------------------------------- /packages/list/src/functions/freeze.ts: -------------------------------------------------------------------------------- 1 | import {List, ensureImmutable} from '../internals'; 2 | import {isImmutable} from '@collectable/core'; 3 | 4 | export function freeze(list: List): List { 5 | return isImmutable(list._owner) ? list : ensureImmutable(list, false); 6 | } 7 | 8 | export function isFrozen(list: List): boolean { 9 | return isImmutable(list._owner); 10 | } -------------------------------------------------------------------------------- /packages/list/src/functions/get.ts: -------------------------------------------------------------------------------- 1 | import {List, getAtOrdinal} from '../internals'; 2 | 3 | export function get(index: number, list: List): T|undefined { 4 | return getAtOrdinal(list, index); 5 | } 6 | 7 | export function first(list: List): T|undefined { 8 | return getAtOrdinal(list, 0); 9 | } 10 | 11 | export function last(list: List): T|undefined { 12 | return getAtOrdinal(list, -1); 13 | } 14 | -------------------------------------------------------------------------------- /packages/list/src/functions/set.ts: -------------------------------------------------------------------------------- 1 | import {List, cloneAsMutable, ensureImmutable, setValueAtOrdinal} from '../internals'; 2 | import {isImmutable} from '@collectable/core'; 3 | 4 | export function set(index: number, value: T, list: List): List { 5 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 6 | setValueAtOrdinal(list, index, value); 7 | return immutable ? ensureImmutable(list, true) : list; 8 | } 9 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/index.js: -------------------------------------------------------------------------------- 1 | import {run} from '@motorcycle/run' 2 | import {makeDomComponent} from '@motorcycle/dom'; 3 | import {App} from './core/app'; 4 | 5 | require('./core/styles.styl'); 6 | require('./data/styles.styl'); 7 | 8 | const domDriver = makeDomComponent(document.getElementById('app-root')); 9 | 10 | function effects({view$}) { 11 | const {dom} = domDriver({view$}); 12 | return {dom}; 13 | } 14 | 15 | run(App, effects); 16 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './at'; 2 | export * from './empty'; 3 | export * from './equality'; 4 | export * from './find'; 5 | export * from './first'; 6 | export * from './freeze'; 7 | export * from './from'; 8 | export * from './get'; 9 | export * from './last'; 10 | export * from './remove'; 11 | export * from './set'; 12 | export * from './size'; 13 | export * from './thaw'; 14 | export * from './unwrap'; 15 | export * from './update'; -------------------------------------------------------------------------------- /src/functions/get.ts: -------------------------------------------------------------------------------- 1 | import {Collection, CollectionTypeInfo, isCollection} from '@collectable/core'; 2 | import {isIndexable} from '../internals'; 3 | 4 | export function getIn(path: any[], collection: Collection): any { 5 | var i = 0, value: any = collection, type: CollectionTypeInfo; 6 | while(i < path.length && isCollection(value) && (type = value['@@type'], isIndexable(type))) { 7 | value = type.get(path[i++], value); 8 | } 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /src/functions/has.ts: -------------------------------------------------------------------------------- 1 | import {Collection, CollectionTypeInfo, isCollection} from '@collectable/core'; 2 | import {isIndexable} from '../internals'; 3 | 4 | export function hasIn(path: any[], collection: Collection): boolean { 5 | var i = 0, value: any = collection, type: CollectionTypeInfo; 6 | while(i < path.length && isCollection(value) && (type = value['@@type'], isIndexable(type))) { 7 | if(!type.has(path[i++], value)) return false; 8 | } 9 | return true; 10 | } 11 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/internals/debug.ts: -------------------------------------------------------------------------------- 1 | // ## DEV [[ 2 | var __logCallback: Function; 3 | export function log(...args: any[]): void; 4 | export function log(): void { 5 | if(__logCallback) __logCallback(Array.from(arguments)); 6 | } 7 | export function setCallback(callback: Function): void { 8 | __logCallback = callback; 9 | } 10 | 11 | declare var window; 12 | if(typeof window !== 'undefined') { 13 | window.addEventListener('error', ev => { 14 | log(ev.error); 15 | }); 16 | } 17 | // ]] ## -------------------------------------------------------------------------------- /packages/list/src/functions/equality.ts: -------------------------------------------------------------------------------- 1 | import {List} from '../internals'; 2 | import {size, iterate} from '../functions'; 3 | 4 | export function isEqual(list: List, other: List): boolean { 5 | if(list === other) return true; 6 | if(size(list) !== size(other)) return false; 7 | var ita = iterate(list), itb = iterate(other); 8 | do { 9 | var ca = ita.next(); 10 | var cb = itb.next(); 11 | if(ca.value !== cb.value) return false; 12 | } while(!ca.done); 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /packages/list/visualiser/home.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/list/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './append'; 2 | export * from './concat'; 3 | export * from './empty'; 4 | export * from './equality'; 5 | export * from './freeze'; 6 | export * from './from'; 7 | export * from './get'; 8 | export * from './hasIndex'; 9 | export * from './insert'; 10 | export * from './iterate'; 11 | export * from './prepend'; 12 | export * from './remove'; 13 | export * from './unwrap'; 14 | export * from './set'; 15 | export * from './size'; 16 | export * from './slice'; 17 | export * from './thaw'; 18 | export * from './update'; -------------------------------------------------------------------------------- /packages/list/src/functions/remove.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {List, cloneAsMutable, deleteValues, ensureImmutable} from '../internals'; 3 | 4 | export function remove(index: number, list: List): List { 5 | return removeRange(index, index + 1, list); 6 | } 7 | 8 | export function removeRange(start: number, end: number, list: List): List { 9 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 10 | list = deleteValues(list, start, end); 11 | return immutable ? ensureImmutable(list, true) : list; 12 | } 13 | -------------------------------------------------------------------------------- /packages/list/src/internals/debug.ts: -------------------------------------------------------------------------------- 1 | // ## DEV [[ 2 | export function log(...args: any[]) 3 | export function log() { 4 | publish(Array.from(arguments)); 5 | } 6 | 7 | var __publishCallback: Function; 8 | export function publish(...args: any[]): void; 9 | export function publish(): void { 10 | if(__publishCallback) __publishCallback.apply(null, arguments); 11 | } 12 | export function setCallback(callback: Function): void { 13 | __publishCallback = callback; 14 | } 15 | 16 | declare var window; 17 | if(typeof window !== 'undefined') { 18 | window.addEventListener('error', ev => { 19 | log(ev.error); 20 | }); 21 | } 22 | // ]] ## -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strictNullChecks": true, 7 | "target": "es6", 8 | "sourceMap": true, 9 | "preserveConstEnums": false, 10 | "allowSyntheticDefaultImports": true, 11 | "removeComments": false, 12 | "noUnusedLocals": true, 13 | "outDir": "./.build", 14 | "lib": ["dom", "es5", "es6", "es2015"], 15 | "skipLibCheck": true 16 | }, 17 | "include": [ 18 | "./src/**/*.ts", 19 | "./tests/**/*.ts", 20 | "./packages/*/src/**/*.ts", 21 | "./packages/*/tests/**/*.ts" 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/renderer.js: -------------------------------------------------------------------------------- 1 | import {writeLogs} from './console'; 2 | import {render} from '../data'; 3 | 4 | function mapState({index, model, versions}) { 5 | if(versions.size === 0) return void 0; 6 | const current = versions.get(index); 7 | return Object.assign({index}, current); 8 | } 9 | 10 | export function Renderer({dom, state$}) { 11 | const view$ = state$ 12 | .map(mapState) 13 | .filter(x => x) 14 | .skipRepeatsWith((a, b) => a.index === b.index && a.model === b.model) 15 | .map(state => { 16 | writeLogs(state); 17 | return render(state); 18 | }); 19 | return {view$}; 20 | } -------------------------------------------------------------------------------- /packages/list/src/functions/from.ts: -------------------------------------------------------------------------------- 1 | import {List, createList, appendValues, ensureImmutable} from '../internals'; 2 | 3 | export function fromArray(values: T[]): List { 4 | if(!Array.isArray(values)) { 5 | throw new Error('First argument must be an array of values'); 6 | } 7 | var state = createList(true); 8 | if(values.length > 0) { 9 | appendValues(state, values); 10 | } 11 | return ensureImmutable(state, true); 12 | } 13 | 14 | export function fromIterable(values: Iterable): List { 15 | return fromArray(Array.from(values)); 16 | } 17 | 18 | export function fromArgs(...values: T[]): List { 19 | return fromArray(values); 20 | } -------------------------------------------------------------------------------- /packages/core/src/functions.ts: -------------------------------------------------------------------------------- 1 | export function isDefined(value: T|undefined): value is T { 2 | return value !== void 0; 3 | } 4 | 5 | export function isUndefined(value: T|undefined): value is undefined { 6 | return value === void 0; 7 | } 8 | 9 | export function isNullOrUndefined(value: T|null|undefined): value is null|undefined { 10 | return value === void 0 || value === null; 11 | } 12 | 13 | export function abs(value: number): number { 14 | return value < 0 ? -value : value; 15 | } 16 | 17 | export function min(a: number, b: number): number { 18 | return a <= b ? a : b; 19 | } 20 | 21 | export function max(a: number, b: number): number { 22 | return a >= b ? a : b; 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@collectable/core", 3 | "version": "2.1.0", 4 | "description": "[Collectable.js] Core Module", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "jsnext:main": "lib/module/index.js", 8 | "typings": "lib/typings/index.d.ts", 9 | "scripts": { 10 | "test-dev": "cd ../.. && mocha --require source-map-support/register --ui tdd --watch --bail .build/packages/core/tests/**/*.js" 11 | }, 12 | "files": ["lib"], 13 | "author": "Nathan Ridley ", 14 | "license": "MIT", 15 | "bugs": "https://github.com/frptools/collectable/issues", 16 | "repository": "git@github.com:frptools/collectable.git" 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/iterator.ts: -------------------------------------------------------------------------------- 1 | export class MappableIterator implements IterableIterator { 2 | private it: IterableIterator; 3 | constructor( 4 | private iterable: Iterable, 5 | private map: (value: T) => U 6 | ) { 7 | this.it = >this.iterable[Symbol.iterator](); 8 | } 9 | 10 | next(value?: any): IteratorResult { 11 | var result = >this.it.next(value); 12 | if(result.done) { 13 | result.value = void 0; 14 | } 15 | else { 16 | result.value = this.map(result.value); 17 | } 18 | return result; 19 | } 20 | 21 | [Symbol.iterator](): IterableIterator { 22 | return this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@collectable/list", 3 | "version": "2.0.0", 4 | "description": "[Collectable.js] Immutable List", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "jsnext:main": "lib/module/index.js", 8 | "typings": "lib/typings/index.d.ts", 9 | "scripts": { 10 | "test-dev": "cd ../.. && mocha --require source-map-support/register --ui tdd --watch --bail .build/packages/list/tests/**/*.js" 11 | }, 12 | "files": ["lib"], 13 | "author": "Nathan Ridley ", 14 | "license": "MIT", 15 | "bugs": "https://github.com/frptools/collectable/issues", 16 | "repository": "git@github.com:frptools/collectable.git", 17 | "dependencies": { 18 | "@collectable/core": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/set/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@collectable/set", 3 | "version": "0.2.0", 4 | "description": "[Collectable.js] Immutable Set", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "jsnext:main": "lib/module/index.js", 8 | "typings": "lib/typings/index.d.ts", 9 | "scripts": { 10 | "test-dev": "cd ../.. && mocha --require source-map-support/register --ui tdd --watch --bail .build/packages/list/tests/**/*.js" 11 | }, 12 | "files": ["lib"], 13 | "contributors": [ 14 | "Nathan Ridley " 15 | ], 16 | "license": "MIT", 17 | "bugs": "https://github.com/frptools/collectable/issues", 18 | "repository": "git@github.com:frptools/collectable.git", 19 | "dependencies": { 20 | "@collectable/core": "latest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/list/tests/functions/size.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, size, fromArray, appendArray} from '../../src'; 3 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 4 | 5 | suite('[List]', () => { 6 | suite('size()', () => { 7 | test('returns 0 if the list is empty', () => { 8 | assert.strictEqual(size(empty()), 0); 9 | }); 10 | 11 | test('returns the size of a single-node list', () => { 12 | var list = fromArray(['X', 'Y']); 13 | assert.strictEqual(size(list), 2); 14 | }); 15 | 16 | test('returns the correct size of a list with uncommitted changes', () => { 17 | var values = makeValues(BRANCH_FACTOR*4); 18 | assert.strictEqual(size(appendArray(['X', 'Y'], fromArray(values))), values.length + 2); 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /packages/list/src/functions/insert.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {List, cloneAsMutable, insertValues, ensureImmutable} from '../internals'; 3 | 4 | export function insert(index: number, value: T, list: List): List { 5 | return insertArray(index, [value], list); 6 | } 7 | 8 | export function insertArray(index: number, values: T[], list: List): List { 9 | if(values.length === 0) return list; 10 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 11 | insertValues(list, index, values); 12 | return immutable ? ensureImmutable(list, true) : list; 13 | } 14 | 15 | export function insertIterable(index: number, values: Iterable, list: List): List { 16 | return insertArray(index, Array.from(values), list); 17 | } 18 | -------------------------------------------------------------------------------- /packages/map/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@collectable/map", 3 | "version": "0.2.0", 4 | "description": "[Collectable.js] Immutable Map", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "jsnext:main": "lib/module/index.js", 8 | "typings": "lib/typings/index.d.ts", 9 | "scripts": { 10 | "test-dev": "cd ../.. && mocha --require source-map-support/register --ui tdd --watch --bail .build/packages/map/tests/**/*.js" 11 | }, 12 | "files": ["lib"], 13 | "contributors": [ 14 | "Nathan Ridley " 15 | ], 16 | "license": "MIT", 17 | "bugs": "https://github.com/frptools/collectable/issues", 18 | "repository": "git@github.com:frptools/collectable.git", 19 | "dependencies": { 20 | "@typed/curry": "latest", 21 | "@collectable/core": "latest" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/map/tests/get.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, isThawed, get, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('get()', () => { 9 | test('returns the value with the specified key', () => { 10 | var map = set('x', 3, empty()); 11 | 12 | assert.strictEqual(get('x', map), 3); 13 | 14 | assert.isFalse(isThawed(map)); 15 | assert.deepEqual(toJS(map), {x: 3}); 16 | }); 17 | 18 | test('returns undefined if the specified key is missing', () => { 19 | var map = set('x', 3, empty()); 20 | 21 | assert.isUndefined(get('y', map)); 22 | 23 | assert.isFalse(isThawed(map)); 24 | assert.deepEqual(toJS(map), {x: 3}); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@collectable/red-black-tree", 3 | "version": "1.0.2", 4 | "description": "[Collectable.js] Immutable Red-Black Tree", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "jsnext:main": "lib/module/index.js", 8 | "typings": "lib/typings/index.d.ts", 9 | "scripts": { 10 | "test-dev": "cd ../.. && mocha --require source-map-support/register --ui tdd --watch --bail .build/packages/red-black-tree/tests/**/*.js" 11 | }, 12 | "files": ["lib"], 13 | "contributors": [ 14 | "Nathan Ridley " 15 | ], 16 | "license": "MIT", 17 | "bugs": "https://github.com/frptools/collectable/issues", 18 | "repository": "git@github.com:frptools/collectable.git", 19 | "dependencies": { 20 | "@typed/curry": "latest", 21 | "@collectable/core": "latest" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/list/src/functions/unwrap.ts: -------------------------------------------------------------------------------- 1 | import {curry3} from '@typed/curry'; 2 | import {MappingFunction, preventCircularRefs, unwrapAny} from '@collectable/core'; 3 | import {List, arrayFrom, mapArrayFrom} from '../internals'; 4 | 5 | const unwrapDeep: (list: List) => T[] = curry3(preventCircularRefs)(newArray, (c, t) => mapArrayFrom(unwrapAny, c, t)); 6 | 7 | export function mapToArray(mapper: MappingFunction, list: List): U[] { 8 | return mapArrayFrom(mapper, list, new Array(list._size)); 9 | } 10 | 11 | export function join(separator: any, list: List): string { 12 | return arrayFrom(list).join(separator); 13 | } 14 | 15 | export function unwrap(deep: boolean, list: List): T[] { 16 | return deep ? unwrapDeep(list) : arrayFrom(list); 17 | } 18 | 19 | function newArray(list: List): T[] { 20 | return new Array(list._size); 21 | } 22 | -------------------------------------------------------------------------------- /packages/set/src/functions/unwrap.ts: -------------------------------------------------------------------------------- 1 | import {curry3} from '@typed/curry'; 2 | import {preventCircularRefs, unwrapAny} from '@collectable/core'; 3 | import {HashSet} from '../internals'; 4 | 5 | const newArray: (set: HashSet) => T[] = (set) => new Array(set.values.size); 6 | const unwrapShallow: (map: HashSet, target: T[]) => T[] = curry3(unwrapSet)(false); 7 | const unwrapDeep: (set: HashSet) => T[] = curry3(preventCircularRefs)(newArray, curry3(unwrapSet)(true)); 8 | 9 | export function unwrap(deep: boolean, set: HashSet): T[] { 10 | return deep ? unwrapDeep(set) : unwrapShallow(set, newArray(set)); 11 | } 12 | 13 | function unwrapSet(deep: boolean, set: HashSet, target: T[]): T[] { 14 | var it = set.values.values(); 15 | var current: IteratorResult; 16 | var i = 0; 17 | while(!(current = it.next()).done) { 18 | target[i++] = deep ? unwrapAny(current.value) : current.value; 19 | } 20 | return target; 21 | } 22 | -------------------------------------------------------------------------------- /packages/list/visualiser/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: { 5 | app: './index.js' 6 | }, 7 | output: { 8 | path: '/dist', 9 | publicPath: '/', 10 | filename: '[name].js', 11 | sourceMapFilename: '[name].js.map' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /(node_modules|bower_components)/, 18 | loader: 'babel' 19 | }, 20 | { 21 | test: /\.styl$/, loaders: ['style', 'css', 'stylus'] 22 | }, 23 | { 24 | test: /\.(png|jpg|gif)$/, loader: 'url' 25 | } 26 | ] 27 | }, 28 | resolve: { 29 | extensions: ['.js'], 30 | modules: ['modules', 'node_modules'] 31 | }, 32 | devtool: '#inline-source-map', 33 | plugins: [ 34 | new HtmlWebpackPlugin({ 35 | title: 'Collectable.js Development Tool', 36 | template: './home.ejs', 37 | hash: true 38 | }) 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /src/internals/common.ts: -------------------------------------------------------------------------------- 1 | import {CollectionTypeInfo, IndexableCollectionTypeInfo} from '@collectable/core'; 2 | import {fromArray, fromObject, fromMap, fromSet, fromIterable} from '../functions'; 3 | 4 | export function isIndexable(type: CollectionTypeInfo): type is IndexableCollectionTypeInfo { 5 | return type.indexable; 6 | } 7 | 8 | export function convertValue(value: any): any { 9 | if(value && typeof value === 'object') { 10 | if(Array.isArray(value)) { 11 | return fromArray(value); 12 | } 13 | if(value.constructor === Object) { 14 | return fromObject(value); 15 | } 16 | if(value instanceof Map) { 17 | return fromMap(value); 18 | } 19 | if(value instanceof Set) { 20 | return fromSet(value); 21 | } 22 | if(Symbol.iterator in value) { 23 | return fromIterable(value); 24 | } 25 | } 26 | return value; 27 | } 28 | 29 | export function convertPair(entry: [K, V]): [K, any] { 30 | return [entry[0], convertValue(entry[1])]; 31 | } 32 | -------------------------------------------------------------------------------- /packages/list/src/functions/update.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {List, cloneAsMutable, ensureImmutable, getAtOrdinal, setValueAtOrdinal} from '../internals'; 3 | 4 | export type UpdateListCallback = (value: T) => T|void; 5 | export type UpdateIndexCallback = (value: T) => T; 6 | 7 | export function updateList(callback: UpdateListCallback>, list: List): List { 8 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 9 | var newList = callback(list) || list; 10 | return immutable ? ensureImmutable(newList, true) : newList; 11 | } 12 | 13 | export function update(index: number, callback: UpdateIndexCallback, list: List): List { 14 | var oldv = getAtOrdinal(list, index); 15 | var newv = callback(oldv); 16 | if(newv === oldv) return list; 17 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 18 | setValueAtOrdinal(list, index, newv); 19 | return immutable ? ensureImmutable(list, true) : list; 20 | } 21 | -------------------------------------------------------------------------------- /packages/map/tests/getSize.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, getSize, set, remove} from '../src'; 3 | 4 | suite('Map', () => { 5 | suite('getSize()', () => { 6 | test('returns 0 when the map empty', () => { 7 | assert.strictEqual(getSize(empty()), 0); 8 | }); 9 | 10 | test('returns the correct size after adding entries', () => { 11 | var map1 = set('x', 1, empty()); 12 | var map2 = set('x', 2, map1); 13 | var map3 = set('y', 1, map1); 14 | assert.strictEqual(getSize(map1), 1); 15 | assert.strictEqual(getSize(map2), 1); 16 | assert.strictEqual(getSize(map3), 2); 17 | }); 18 | 19 | test('returns the correct size after removing entries', () => { 20 | var map = set('x', 1, empty()); 21 | map = set('y', 3, map); 22 | map = set('z', 5, map); 23 | assert.strictEqual(getSize(map = remove('x', map)), 2); 24 | assert.strictEqual(getSize(map = remove('y', map)), 1); 25 | assert.strictEqual(getSize(remove('z', map)), 0); 26 | }); 27 | }); 28 | }); -------------------------------------------------------------------------------- /src/functions/set.ts: -------------------------------------------------------------------------------- 1 | import {Collection, CollectionTypeInfo, isCollection, batch} from '@collectable/core'; 2 | import {fromPairs} from '@collectable/map'; 3 | import {isIndexable} from '../internals'; 4 | 5 | export function setIn(path: any[], value: any, collection: Collection): Collection { 6 | batch.start(); 7 | collection = setDeep(collection, path, 0, value); 8 | batch.end(); 9 | return collection; 10 | } 11 | 12 | function setDeep(collection: Collection, path: any[], keyidx: number, value: any): Collection { 13 | var key = path[keyidx], type: CollectionTypeInfo; 14 | batch.start(); 15 | if(isCollection(collection) && (type = collection['@@type'], isIndexable(type)) && type.verifyKey(key, collection)) { 16 | return keyidx === path.length - 1 17 | ? type.set(key, value, collection) 18 | : type.update(key, c => setDeep(c, path, keyidx + 1, value), collection); 19 | } 20 | return fromPairs([[key, keyidx === path.length - 1 ? value : setDeep(void 0, path, keyidx + 1, value)]]); 21 | } 22 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: { 5 | app: './app/index.js' 6 | }, 7 | output: { 8 | path: '/dist', 9 | publicPath: '/', 10 | filename: '[name].js', 11 | sourceMapFilename: '[name].js.map' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /(node_modules|bower_components)/, 18 | loader: 'babel-loader' 19 | }, 20 | { 21 | test: /\.styl$/, loaders: ['style-loader', 'css-loader', 'stylus-loader'] 22 | }, 23 | { 24 | test: /\.(png|jpg|gif)$/, loader: 'url-loader' 25 | } 26 | ] 27 | }, 28 | resolve: { 29 | extensions: ['.js'], 30 | modules: ['modules', 'node_modules'] 31 | }, 32 | devtool: '#inline-source-map', 33 | plugins: [ 34 | new HtmlWebpackPlugin({ 35 | title: 'Collectable.js Red/Black Tree Visualiser', 36 | template: './index.html', 37 | hash: true 38 | }) 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /packages/map/tests/has.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, thaw, has, set, remove} from '../src'; 3 | 4 | suite('Map', () => { 5 | suite('has()', () => { 6 | test('returns true if the specified property exists', () => { 7 | var map = set('x', 3, empty()); 8 | assert.isTrue(has('x', map)); 9 | }); 10 | 11 | test('returns false if the specified property is missing', () => { 12 | var map = set('x', 3, empty()); 13 | assert.isFalse(has('y', map)); 14 | }); 15 | 16 | test('returns true after assigning a property to a mutable map', () => { 17 | var map = thaw(empty()); 18 | assert.isFalse(has('x', map)); 19 | set('x', 3, map); 20 | assert.isTrue(has('x', map)); 21 | }); 22 | 23 | test('return false after removing a property from a mutable map', () => { 24 | var map = thaw(set('x', 3, empty())); 25 | assert.isTrue(has('x', map)); 26 | remove('x', map); 27 | assert.isFalse(has('x', map)); 28 | }); 29 | }); 30 | }); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Mocha", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "outFiles": ["${workspaceRoot}/.build/**/*.js"], 10 | "runtimeExecutable": "mocha", 11 | "windows": { 12 | "runtimeExecutable": "mocha.cmd" 13 | }, 14 | "runtimeArgs": [ 15 | "--opts", "./mocha.opts" 16 | ], 17 | "sourceMaps": true 18 | }, 19 | { 20 | "name": "Attach", 21 | "type": "node", 22 | "request": "attach", 23 | "port": 5858, 24 | "address": "localhost", 25 | "restart": false, 26 | "sourceMaps": false, 27 | "outFiles": [], 28 | "localRoot": "${workspaceRoot}", 29 | "remoteRoot": null 30 | }, 31 | { 32 | "name": "Attach to Process", 33 | "type": "node", 34 | "request": "attach", 35 | "processId": "${command:PickProcess}", 36 | "port": 5858, 37 | "sourceMaps": false, 38 | "outFiles": [] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/empty.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, size, isRedBlackTree} from '../../src'; 3 | 4 | suite('[RedBlackTree]', () => { 5 | suite('empty()', () => { 6 | test('returns a tree of size 0', () => { 7 | const tree = empty(); 8 | assert.strictEqual(size(tree), 0); 9 | }); 10 | }); 11 | 12 | suite('isRedBlackTree()', () => { 13 | test('returns true if the argument is a valid RedBlackTree instance', () => { 14 | const tree = empty(); 15 | assert.isTrue(isRedBlackTree(tree)); 16 | }); 17 | test('returns false if the argument is not a valid RedBlackTree instance', () => { 18 | assert.isFalse(isRedBlackTree(0)); 19 | assert.isFalse(isRedBlackTree(1)); 20 | assert.isFalse(isRedBlackTree('foo')); 21 | assert.isFalse(isRedBlackTree(null)); 22 | assert.isFalse(isRedBlackTree(void 0)); 23 | assert.isFalse(isRedBlackTree({})); 24 | assert.isFalse(isRedBlackTree(Symbol())); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/size.ts: -------------------------------------------------------------------------------- 1 | import {RedBlackTree, RedBlackTreeImpl} from '../internals'; 2 | 3 | /** 4 | * Returns the current number of entries in the tree 5 | * 6 | * @export 7 | * @template K The type of keys in the tree 8 | * @template V The type of values in the tree 9 | * @param {RedBlackTree} tree The input tree 10 | * @returns {number} The number of entries in the tree 11 | */ 12 | export function size(tree: RedBlackTree): number; 13 | export function size(tree: RedBlackTreeImpl): number { 14 | return tree._size; 15 | } 16 | 17 | /** 18 | * Determines whether or not the tree currently has any entries 19 | * 20 | * @export 21 | * @template K The type of keys in the tree 22 | * @template V The type of values in the tree 23 | * @param {RedBlackTree} tree The input tree 24 | * @returns {boolean} True if the tree is empty, otherwise false 25 | */ 26 | export function isEmpty(tree: RedBlackTree): boolean; 27 | export function isEmpty(tree: RedBlackTreeImpl): boolean { 28 | return tree._size === 0; 29 | } 30 | -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/size.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, size, isEmpty} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {createTree, sortedValues} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, 7 | emptyTree: RedBlackTreeImpl; 8 | 9 | suite('[RedBlackTree]', () => { 10 | setup(() => { 11 | emptyTree = >empty(); 12 | tree = createTree(); 13 | }); 14 | 15 | suite('size()', () => { 16 | test('returns 0 if the tree is empty', () => { 17 | assert.strictEqual(size(emptyTree), 0); 18 | }); 19 | 20 | test('returns the number of elements in the tree', () => { 21 | assert.strictEqual(size(tree), sortedValues.length); 22 | }); 23 | }); 24 | 25 | suite('isEmpty()', () => { 26 | test('returns true if the tree is empty', () => { 27 | assert.isTrue(isEmpty(emptyTree)); 28 | }); 29 | 30 | test('returns false if the tree is not empty', () => { 31 | assert.isFalse(isEmpty(tree)); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/map/src/functions/unwrap.ts: -------------------------------------------------------------------------------- 1 | import {curry3} from '@typed/curry'; 2 | import {preventCircularRefs, unwrapAny} from '@collectable/core'; 3 | import {HashMap} from '../internals'; 4 | 5 | export type Associative = {[key: string]: T}; 6 | const newObject: () => Associative = () => ({}); 7 | const unwrapShallow: (map: HashMap, target: Associative) => Associative = curry3(unwrapMap)(false); 8 | const unwrapDeep: (map: HashMap) => Associative = curry3(preventCircularRefs)(newObject, curry3(unwrapMap)(true)); 9 | 10 | export function unwrap(deep: boolean, map: HashMap): Associative { 11 | return deep ? unwrapDeep(map) : unwrapShallow(map, newObject()); 12 | } 13 | 14 | function unwrapMap(deep: boolean, map: HashMap, target: Associative): Associative { 15 | var it = map._values.entries(); 16 | var current: IteratorResult<[K, V]>; 17 | while(!(current = it.next()).done) { 18 | var entry = current.value; 19 | var value = entry[1]; 20 | target[entry[0]] = deep ? unwrapAny(value) : value; 21 | } 22 | return target; 23 | } -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rbtree-visualiser", 3 | "version": "1.0.0", 4 | "description": "Visualiser for Collectable.js' persistent red/black tree", 5 | "main": "app/index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --port 9000 --hot --inline --progress --content-base dist/" 8 | }, 9 | "repository": {}, 10 | "license": "MIT", 11 | "dependencies": { 12 | "@most/create": "^2.0.1", 13 | "@motorcycle/dom": "^8.0.0", 14 | "@motorcycle/run": "^1.2.5", 15 | "@typed/curry": "^1.0.1", 16 | "css-loader": "^0.26.1", 17 | "file-loader": "^0.10.0", 18 | "immutable": "^3.8.1", 19 | "style-loader": "^0.13.1", 20 | "stylus": "^0.54.5", 21 | "stylus-loader": "^2.5.0", 22 | "url-loader": "^0.5.7" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.23.1", 26 | "babel-loader": "^6.3.2", 27 | "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", 28 | "circular-json": "^0.3.1", 29 | "html-webpack-plugin": "^2.28.0", 30 | "webpack": "^2.2.1", 31 | "webpack-dev-server": "^2.4.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/list/visualiser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collectable-visualiser--tool", 3 | "version": "1.0.0", 4 | "description": "For local testing of collectable.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --port 9000 --hot --inline --progress --content-base dist/" 8 | }, 9 | "author": "Nathan Ridley", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@cycle/most-run": "^4.1.3", 13 | "@most/create": "^2.0.1", 14 | "@motorcycle/dom": "^3.0.0", 15 | "circular-json": "^0.3.1", 16 | "immutable": "^3.8.1", 17 | "most": "^1.0.4" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.17.0", 21 | "babel-loader": "^6.2.5", 22 | "babel-preset-es2015": "^6.16.0", 23 | "babel-preset-es2015-native-modules": "^6.9.4", 24 | "css-loader": "^0.25.0", 25 | "file-loader": "^0.9.0", 26 | "html-webpack-plugin": "^2.22.0", 27 | "style-loader": "^0.13.1", 28 | "stylus": "^0.54.5", 29 | "stylus-loader": "^2.3.1", 30 | "url-loader": "^0.5.7", 31 | "webpack": "2.1.0-beta.22", 32 | "webpack-dev-server": "beta" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Nathan Ridley 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/list/src/functions/concat.ts: -------------------------------------------------------------------------------- 1 | import {publish} from '../internals/debug'; // ## DEV ## 2 | import {isImmutable} from '@collectable/core'; 3 | import {List, cloneAsMutable, concatLists, ensureImmutable} from '../internals'; 4 | 5 | export function concat(left: List, right: List): List { 6 | publish([left, right], true, 'pre-concat'); // ## DEV ## 7 | if(left._size === 0) return right; 8 | if(right._size === 0) return left; 9 | var immutable = isImmutable(left._owner) && (left = cloneAsMutable(left), true); 10 | left = concatLists(left, cloneAsMutable(right)); 11 | return immutable ? ensureImmutable(left, true) : left; 12 | } 13 | 14 | export function concatLeft(right: List, left: List): List { 15 | return concat(left, right); 16 | } 17 | 18 | export function concatAll(lists: List[]): List { 19 | var list: List = lists[0]; 20 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 21 | for(var i = 1; i < lists.length; i++) { 22 | list = concatLists(list, cloneAsMutable(lists[i])); 23 | } 24 | return immutable ? ensureImmutable(list, true) : list; 25 | } 26 | -------------------------------------------------------------------------------- /src/functions/update.ts: -------------------------------------------------------------------------------- 1 | import {Collection, CollectionTypeInfo, isCollection, batch} from '@collectable/core'; 2 | import {fromPairs} from '@collectable/map'; 3 | import {isIndexable} from '../internals'; 4 | 5 | export type UpdateInCallback = (value: T|undefined) => T; 6 | 7 | export function updateIn, T, U>(path: any[], update: UpdateInCallback, collection: C): C { 8 | collection = updateDeep(collection, path, 0, update); 9 | return collection; 10 | } 11 | 12 | function updateDeep, T, U>(collection: C, path: any[], keyidx: number, update: UpdateInCallback): C { 13 | var key = path[keyidx], type: CollectionTypeInfo; 14 | batch.start(); 15 | if(isCollection(collection) && (type = collection['@@type'], isIndexable(type)) && type.verifyKey(key, collection)) { 16 | return type.update(key, keyidx === path.length - 1 ? update 17 | : c => updateDeep(c, path, keyidx + 1, update), collection); 18 | } 19 | var value = keyidx === path.length - 1 ? update(void 0) : updateDeep(void 0, path, keyidx + 1, update); 20 | return fromPairs([[key, value]]); 21 | } 22 | -------------------------------------------------------------------------------- /packages/list/tests/functions/from.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {fromArray} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 5 | 6 | suite('[List]', () => { 7 | suite('fromArray()', () => { 8 | test('should return an empty list if passed an empty array', () => { 9 | const list = fromArray([]); 10 | assert.strictEqual(list._size, 0); 11 | assert.isTrue(list._left.isDefaultEmpty()); 12 | assert.isTrue(list._right.isDefaultEmpty()); 13 | }); 14 | 15 | test('should return a list containing all the values in the array', () => { 16 | var values = makeValues(BRANCH_FACTOR >>> 1); 17 | assert.deepEqual(arrayFrom(fromArray(values)), values); 18 | 19 | values = makeValues(BRANCH_FACTOR); 20 | assert.deepEqual(arrayFrom(fromArray(values)), values); 21 | 22 | values = makeValues(BRANCH_FACTOR + 1); 23 | var list = fromArray(values); 24 | assert.deepEqual(arrayFrom(list), values); 25 | 26 | values = makeValues(BRANCH_FACTOR*BRANCH_FACTOR); 27 | list = fromArray(values); 28 | assert.deepEqual(arrayFrom(list), values); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/freeze.ts: -------------------------------------------------------------------------------- 1 | import {RedBlackTree, RedBlackTreeImpl, cloneAsImmutable} from '../internals'; 2 | import {isImmutable} from '@collectable/core'; 3 | 4 | /** 5 | * Returns an immutable version of the input tree. 6 | * 7 | * @export 8 | * @template K The type of keys in the tree 9 | * @template V The type of values in the tree 10 | * @param {RedBlackTree} tree The input tree 11 | * @returns {RedBlackTree} An immutable copy of the input tree, or the same tree if already immutable 12 | */ 13 | export function freeze(tree: RedBlackTree): RedBlackTree; 14 | export function freeze(tree: RedBlackTreeImpl): RedBlackTree { 15 | return isImmutable(tree._owner) ? tree : cloneAsImmutable(tree); 16 | } 17 | 18 | /** 19 | * Determines whether or not the tree is currently immutable. 20 | * 21 | * @export 22 | * @template K The type of keys in the tree 23 | * @template V The type of values in the tree 24 | * @param {RedBlackTree} tree The input tree 25 | * @returns {boolean} True if the tree is currently immutable, otherwise false 26 | */ 27 | export function isFrozen(tree: RedBlackTree): boolean; 28 | export function isFrozen(tree: RedBlackTreeImpl): boolean { 29 | return isImmutable(tree._owner); 30 | } -------------------------------------------------------------------------------- /src/functions/from.ts: -------------------------------------------------------------------------------- 1 | import {batch, MappableIterator} from '@collectable/core'; 2 | import {convertPair, convertValue} from '../internals'; 3 | import {List, fromIterable as listFromIterable} from '@collectable/list'; 4 | import {Map as CMap, empty, fromIterable as mapFromIterable, set} from '@collectable/map'; 5 | import {Set as CSet, fromIterable as setFromIterable} from '@collectable/set'; 6 | 7 | export function fromObject(value: Object): CMap { 8 | var keys = Object.keys(value); 9 | batch.start(); 10 | var map = empty(); 11 | for(var i = 0; i < keys.length; i++) { 12 | var key = keys[i]; 13 | set(key, convertValue(value[key]), map); 14 | } 15 | batch.end(); 16 | return map; 17 | } 18 | 19 | export function fromArray(array: T[]): List { 20 | return listFromIterable(new MappableIterator(array, convertValue)); 21 | } 22 | 23 | export function fromMap(map: Map): CMap { 24 | return mapFromIterable(new MappableIterator<[K, V], [K, V]>(map.entries(), convertPair)); 25 | } 26 | 27 | export function fromSet(set: Set): CSet { 28 | return setFromIterable(new MappableIterator(set, convertValue)); 29 | } 30 | 31 | export function fromIterable(iterable: Iterable): List { 32 | return listFromIterable(new MappableIterator(iterable, convertValue)); 33 | } 34 | -------------------------------------------------------------------------------- /packages/set/src/internals/set.ts: -------------------------------------------------------------------------------- 1 | import {Collection, CollectionTypeInfo, isDefined, nextId, batch} from '@collectable/core'; 2 | import {iterate, isEqual, unwrap} from '../functions'; 3 | 4 | const SET_TYPE: CollectionTypeInfo = { 5 | type: Symbol('Collectable.Set'), 6 | indexable: false, 7 | 8 | equals(other: any, collection: any): boolean { 9 | return isEqual(other, collection); 10 | }, 11 | 12 | unwrap(set: HashSet): any { 13 | return unwrap(true, set); 14 | } 15 | }; 16 | 17 | export class HashSet implements Collection { 18 | get '@@type'() { return SET_TYPE; } 19 | 20 | constructor( 21 | public values: Set, 22 | public owner: number, 23 | public group: number 24 | ) {} 25 | 26 | [Symbol.iterator](): IterableIterator { 27 | return iterate(this); 28 | } 29 | } 30 | 31 | export function cloneSet(state: HashSet, mutable = false): HashSet { 32 | return new HashSet(new Set(state.values), batch.owner(mutable), nextId()); 33 | } 34 | 35 | export function createSet(values?: T[]|Iterable): HashSet { 36 | return new HashSet( 37 | isDefined(values) ? new Set(values) : new Set(), 38 | nextId(), 39 | batch.owner(false) 40 | ); 41 | } 42 | 43 | export function emptySet(): HashSet { 44 | return _empty; 45 | } 46 | 47 | const _empty = createSet(); -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/get.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, get, iterateFromKey} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {createTree, sortedValues} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, emptyTree; 7 | 8 | suite('[RedBlackTree]', () => { 9 | setup(() => { 10 | emptyTree = empty(); 11 | tree = createTree(); 12 | }); 13 | 14 | suite('get()', () => { 15 | test('returns undefined if the key does not exist in the list', () => { 16 | assert.strictEqual(get(1, emptyTree), void 0); 17 | }); 18 | 19 | test('returns the value associated with the specified key', () => { 20 | for(var i = 0; i < sortedValues.length; i++) { 21 | assert.strictEqual(get(sortedValues[i], tree), `#${sortedValues[i]}`); 22 | } 23 | }); 24 | }); 25 | 26 | suite('iterateFromKey()', () => { 27 | test('returns an iterator starting from the specified key', () => { 28 | const it = iterateFromKey(false, sortedValues[5], tree); 29 | assert.deepEqual(Array.from(it).map(n => n.key), sortedValues.slice(5)); 30 | }); 31 | 32 | test('the iterator should be in a completed state if the key was not found', () => { 33 | const it = iterateFromKey(false, sortedValues[5], emptyTree); 34 | assert.isTrue(it.next().done); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /packages/list/src/functions/slice.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {empty} from './empty'; 3 | import {List, cloneAsMutable, sliceList, ensureImmutable} from '../internals'; 4 | 5 | export function skip(count: number, list: List): List { 6 | if(count === 0) return list; 7 | if(count >= list._size) return empty(); 8 | return slice(count, 0, list); 9 | } 10 | 11 | export function skipLast(count: number, list: List): List { 12 | if(count === 0) return list; 13 | if(count >= list._size) return empty(); 14 | return slice(0, -count, list); 15 | } 16 | 17 | export function take(count: number, list: List): List { 18 | if(count === 0) return empty(); 19 | if(count >= list._size) return list; 20 | return slice(0, count, list); 21 | } 22 | 23 | export function takeLast(count: number, list: List): List { 24 | if(count === 0) return empty(); 25 | if(count >= list._size) return list; 26 | return slice(-count, 0, list); 27 | } 28 | 29 | export function slice(start: number, end: number, list: List): List { 30 | if(list._size === 0) return list; 31 | if(end === 0) end = list._size; 32 | var oldList = list; 33 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 34 | sliceList(list, start, end); 35 | return immutable && oldList._size !== list._size ? ensureImmutable(list, true) : oldList; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Collectable.js: Core Module 2 | 3 | [![Build Status](https://travis-ci.org/frptools/collectable.svg?branch=master)](https://travis-ci.org/frptools/collectable) 4 | [![NPM version](https://badge.fury.io/js/%40collectable%2Fcore.svg)](http://badge.fury.io/js/%40collectable%2Fcore) 5 | [![GitHub version](https://badge.fury.io/gh/frptools%2Fcollectable.svg)](https://badge.fury.io/gh/frptools%2Fcollectable) 6 | [![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/FRPTools/Lobby) 7 | 8 | **This module provides functionality that is internal to Collectable.js data structures. You do not need to take it as a dependency directly.** 9 | 10 | For unit testing during development set VSCode to build (CTRL/CMD-SHIFT-B; the task is set to 11 | watch/rebuild on changes), then, from your terminal/console in this directory, run one of: 12 | 13 | ``` 14 | $ npm run test-dev 15 | $ yarn run test-dev 16 | ``` 17 | 18 | The project will compile, Mocha will run in watch mode and bail when encountering a failing test. 19 | Because VSCode is building in watch mode too, making a change will recompile source files, and the 20 | compiled outputs will be picked up automatically picked up by Mocha and rerun. 21 | 22 | This development testing process is independent of the main Gulp-based build and exists only to make 23 | development easier. Build with Gulp after you're satisfied that source code and tests work correctly. -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/equality.ts: -------------------------------------------------------------------------------- 1 | import {isEqual as equals} from '@collectable/core'; 2 | import {RedBlackTree} from '../internals'; 3 | import {size, iterateFromFirst} from '../functions'; 4 | 5 | /** 6 | * Determines whether two trees have equivalent sets of keys and values. Though order of insertion can affect the 7 | * internal structure of a red black tree, only the actual set of entries and their ordinal positions are considered. 8 | * 9 | * @export 10 | * @template K The type of keys in the tree 11 | * @template V The type of values in the tree 12 | * @param {RedBlackTree} tree The input tree 13 | * @param {RedBlackTree} other Another tree to compare entries with 14 | * @returns {boolean} True if both trees are of the same size and have equivalent sets of keys and values for each entry 15 | * at corresponding indices in each tree, otherwise false. 16 | */ 17 | export function isEqual(tree: RedBlackTree, other: RedBlackTree): boolean { 18 | if(tree === other) return true; 19 | if(size(tree) !== size(other)) return false; 20 | // Iterator is required because two trees may have the same set of keys and values but slightly different structures 21 | var ita = iterateFromFirst(tree), itb = iterateFromFirst(other); 22 | do { 23 | var ca = ita.next(); 24 | var cb = itb.next(); 25 | if(!equals(ca.value, cb.value)) return false; 26 | } while(!ca.done); 27 | return true; 28 | } 29 | -------------------------------------------------------------------------------- /packages/list/tests/functions/iterate.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, fromArray} from '../../src'; 3 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 4 | 5 | suite('[List]', () => { 6 | suite('[Symbol.iterator]()', () => { 7 | test('returns an ES6-compliant iterator', () => { 8 | var it = empty()[Symbol.iterator](); 9 | assert.isFunction(it.next); 10 | }); 11 | 12 | test('starts in a completed state if the list is empty', () => { 13 | var it = empty()[Symbol.iterator](); 14 | var current = it.next(); 15 | assert.isTrue(current.done); 16 | assert.isUndefined(current.value); 17 | }); 18 | 19 | test('iterates through all values sequentially', () => { 20 | var list = fromArray(['X', 'Y']); 21 | assert.deepEqual(Array.from(list), ['X', 'Y']); 22 | }); 23 | 24 | test('is done when all values have been iterated over', () => { 25 | var list = fromArray(['X', 'Y']); 26 | var it = list[Symbol.iterator](); 27 | assert.deepEqual(it.next(), {value: 'X', done: false}); 28 | assert.deepEqual(it.next(), {value: 'Y', done: false}); 29 | assert.deepEqual(it.next(), {value: void 0, done: true}); 30 | }); 31 | 32 | test('traverses multiple leaf nodes', () => { 33 | var values = makeValues(BRANCH_FACTOR*4); 34 | var list = fromArray(values); 35 | assert.deepEqual(Array.from(list), values); 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /packages/map/tests/asMutable.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, isThawed, thaw, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('asMutable()', () => { 9 | test('creates a mutable copy of the original map', () => { 10 | var map = set('x', 3, empty()); 11 | 12 | assert.isFalse(isThawed(map)); 13 | 14 | var map1 = thaw(map); 15 | 16 | assert.notStrictEqual(map, map1); 17 | assert.isFalse(isThawed(map)); 18 | assert.isTrue(isThawed(map1)); 19 | assert.deepEqual(toJS(map), {x: 3}); 20 | assert.deepEqual(toJS(map1), {x: 3}); 21 | }); 22 | 23 | test('returns the same map if already mutable', () => { 24 | var map = set('x', 3, empty()); 25 | var map1 = thaw(map); 26 | var map2 = thaw(map1); 27 | 28 | assert.notStrictEqual(map, map1); 29 | assert.strictEqual(map1, map2); 30 | assert.deepEqual(toJS(map), {x: 3}); 31 | assert.deepEqual(toJS(map1), {x: 3}); 32 | }); 33 | 34 | test('operations performed on a mutable map update and return the original map', () => { 35 | var map = thaw(set('x', 3, empty())); 36 | var map1 = set('y', 1, map); 37 | var map2 = set('z', 2, map1); 38 | 39 | assert.strictEqual(map, map1); 40 | assert.strictEqual(map, map2); 41 | assert.deepEqual(toJS(map), {x: 3, y: 1, z: 2}); 42 | }); 43 | }); 44 | }); -------------------------------------------------------------------------------- /packages/core/src/ownership.ts: -------------------------------------------------------------------------------- 1 | var _nextId = 0; 2 | export function nextId() { 3 | return ++_nextId; 4 | } 5 | 6 | var _owner = 0, _depth = 0; 7 | export interface Batch { 8 | (callback: (owner?: number) => void): any; 9 | (callback: (owner?: number) => void): T; 10 | start(): void; 11 | end(): boolean; 12 | owner(ensureMutable: boolean): number; 13 | readonly active: boolean; 14 | } 15 | 16 | function start(): void { 17 | if(_depth === 0) { 18 | _owner = nextId(); 19 | } 20 | _depth++; 21 | } 22 | 23 | function end(): boolean { 24 | if(_depth > 0) { 25 | if(--_depth > 0) { 26 | return false; 27 | } 28 | _owner = 0; 29 | } 30 | return true; 31 | } 32 | 33 | function owner(ensureMutable: true): number { 34 | return ensureMutable ? _owner || -1 : _owner; 35 | } 36 | 37 | export const batch: Batch = Object.assign( 38 | function (callback: (owner?: number) => any): any { 39 | start(); 40 | var result = callback(_owner); 41 | end(); 42 | return result; 43 | }, 44 | { 45 | start, 46 | end, 47 | owner 48 | } 49 | ); 50 | Object.defineProperties(batch, { 51 | active: { 52 | get(): boolean { 53 | return _owner !== 0; 54 | } 55 | } 56 | }); 57 | 58 | export function isMutable(owner: number): boolean { 59 | return owner === -1 || (owner !== 0 && owner === _owner); 60 | } 61 | 62 | export function isImmutable(owner: number): boolean { 63 | return owner === 0 || (owner !== -1 && owner !== _owner); 64 | } 65 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/empty.ts: -------------------------------------------------------------------------------- 1 | import {Collection} from '@collectable/core'; 2 | import {RedBlackTree, RedBlackTreeImpl, Comparator, createTree} from '../internals'; 3 | 4 | /** 5 | * Creates an empty tree. If no comparator function is supplied, keys are compared using logical less-than and 6 | * greater-than operations, which will generally only be suitable for numeric or string keys. 7 | * 8 | * @export 9 | * @template K The type of keys in the tree 10 | * @template V The type of values in the tree 11 | * @param {Comparator} [comparator] A comparison function, taking two keys, and returning a value less than 0 if the 12 | * first key is smaller than the second, a value greater than 0 if the first key is 13 | * greater than the second, or 0 if they're the same. 14 | * @returns {RedBlackTree} An empty tree 15 | */ 16 | export function empty(comparator?: Comparator): RedBlackTree { 17 | return createTree(false, comparator); 18 | } 19 | 20 | /** 21 | * Determines whether the input argument is an instance of a Collectable.js RedBlackTree structure. 22 | * 23 | * @export 24 | * @param {RedBlackTree} arg The input value to check 25 | * @returns {boolean} True if the input value is a RedBlackTree, otherwise false 26 | */ 27 | export function isRedBlackTree(arg: Collection): boolean { 28 | return typeof arg === 'object' && arg !== null && arg instanceof RedBlackTreeImpl; 29 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": false, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "no-var-keyword": false, 18 | "no-invalid-this": true, 19 | "one-line": [ 20 | true, 21 | "check-open-brace", 22 | "check-whitespace" 23 | ], 24 | "quotemark": [ 25 | true, 26 | "single" 27 | ], 28 | "semicolon": [ 29 | true, 30 | "always" 31 | ], 32 | "triple-equals": [ 33 | true, 34 | "allow-null-check" 35 | ], 36 | "typedef-whitespace": [ 37 | true, 38 | { 39 | "call-signature": "nospace", 40 | "index-signature": "nospace", 41 | "parameter": "nospace", 42 | "property-declaration": "nospace", 43 | "variable-declaration": "nospace" 44 | } 45 | ], 46 | "variable-name": [ 47 | true, 48 | "ban-keywords" 49 | ], 50 | "whitespace": [ 51 | true, 52 | "check-decl", 53 | "check-separator", 54 | "check-type" 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /packages/list/tests/functions/hasIndex.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, fromArray, hasIndex} from '../../src'; 3 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 4 | 5 | suite('[List]', () => { 6 | suite('hasIndex()', () => { 7 | test('returns false if the list is empty', () => { 8 | assert.isFalse(hasIndex(0, empty())); 9 | }); 10 | 11 | test('returns false if the index is out of range', () => { 12 | var list0 = fromArray(['X', 'Y']); 13 | const size = Math.pow(BRANCH_FACTOR, 2) + BRANCH_FACTOR; 14 | var list1 = fromArray(makeValues(size)); 15 | assert.isFalse(hasIndex(2, list0)); 16 | assert.isFalse(hasIndex(20, list0)); 17 | assert.isFalse(hasIndex(size, list1)); 18 | assert.isFalse(hasIndex(size*2, list1)); 19 | }); 20 | 21 | test('returns true if the index is in range', () => { 22 | var list0 = fromArray(['X', 'Y']); 23 | const size = Math.pow(BRANCH_FACTOR, 2) + BRANCH_FACTOR; 24 | var list1 = fromArray(makeValues(size)); 25 | assert.isTrue(hasIndex(0, list0)); 26 | assert.isTrue(hasIndex(1, list0)); 27 | assert.isTrue(hasIndex(0, list1)); 28 | assert.isTrue(hasIndex(size - 1, list1)); 29 | }); 30 | 31 | test('treats a negative index as an offset from the end of the list', () => { 32 | var list = fromArray(['X', 'Y', 'Z']); 33 | assert.isTrue(hasIndex(-1, list)); 34 | assert.isTrue(hasIndex(-2, list)); 35 | assert.isTrue(hasIndex(-3, list)); 36 | assert.isFalse(hasIndex(-4, list)); 37 | }); 38 | }); 39 | }); -------------------------------------------------------------------------------- /packages/core/src/hash.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from './functions'; 2 | import {PCGRandom} from './random'; 3 | 4 | export function hash(arg: any): number { 5 | if(isZero(arg)) return 0; 6 | if(typeof arg.valueOf === 'function' && arg.valueOf !== Object.prototype.valueOf) { 7 | arg = arg.valueOf(); 8 | if(isZero(arg)) return 0; 9 | } 10 | switch(typeof arg) { 11 | case 'number': return hashNumber(arg); 12 | case 'string': return hashString(arg); 13 | case 'function': 14 | case 'object': return hashObject(arg); 15 | case 'boolean': return arg === true ? 1 : 0; 16 | default: return 0; 17 | } 18 | } 19 | 20 | function isZero(value: any): boolean { 21 | return value === null || value === void 0 || value === false; 22 | } 23 | 24 | const OBJECT_HASH = { 25 | pcg: new PCGRandom(13), 26 | map: new WeakMap() 27 | }; 28 | 29 | function hashObject(o: Object): number { 30 | var cache = OBJECT_HASH; 31 | var n = cache.map.get(o); 32 | if(isDefined(n)) return n; 33 | n = cache.pcg.integer(0x7FFFFFFF); 34 | cache.map.set(o, n); 35 | return n; 36 | } 37 | 38 | function hashNumber(n: number): number { 39 | if(n !== n || n === Infinity) return 0; 40 | var h = n | 0; 41 | if(h !== n) h ^= n * 0xFFFFFFFF; 42 | while(n > 0xFFFFFFFF) h ^= (n /= 0xFFFFFFFF); 43 | return opt(n); 44 | } 45 | 46 | function hashString(str: string): number { 47 | var h = 5381, i = str.length; 48 | while(i) h = (h * 33) ^ str.charCodeAt(--i); 49 | return opt(h); 50 | } 51 | 52 | function opt(n: number) { 53 | return (n & 0xbfffffff) | ((n >>> 1) & 0x40000000); 54 | } -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/navigation.js: -------------------------------------------------------------------------------- 1 | import {div, a} from '@motorcycle/dom'; 2 | 3 | var oldIndex = 0; 4 | function focusItem(index, init = false) { 5 | return function(/*vn0, vn1*/) { 6 | if(!init && index === oldIndex) return; 7 | oldIndex = index; 8 | const body = document.querySelector('body'); 9 | const el = arguments[arguments.length - 1].element; 10 | setTimeout(() => { 11 | const y0 = body.scrollTop; 12 | const y1 = body.scrollTop + body.offsetHeight; 13 | if(el.offsetTop + el.offsetHeight > y1 || el.offsetTop < y0) { 14 | body.scrollTop = el.offsetTop - body.offsetHeight/2 + el.offsetHeight/2; 15 | } 16 | }, 1); 17 | // el.scrollIntoViewIfNeeded(false); 18 | } 19 | } 20 | 21 | function render(dom) { 22 | return function(state) { 23 | const items = state.versions 24 | .valueSeq() 25 | .map((entry, i) => { 26 | const active = i === (state.index || 0); 27 | const {done} = entry; 28 | const postpatch = active ? focusItem(state.index) : void 0; 29 | const insert = active ? focusItem(state.index, true) : void 0; 30 | return div('.list-item', {class: {active, done}, postpatch, insert}, [ 31 | a('.link', {href: 'javascript:void 0', attrs: {'data-index': i.toString()}}, `${i+1}. ${entry.label}`) 32 | ]); 33 | }) 34 | .toArray(); 35 | return div('.nav', [ 36 | div('.versions', items) 37 | ]); 38 | }; 39 | } 40 | 41 | export function Navigation({dom, state$}) { 42 | return { 43 | view$: state$.map(render(dom)) 44 | }; 45 | } -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/thaw.ts: -------------------------------------------------------------------------------- 1 | import {RedBlackTree, RedBlackTreeImpl, cloneAsMutable} from '../internals'; 2 | import {isMutable} from '@collectable/core'; 3 | 4 | /** 5 | * Returns a mutable copy of the tree. Operations performed on mutable trees are applied to the input tree directly, 6 | * and the same mutable tree is returned after the operation is complete. Structurally, any internals that are shared 7 | * with other immutable copies of the tree are cloned safely, but only as needed, and only once. Subsequent operations 8 | * are applied to the same internal structures without making further copies. 9 | * 10 | * @export 11 | * @template K The type of keys in the tree 12 | * @template V The type of values in the tree 13 | * @param {RedBlackTree} tree The tree to be made mutable 14 | * @returns {RedBlackTree} A mutable version of the input tree, or the same tree if it was already mutable 15 | */ 16 | export function thaw(tree: RedBlackTree): RedBlackTree; 17 | export function thaw(tree: RedBlackTreeImpl): RedBlackTree { 18 | return isMutable(tree._owner) ? tree : cloneAsMutable(tree); 19 | } 20 | 21 | /** 22 | * Determines whether or not the specified tree is currently mutable 23 | * 24 | * @export 25 | * @template K The type of keys in the tree 26 | * @template V The type of values in the tree 27 | * @param {RedBlackTree} tree The tree to be checked 28 | * @returns {boolean} True if the tree is mutable, otherwise false 29 | */ 30 | export function isThawed(tree: RedBlackTree): boolean; 31 | export function isThawed(tree: RedBlackTreeImpl): boolean { 32 | return isMutable(tree._owner); 33 | } -------------------------------------------------------------------------------- /packages/list/src/functions/prepend.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {log} from '../internals/debug'; // ## DEV ## 3 | import {CONST, OFFSET_ANCHOR, List, cloneAsMutable, prependValues, ensureImmutable} from '../internals'; 4 | 5 | export function prepend(value: T, list: List): List { 6 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 7 | var head = list._left; 8 | var slot = head.slot; 9 | log(`Begin prepend of value "${value}" to list of size ${list._size}`); // ## DEV ## 10 | if(head.group !== 0 && head.offset === 0 && slot.group !== 0 && slot.size < CONST.BRANCH_FACTOR) { 11 | list._lastWrite = OFFSET_ANCHOR.LEFT; 12 | list._size++; 13 | if(slot.group === list._group) { 14 | slot.adjustRange(1, 0, true); 15 | } 16 | else { 17 | slot = slot.cloneWithAdjustedRange(list._group, 1, 0, true, true); 18 | if(head.group !== list._group) { 19 | head = head.cloneToGroup(list._group); 20 | list._left = head; 21 | } 22 | head.slot = slot; 23 | } 24 | head.sizeDelta++; 25 | head.slotsDelta++; 26 | slot.slots[0] = arguments[0]; 27 | } 28 | else { 29 | prependValues(list, [value]); 30 | } 31 | return immutable ? ensureImmutable(list, true) : list; 32 | } 33 | 34 | export function prependArray(values: T[], list: List): List { 35 | if(values.length === 0) return list; 36 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 37 | prependValues(list, values); 38 | return immutable ? ensureImmutable(list, true) : list; 39 | } 40 | 41 | export function prependIterable(values: Iterable, list: List): List { 42 | return prependArray(Array.from(values), list); 43 | } 44 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/data/styles.styl: -------------------------------------------------------------------------------- 1 | @import '../core/palette.styl'; 2 | 3 | .diagram 4 | .node 5 | circle 6 | animation: 1s ease-in-out 1 7 | text 8 | font-family: 'Lato', sans-serif 9 | font-size: 10px 10 | fill: white 11 | text-anchor: middle 12 | alignment-baseline: middle 13 | &.red 14 | fill: $palette-1 15 | &.black 16 | fill: #333 17 | &.none 18 | fill: #666 19 | &.dummy 20 | fill: transparent 21 | stroke: #c7c7c7 22 | stroke-width: 2 23 | stroke-dasharray: 4, 2 24 | &.flag-rotate 25 | circle 26 | stroke: $palette-2 27 | stroke-width: 2 28 | &.flag-cycle 29 | circle 30 | fill: $palette-2 31 | stroke: darken($palette-1, 30%) 32 | stroke-dasharray: 2, 2 33 | stroke-width: 2 34 | text 35 | fill: darken($palette-1, 30%) 36 | font-weight: 700 37 | 38 | 39 | &.flag-rotate 40 | &.flag-rotate-upper 41 | &.flag-rotate-child 42 | &.red 43 | &.black 44 | circle 45 | filter: url(#drop-shadow) 46 | &.flag-rotate-upper 47 | &.red 48 | &.black 49 | circle 50 | fill: $palette-4 51 | &.flag-rotate 52 | &.red 53 | &.black 54 | circle 55 | stroke: white 56 | stroke-width: 2 57 | 58 | .edge 59 | animation: 1s ease-in-out 1 60 | fill: none 61 | stroke: #ccc 62 | stroke-width: 2 63 | stroke-dasharray: 4, 2 64 | &.lr 65 | marker-end: url(#right-arrow) 66 | &.rl 67 | marker-end: url(#left-arrow) 68 | &.tb 69 | marker-end: url(#down-arrow) 70 | &.flag 71 | stroke: $palette-1 72 | .arrow 73 | fill: grey 74 | -------------------------------------------------------------------------------- /packages/map/tests/updateMap.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, isThawed, updateMap, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('updateMap()', () => { 9 | // test('returns the same map if no changes are made', () => { 10 | // var map = emptyMap(); 11 | // var map1 = updateMap(m => {}, map); 12 | // var map2 = updateMap(m => m, map); 13 | // assert.strictEqual(map, map1); 14 | // assert.strictEqual(map, map2); 15 | // }); 16 | 17 | test('creates a mutable copy of the original map, then freezes and returns it', () => { 18 | var map = set('x', 3, empty()); 19 | 20 | var map1a: any = void 0; 21 | var map1 = updateMap(m => { 22 | set('y', 5, m); 23 | set('z', 7, m); 24 | assert.isTrue(isThawed(m)); 25 | map1a = m; 26 | }, map); 27 | 28 | var map2a: any = void 0; 29 | var map2 = updateMap(m => { 30 | var m1 = set('x', 9, m); 31 | var m2 = m = set('k', 1, m); 32 | assert.strictEqual(m, m1); 33 | assert.strictEqual(m, m2); 34 | map2a = m2; 35 | assert.isTrue(isThawed(m)); 36 | return m2; 37 | }, map1); 38 | 39 | assert.strictEqual(map1, map1a); 40 | assert.strictEqual(map2, map2a); 41 | assert.isFalse(isThawed(map)); 42 | assert.isFalse(isThawed(map1)); 43 | assert.isFalse(isThawed(map2)); 44 | assert.isFalse(isThawed(map1a)); 45 | assert.isFalse(isThawed(map2a)); 46 | assert.deepEqual(toJS(map), {x: 3}); 47 | assert.deepEqual(toJS(map1), {x: 3, y: 5, z: 7}); 48 | assert.deepEqual(toJS(map2), {x: 9, y: 5, z: 7, k: 1}); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /packages/core/src/collection.ts: -------------------------------------------------------------------------------- 1 | export interface CollectionTypeInfo { 2 | readonly type: symbol; 3 | readonly indexable: boolean; 4 | equals(other: any, collection: any): boolean; 5 | unwrap(collection: any): any; 6 | } 7 | 8 | export interface IndexableCollectionTypeInfo extends CollectionTypeInfo { 9 | get(key: any, collection: any): any; 10 | has(key: any, collection: any): boolean; 11 | set(key: any, value: any, collection: any): any; 12 | update(key: any, updater: (value) => any, collection: any): any; 13 | verifyKey(key: any, collection: any): boolean; 14 | } 15 | 16 | export interface Collection { 17 | readonly '@@type': CollectionTypeInfo; 18 | [Symbol.iterator](): IterableIterator; 19 | } 20 | 21 | export function isCollection(value: any): value is Collection { 22 | return value && typeof value === 'object' && '@@type' in value && Symbol.iterator in value; 23 | } 24 | 25 | export function isEqual(a: any, b: any) { 26 | if(a === b) return true; 27 | if(!isCollection(a) || !isCollection(b) || a['@@type'] !== b['@@type']) return false; 28 | const type = a['@@type']; 29 | return type.equals(a, b); 30 | } 31 | 32 | const CIRCULARS = new WeakMap(); 33 | export function preventCircularRefs>(createTarget: (collection: C) => T, unwrap: (collection: C, target: T) => T, collection: C): T { 34 | if(CIRCULARS.has(collection)) { 35 | return CIRCULARS.get(collection); 36 | } 37 | var target = createTarget(collection); 38 | CIRCULARS.set(collection, target); 39 | var value = unwrap(collection, target); 40 | CIRCULARS.delete(collection); 41 | return value; 42 | } 43 | 44 | export function unwrapAny(value: any): any { 45 | return isCollection(value) ? value['@@type'].unwrap(value) : value; 46 | } -------------------------------------------------------------------------------- /packages/map/src/internals/map.ts: -------------------------------------------------------------------------------- 1 | import {Collection, IndexableCollectionTypeInfo, nextId, batch} from '@collectable/core'; 2 | import {get, has, set, update, iterate, unwrap, isEqual} from '../functions'; 3 | 4 | const MAP_TYPE: IndexableCollectionTypeInfo = { 5 | type: Symbol('Collectable.List'), 6 | indexable: true, 7 | 8 | equals(other: any, collection: any): boolean { 9 | return isEqual(other, collection); 10 | }, 11 | 12 | unwrap(collection: any): any { 13 | return unwrap(true, collection); 14 | }, 15 | 16 | get(key: any, collection: any): any { 17 | return get(key, collection); 18 | }, 19 | 20 | has(key: any, collection: any): boolean { 21 | return has(key, collection); 22 | }, 23 | 24 | set(key: any, value: any, collection: any): any { 25 | return set(key, value, collection); 26 | }, 27 | 28 | update(key: any, updater: (value) => any, collection: any): any { 29 | return update(key, updater, collection); 30 | }, 31 | 32 | verifyKey(key: any, collection: any): boolean { 33 | return true; 34 | }, 35 | }; 36 | 37 | export class HashMap implements Collection<[K, V]> { 38 | get '@@type'() { return MAP_TYPE; } 39 | 40 | constructor( 41 | public _values: Map, 42 | public _owner: number, 43 | public _group: number 44 | ) {} 45 | 46 | [Symbol.iterator](): IterableIterator<[K, V]|undefined> { 47 | return iterate(this); 48 | } 49 | } 50 | 51 | export function cloneMap(map: HashMap, mutable = false): HashMap { 52 | return new HashMap(new Map(map._values), batch.owner(mutable), nextId()); 53 | } 54 | 55 | export function createMap(map?: Map): HashMap { 56 | return new HashMap( 57 | map || new Map(), 58 | nextId(), 59 | batch.owner(false) 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /packages/map/tests/asImmutable.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, isThawed, thaw, freeze, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('asImmutable()', () => { 9 | test('creates an immutable copy of a mutable map', () => { 10 | var map = thaw(set('x', 3, empty())); 11 | var map1 = freeze(map); 12 | 13 | assert.notStrictEqual(map, map1); 14 | assert.isTrue(isThawed(map)); 15 | assert.isFalse(isThawed(map1)); 16 | 17 | set('y', 2, map); 18 | set('z', 2, map1); 19 | 20 | assert.isTrue(isThawed(map)); 21 | assert.isFalse(isThawed(map1)); 22 | assert.deepEqual(toJS(map), {x: 3, y: 2}); 23 | assert.deepEqual(toJS(map1), {x: 3}); 24 | }); 25 | 26 | test('returns the same map if already mutable', () => { 27 | var map = thaw(set('x', 3, empty())); 28 | var map1 = freeze(map); 29 | var map2 = freeze(map1); 30 | 31 | assert.notStrictEqual(map, map1); 32 | assert.strictEqual(map1, map2); 33 | assert.deepEqual(toJS(map), {x: 3}); 34 | assert.deepEqual(toJS(map1), {x: 3}); 35 | }); 36 | 37 | test('operations performed on an immutable map return a new map', () => { 38 | var map = thaw(set('x', 3, empty())); 39 | set('y', 1, map); 40 | set('z', 2, map); 41 | 42 | var map1 = freeze(map); 43 | var map2 = set('z', 3, map1); 44 | 45 | assert.notStrictEqual(map, map1); 46 | assert.notStrictEqual(map, map2); 47 | assert.notStrictEqual(map1, map2); 48 | 49 | assert.deepEqual(toJS(map), {x: 3, y: 1, z: 2}); 50 | assert.deepEqual(toJS(map1), {x: 3, y: 1, z: 2}); 51 | assert.deepEqual(toJS(map2), {x: 3, y: 1, z: 3}); 52 | }); 53 | }); 54 | }); -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Collectable.js: General API 2 | 3 | The general API provides methods to work with deeply-nested combinations of different Collectable.js data structures. 4 | 5 | *This documentation is under construction. The list of functions, descriptions and examples are pending.* 6 | 7 | ## Usage 8 | 9 | To combine multiple data structures effectively, import universal methods from the main package and collection-specific methods from other relevant packages as needed: 10 | 11 | ```js 12 | import {fromObject, updateIn, setIn} from 'collectable'; 13 | import {append} from '@collectable/list/curried'; 14 | 15 | const input = { 16 | foo: 'abc', 17 | xyz: [3, [5, 6], 7, 9] 18 | }; 19 | const map0 = fromObject(input); // <{foo: 'abc', xyz: <[3, [5, 6], 7, 9]>}> 20 | const map1 = updateIn(['xyz', 1, 0], n => 4, map0); // <{foo: 'abc', xyz: <[3, [4, 6], 7, 9]>}> 21 | const map2 = setIn(['foo', 'bar'], x => 'baz', map1); // <{foo: <{bar: 'baz'}>, xyz: ...> 22 | const map3 = updateIn(['xyz', 1], append(42)); // <{..., xyz: <[3, [5, 6, 42], 7, 9]>}> 23 | ``` 24 | 25 | Use a modern bundler such as Webpack 2 or Rollup in order to take advantage of tree shaking capabilities, giving you maximum flexbility to take the whole package as a dependency while excluding anything you don't use from the final build. 26 | 27 | ## API 28 | 29 | All collection-manipulation functions are available from module `collectable`. 30 | 31 | Curried versions of each of these (where applicable) are available from module `collectable/curried`. The curried versions of each function will suffer a minor performance hit due to the additional layers of indirection required to provide a curried interface. In most cases this is not worth worrying about, but if maximum performance is desired, consider using the non-curried API instead. 32 | 33 | ---- 34 | 35 | *Documentation pending* -------------------------------------------------------------------------------- /packages/map/tests/update.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, thaw, isThawed, update, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('update()', () => { 9 | test('returns a new map with the specified key updated', () => { 10 | var map = set('x', 3, empty()); 11 | 12 | var map1 = update('x', x => { 13 | assert.strictEqual(x, 3); 14 | return 2; 15 | }, map); 16 | 17 | var map2 = update('y', y => { 18 | assert.isUndefined(y); 19 | return 2; 20 | }, map); 21 | 22 | assert.isFalse(isThawed(map)); 23 | assert.isFalse(isThawed(map1)); 24 | assert.isFalse(isThawed(map2)); 25 | assert.notStrictEqual(map, map1); 26 | assert.notStrictEqual(map, map2); 27 | assert.notStrictEqual(map1, map2); 28 | assert.deepEqual(toJS(map), {x: 3}); 29 | assert.deepEqual(toJS(map1), {x: 2}); 30 | assert.deepEqual(toJS(map2), {x: 3, y: 2}); 31 | }); 32 | 33 | test('returns the same map if the returned value is unchanged', () => { 34 | var map = set('x', 3, empty()); 35 | var map1 = update('x', x => 3, map); 36 | var map2 = update('y', y => void 0, map); 37 | 38 | assert.isFalse(isThawed(map)); 39 | assert.strictEqual(map, map1); 40 | assert.strictEqual(map, map2); 41 | assert.notProperty(toJS(map), 'y'); 42 | assert.deepEqual(toJS(map), {x: 3}); 43 | }); 44 | 45 | test('returns the same map if the original map is already mutable', () => { 46 | var map = thaw(set('x', 3, empty())); 47 | 48 | assert.isTrue(isThawed(map)); 49 | assert.deepEqual(toJS(map), {x: 3}); 50 | 51 | var map1 = update('y', y => 2, map); 52 | 53 | assert.strictEqual(map, map1); 54 | assert.isTrue(isThawed(map1)); 55 | assert.deepEqual(toJS(map1), {x: 3, y: 2}); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/first.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {RedBlackTreeEntry, empty, first, firstKey, firstValue, iterateFromFirst} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {createTree, sortedValues} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, emptyTree; 7 | 8 | suite('[RedBlackTree]', () => { 9 | setup(() => { 10 | emptyTree = empty(); 11 | tree = createTree(); 12 | }); 13 | 14 | suite('first()', () => { 15 | test('returns undefined if the tree is empty', () => { 16 | assert.isUndefined(first(emptyTree)); 17 | }); 18 | 19 | test('returns a pointer to the first node', () => { 20 | const node = >first(tree); 21 | assert.isDefined(node); 22 | assert.strictEqual(node.key, sortedValues[0]); 23 | }); 24 | }); 25 | 26 | suite('firstKey()', () => { 27 | test('returns undefined if the tree is empty', () => { 28 | assert.isUndefined(firstKey(emptyTree)); 29 | }); 30 | 31 | test('returns the leftmost key', () => { 32 | const key = firstKey(tree); 33 | assert.strictEqual(key, sortedValues[0]); 34 | }); 35 | }); 36 | 37 | suite('firstValue()', () => { 38 | test('returns undefined if the tree is empty', () => { 39 | assert.isUndefined(firstValue(emptyTree)); 40 | }); 41 | 42 | test('returns the leftmost value', () => { 43 | const value = firstValue(tree); 44 | assert.strictEqual(value, `#${sortedValues[0]}`); 45 | }); 46 | }); 47 | 48 | suite('iterateFromFirst()', () => { 49 | test('returns a reverse iterator starting from the leftmost node', () => { 50 | const it = iterateFromFirst(tree); 51 | assert.deepEqual(Array.from(it).map(n => n.key), sortedValues); 52 | }); 53 | 54 | test('the iterator should be in a completed state if the tree is empty', () => { 55 | const it = iterateFromFirst(emptyTree); 56 | assert.isTrue(it.next().done); 57 | }); 58 | }); 59 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/last.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {RedBlackTreeEntry, empty, last, lastKey, lastValue, iterateFromLast} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {createTree, sortedValues} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, emptyTree; 7 | 8 | suite('[RedBlackTree]', () => { 9 | setup(() => { 10 | emptyTree = empty(); 11 | tree = createTree(); 12 | }); 13 | 14 | suite('last()', () => { 15 | test('returns undefined if the tree is empty', () => { 16 | assert.isUndefined(last(emptyTree)); 17 | }); 18 | 19 | test('returns a pointer to the last node', () => { 20 | const node = >last(tree); 21 | assert.isDefined(node); 22 | assert.strictEqual(node.key, sortedValues[sortedValues.length - 1]); 23 | }); 24 | }); 25 | 26 | suite('lastKey()', () => { 27 | test('returns undefined if the tree is empty', () => { 28 | assert.isUndefined(lastKey(emptyTree)); 29 | }); 30 | 31 | test('returns the rightmost key', () => { 32 | const key = lastKey(tree); 33 | assert.strictEqual(key, sortedValues[sortedValues.length - 1]); 34 | }); 35 | }); 36 | 37 | suite('lastValue()', () => { 38 | test('returns undefined if the tree is empty', () => { 39 | assert.isUndefined(lastValue(emptyTree)); 40 | }); 41 | 42 | test('returns the rightmost value', () => { 43 | const value = lastValue(tree); 44 | assert.strictEqual(value, `#${sortedValues[sortedValues.length - 1]}`); 45 | }); 46 | }); 47 | 48 | suite('iterateFromLast()', () => { 49 | test('returns a reverse iterator starting from the rightmost node', () => { 50 | const it = iterateFromLast(tree); 51 | const expected = sortedValues.slice().reverse(); 52 | assert.deepEqual(Array.from(it).map(n => n.key), expected); 53 | }); 54 | 55 | test('the iterator should be in a completed state if the tree is empty', () => { 56 | const it = iterateFromLast(emptyTree); 57 | assert.isTrue(it.next().done); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collectable", 3 | "version": "0.4.0", 4 | "description": "An all-you-can-eat buffet of high-performance immutable/persistent data structures", 5 | "main": "lib/index.js", 6 | "module": "lib/index.js", 7 | "jsnext:main": "lib/index.js", 8 | "typings": "lib/index.d.ts", 9 | "scripts": { 10 | "build-all": "gulp build && gulp build --pkg core && gulp build --pkg list && gulp build --pkg map && gulp build --pkg set", 11 | "test": "mocha --opts ./mocha.opts", 12 | "test-dev": "mocha --opts ./mocha.opts --watch --bail", 13 | "test-dev-mem": "node --max_old_space_size=8192 ./node_modules/mocha/bin/_mocha --opts ./mocha.opts --watch --bail" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "contributors": [ 19 | "Nathan Ridley (https://github.com/axefrog)", 20 | "Tylor Steinberger (https://github.com/TylorS)" 21 | ], 22 | "license": "MIT", 23 | "bugs": "https://github.com/frptools/collectable/issues", 24 | "repository": "git@github.com:frptools/collectable.git", 25 | "devDependencies": { 26 | "@types/chai": "^3.4.34", 27 | "@types/chalk": "^0.4.31", 28 | "@types/mocha": "^2.2.39", 29 | "@types/node": "^7.0.5", 30 | "chai": "^3.5.0", 31 | "gulp": "^3.9.1", 32 | "gulp-mocha": "^4.0.1", 33 | "gulp-plumber": "^1.1.0", 34 | "gulp-sourcemaps": "^2.4.1", 35 | "gulp-transform": "^1.1.0", 36 | "gulp-tslint": "^7.1.0", 37 | "gulp-typedoc": "^2.0.2", 38 | "gulp-typescript": "^3.1.4", 39 | "merge2": "^1.0.3", 40 | "mocha": "^3.2.0", 41 | "rimraf": "^2.6.1", 42 | "source-map-support": "^0.4.11", 43 | "tiny-preprocessor": "^1.0.0", 44 | "tslint": "^4.5.1", 45 | "typedoc": "^0.5.5", 46 | "typedoc-plugin-external-module-map": "^0.0.6", 47 | "typescript": "^2.2.1", 48 | "yargs": "^6.6.0" 49 | }, 50 | "dependencies": { 51 | "@collectable/core": "latest", 52 | "@collectable/list": "latest", 53 | "@collectable/map": "latest", 54 | "@collectable/red-black-tree": "latest", 55 | "@collectable/set": "latest", 56 | "@typed/curry": "latest" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/list/tests/internals/traversal.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, append, fromArray, get, size, thaw} from '../../src'; 3 | import {Slot, OFFSET_ANCHOR, TreeWorker} from '../../src/internals'; 4 | import {text, BRANCH_FACTOR, makeValues} from '../test-utils'; 5 | 6 | suite('[Internals: traversal]', () => { 7 | test('activating the left view for the first time', () => { 8 | var values = makeValues(Math.pow(BRANCH_FACTOR, 3) + 1); 9 | var list = thaw(empty()); 10 | for(var i = 0; i < values.length; i++) { 11 | append(values[i], list); 12 | } 13 | assert.strictEqual(get(0, list), text(0)); 14 | assert.strictEqual(list._left.slot.size, BRANCH_FACTOR); 15 | assert.strictEqual(list._right.slot.size, 1); 16 | }); 17 | 18 | test('refocusing a view down a path reserved by the other view', () => { 19 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2) + BRANCH_FACTOR + 1); 20 | var list = thaw(empty()); 21 | for(var i = 0; i < values.length; i++) { 22 | append(values[i], list); 23 | } 24 | // Force the left view to be created and positioned at the head of the list 25 | assert.strictEqual(get(0, list), text(0)); 26 | assert.strictEqual(list._left.slot.size, BRANCH_FACTOR); 27 | assert.strictEqual(list._right.slot.size, 1); 28 | 29 | // The last write target was the tail, so refocusing to a non-tail ordinal will try to use the left view, which will 30 | // have to ascend to the top of the list where the target slot path is checked out and can't be descended through. 31 | var index = size(list) - BRANCH_FACTOR; 32 | assert.strictEqual(get(index, list), text(index)); 33 | }); 34 | 35 | test('refocusing a reserved tail in a two-node list', () => { 36 | var values = makeValues(BRANCH_FACTOR*2); 37 | var list = fromArray(values); 38 | assert.isTrue((>list._right.parent.slot.slots[1]).isReserved()); 39 | assert.isFalse((>list._right.parent.slot.slots[0]).isReserved()); 40 | var view = TreeWorker.focusView(list, BRANCH_FACTOR - 1, OFFSET_ANCHOR.RIGHT, true); 41 | assert.strictEqual(view, list._right); 42 | assert.isTrue(list._right.slot.isReserved()); 43 | assert.isTrue((>list._right.parent.slot.slots[0]).isReserved()); 44 | assert.isFalse((>list._right.parent.slot.slots[1]).isReserved()); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/list/tests/functions/append.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, append, appendArray, appendIterable} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | 5 | suite('[List]', () => { 6 | suite('append()', () => { 7 | test('should not mutate the original List', () => { 8 | const list = empty(); 9 | const appended = append('foo', list); 10 | assert.strictEqual(list._size, 0); 11 | assert.strictEqual(list._left.slot.slots.length, 0); 12 | assert.notStrictEqual(list, appended); 13 | assert.notDeepEqual(list, appended); 14 | }); 15 | 16 | test('should have size:1 after adding the first element', () => { 17 | const list = append('foo', empty()); 18 | assert.strictEqual(list._size, 1); 19 | assert.deepEqual(arrayFrom(list), ['foo']); 20 | }); 21 | 22 | test('should have size:2 after adding the second element', () => { 23 | const list = append('bar', append('foo', empty())); 24 | assert.strictEqual(list._size, 2); 25 | assert.deepEqual(arrayFrom(list), ['foo', 'bar']); 26 | }); 27 | }); 28 | 29 | suite('appendArray()', () => { 30 | test('should return the original list if called with an empty list', () => { 31 | const list = empty(); 32 | const appended = appendArray([], list); 33 | assert.strictEqual(list._size, 0); 34 | assert.strictEqual(list, appended); 35 | }); 36 | 37 | test('should append each element in the array', () => { 38 | var values = ['foo', 'bar', 'baz']; 39 | const list = appendArray(values, empty()); 40 | assert.strictEqual(list._size, 3); 41 | assert.deepEqual(arrayFrom(list), values); 42 | }); 43 | }); 44 | 45 | suite('appendIterable()', () => { 46 | test('should return the original list if called with an empty iterable', () => { 47 | const list = empty(); 48 | const appended = appendIterable(new Set(), list); 49 | assert.strictEqual(list._size, 0); 50 | assert.strictEqual(list, appended); 51 | }); 52 | 53 | test('should append each value iterated over', () => { 54 | var values = new Set(['foo', 'bar', 'baz']); 55 | const list = appendIterable(values, empty()); 56 | assert.strictEqual(list._size, 3); 57 | assert.sameMembers(arrayFrom(list), Array.from(values)); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/data/data.js: -------------------------------------------------------------------------------- 1 | import {empty, set, remove, isRedBlackTree} from '../../../../../.build/packages/red-black-tree/src'; 2 | import {log} from './index'; 3 | 4 | const unsortedValues = [4740, 7125, 672, 6864, 7232, 8875, 7495, 8161, 706, 2533, 1570, 7568, 1658, 450, 3646, 5 | 8034, 6831, 4674, 1228, 5217, 3609, 571, 5135, 4869, 3755, 2713, 3391, 6, 1485, 9219, 8730, 3536, 4517, 8427, 4495, 6 | 662, 4847, 7866, 2077, 8586, 9128, 6287, 2999, 5173, 1363, 5836, 4990, 4419, 6125, 69, 4041, 9093, 9384, 6520, 2298, 7 | 344, 7155, 778, 229, 3401, 517, 4669, 5113, 1691, 9551, 3437, 3275, 9289, 7670, 9532, 5648, 5797, 5517, 3488, 8343, 8 | 8169, 415, 1564, 2984, 2062, 8060, 6886, 3761, 2701, 7673, 8894, 958, 8988, 954, 5049, 8058, 4040, 3276, 5679, 2021, 9 | 7666, 9599, 4348, 1207, 8591, 2480, 7452, 4048, 3350, 6531, 9771, 7748, 7315, 471, 353, 8512, 8691, 7810, 7611, 4594, 10 | 2551, 4933, 897, 4208, 9691, 1571, 3572, 5834, 6966, 7691, 188, 5525, 2829, 452, 2837, 9508, 6705, 3976, 6027, 9491, 11 | 9010, 3736, 1112, 2863, 6673, 3999, 9411, 3469, 6542, 8632, 2652, 4646, 4734, 5143, 9605, 3555, 3778, 9938, 1788, 12 | 1015, 7383, 6301, 3550, 9054, 1476, 4232, 5886, 4753, 1323, 3821, 2758, 3310, 7807, 7991, 6722, 6519, 3861, 539, 13 | 5478, 8590, 1387, 4249, 3890, 2715, 85, 6190, 307, 8323, 6570, 8780, 1991, 666, 3670, 7111, 8870, 2724, 1501, 7725, 14 | 4163, 6324, 3389, 3673, 4573, 3042, 8176, 6589, 5589, 9507, 3834, 8033, 9354, 5791, 2174, 1975, 9273, 7823, 1137, 15 | 3233, 5851, 9226, 3747, 3794, 5777, 6643, 1832, 9328, 9939, 1333, 7206, 4235, 3253, 462, 8501, 8272, 4664, 8953, 442, 16 | 8931, 7679, 9221, 2894, 948, 4807, 9861, 7630, 5891, 8182].slice(0, 50);//.map(a => [a, Math.random()]).sort((a, b) => a[1] - b[1]).map(a => a[0]); 17 | 18 | const removals = unsortedValues; //.map(a => [a, Math.random()]).sort((a, b) => a[1] - b[1]).map(a => a[0]); 19 | 20 | export function start() { 21 | var tree = empty(); 22 | try { 23 | log(tree, true); 24 | unsortedValues.forEach(n => { 25 | tree = set(n, `#${n}`, tree); 26 | log(tree, true, `Added #${n}`); 27 | }); 28 | removals.forEach(n => { 29 | tree = remove(n, tree); 30 | log(tree, true, `Removed #${n}`); 31 | }); 32 | } 33 | catch(e) { 34 | log(e); 35 | log(tree, false, `ERROR: ${e.message}`); 36 | } 37 | return tree; 38 | } 39 | 40 | export function isInstanceOfVisualisedType(arg) { 41 | return isRedBlackTree(arg); 42 | } 43 | -------------------------------------------------------------------------------- /packages/list/tests/functions/prepend.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, prepend, prependArray, prependIterable} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | 5 | suite('[List]', () => { 6 | suite('prepend()', () => { 7 | test('should not mutate the original List', () => { 8 | const list = empty(); 9 | const prepended = prepend('foo', list); 10 | assert.strictEqual(list._size, 0); 11 | assert.strictEqual(list._left.slot.slots.length, 0); 12 | assert.notStrictEqual(list, prepended); 13 | assert.notDeepEqual(list, prepended); 14 | }); 15 | 16 | test('should have size:1 after adding the first element', () => { 17 | const list = prepend('foo', empty()); 18 | assert.strictEqual(list._size, 1); 19 | assert.deepEqual(arrayFrom(list), ['foo']); 20 | }); 21 | 22 | test('should have size:2 after adding the second element', () => { 23 | const list = prepend('bar', prepend('foo', empty())); 24 | assert.strictEqual(list._size, 2); 25 | assert.deepEqual(arrayFrom(list), ['bar', 'foo']); 26 | }); 27 | }); 28 | 29 | suite('prependArray()', () => { 30 | test('should return the original list if called with an empty list', () => { 31 | const list = empty(); 32 | const prepended = prependArray([], list); 33 | assert.strictEqual(list._size, 0); 34 | assert.strictEqual(list, prepended); 35 | }); 36 | 37 | test('should append elements so that their order in the list matches the source array', () => { 38 | var values = ['foo', 'bar', 'baz']; 39 | const list = prependArray(values, empty()); 40 | assert.strictEqual(list._size, 3); 41 | assert.deepEqual(arrayFrom(list), values); 42 | }); 43 | }); 44 | 45 | suite('prependIterable()', () => { 46 | test('should return the original list if called with an empty iterable', () => { 47 | const list = empty(); 48 | const prepended = prependIterable(new Set(), list); 49 | assert.strictEqual(list._size, 0); 50 | assert.strictEqual(list, prepended); 51 | }); 52 | 53 | test('should prepend each value iterated over', () => { 54 | var values = new Set(['foo', 'bar', 'baz']); 55 | const list = prependIterable(values, empty()); 56 | assert.strictEqual(list._size, 3); 57 | assert.sameMembers(arrayFrom(list), Array.from(values)); 58 | }); 59 | }); 60 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/data/model.js: -------------------------------------------------------------------------------- 1 | function isNone(node) { 2 | return node._left === node; 3 | } 4 | 5 | var _nextId = 0; 6 | function Node(type, size, count, key, ids, pid, gid, pos, flag = '') { 7 | var id, dupe = false; 8 | switch(type) { 9 | case 'dummy': id = `dummy-${gid}-${pos}`; break; 10 | case 'none': id = `leaf-${pid}-${pos}`; break; 11 | default: 12 | id = `node-${key}`; 13 | if(ids.has(id)) { 14 | id += `-${pid}-${pos}`; 15 | dupe = true; 16 | } 17 | break; 18 | } 19 | ids.add(id); 20 | return { 21 | id, 22 | type, 23 | size, 24 | text: key, 25 | count, 26 | flag, 27 | dupe, 28 | }; 29 | } 30 | 31 | const DummyNode = (ids, pid, gid, pos, flag = '') => Node('dummy', 30, 0, void 0, ids, pid, gid, pos, flag); 32 | const VoidNode = (ids, pid, gid, pos) => Node('none', 12, 0, void 0, ids, pid, gid, pos); 33 | const RedNode = (key, count, ids, pid, gid, pos, flag = '') => Node('red', 30, count, key, ids, pid, gid, pos, flag); 34 | const BlackNode = (key, count, ids, pid, gid, pos, flag = '') => Node('black', 30, count, key, ids, pid, gid, pos, flag); 35 | 36 | function SubtreeBranch(node, left, ids, pid, gid, pos) { 37 | const red = ('__red' in node ? node.__red : node._red); 38 | const branchNode = red ? (node._red ? RedNode : BlackNode)(node.key, node._count, ids, pid, gid, pos, node.__flag) : DummyNode(ids, pid, gid, pos, node.__flag); 39 | if(node._red) pid = branchNode.id; 40 | return { 41 | type: 'branch', 42 | node: branchNode, 43 | dummy: !red, 44 | inner: Subtree(red ? left ? node._right : node._left : node, ids, pid, gid, `${pos}i`), 45 | outer: red ? Subtree(left ? node._left : node._right, ids, pid, gid, `${pos}o`) : void 0, 46 | }; 47 | } 48 | 49 | function Subtree(node, ids, pid, gid, pos) { 50 | if(isNone(node)) return VoidNode(ids, pid, gid, pos); 51 | const center = (node._red ? RedNode : BlackNode)(node.key, node._count, ids, pid, gid, pos, node.__flag); 52 | var left, right; 53 | if(center.dupe) { 54 | node.__flag += ` flag-cycle`; 55 | return center; 56 | } 57 | return { 58 | type: 'subtree', 59 | node: center, 60 | left: SubtreeBranch(node._left, true, ids, center.id, pid, `${pos}l`), 61 | right: SubtreeBranch(node._right, false, ids, center.id, pid, `${pos}r`), 62 | }; 63 | } 64 | 65 | export function createModel(tree) { 66 | const ids = new Set(); 67 | return Subtree(tree._root, ids, 'R', 'R', 'root'); 68 | } -------------------------------------------------------------------------------- /packages/set/README.md: -------------------------------------------------------------------------------- 1 | # Collectable.js: Immutable Set 2 | 3 | > An persistent set data structure, backed by an immutable hash map 4 | 5 | [![Build Status](https://travis-ci.org/frptools/collectable.svg?branch=master)](https://travis-ci.org/frptools/collectable) 6 | [![NPM version](https://badge.fury.io/js/%40collectable%2Fset.svg)](http://badge.fury.io/js/%40collectable%2Fset) 7 | [![GitHub version](https://badge.fury.io/gh/frptools%2Fcollectable.svg)](https://badge.fury.io/gh/frptools%2Fcollectable) 8 | 9 | *This documentation is under construction. The list of functions, descriptions and examples are pending.* 10 | 11 | ## Installation 12 | 13 | ``` 14 | # via NPM 15 | npm install --save @collectable/set 16 | 17 | # or Yarn 18 | yarn add @collectable/set 19 | ``` 20 | 21 | If you intend to use other data structures as well, install the main collectable package instead. It takes a dependency on each of these data structures, and so they will become available implicitly, after installation. 22 | 23 | ``` 24 | # via NPM 25 | npm install --save collectable 26 | 27 | # or Yarn 28 | yarn add collectable 29 | ``` 30 | 31 | TypeScript type definitions are included by default. 32 | 33 | ## Usage 34 | 35 | Import and use the functions you need: 36 | 37 | ```js 38 | import {fromArray, arrayFrom} from '@collectable/set'; 39 | 40 | const set = fromArray(['X', 'Y']); // => <[X, Y]> 41 | const array = arrayFrom(set); // => [X, Y] 42 | ``` 43 | 44 | Pre-curried versions of functions for a given data structure are available by appending `/curried` to the import path, like so: 45 | 46 | ```ts 47 | import {fromArray, append} from '@collectable/set/curried'; 48 | 49 | const addZ = append('Z'); 50 | const set = addZ(fromArray(['X', 'Y'])); // => <[X, Y, Z]> 51 | ``` 52 | 53 | Use a modern bundler such as Webpack 2 or Rollup in order to take advantage of tree shaking capabilities, giving you maximum flexbility to use what you need while excluding anything else from the final build. 54 | 55 | ## API 56 | 57 | All set-manipulation functions are available from module `@collectable/set`. 58 | 59 | Curried versions of each of these (where applicable) are available from module `@collectable/set/curried`. The curried versions of each function will suffer a minor performance hit due to the additional layers of indirection required to provide a curried interface. In most cases this is not worth worrying about, but if maximum performance is desired, consider using the non-curried API instead. 60 | 61 | ---- 62 | 63 | *Documentation pending* -------------------------------------------------------------------------------- /packages/list/tests/functions/set.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, fromArray, set} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 5 | 6 | suite('[List]', () => { 7 | suite('set()', () => { 8 | test('throws an error if the index is out of range', () => { 9 | assert.throws(() => set(0, 'X', empty())); 10 | assert.throws(() => set(2, 'Z', fromArray(['X', 'Y']))); 11 | assert.throws(() => set(-3, 'Z', fromArray(['X', 'Y']))); 12 | }); 13 | 14 | test('updates the value at the specified index', () => { 15 | var values = ['A', 'B', 'C', 'X', 'Y', 'Z']; 16 | var list1 = fromArray(values); 17 | var list2 = set(0, 'J', list1); 18 | list2 = set(2, 'K', list2); 19 | list2 = set(5, 'L', list2); 20 | assert.deepEqual(arrayFrom(list1), values); 21 | assert.deepEqual(arrayFrom(list2), ['J', 'B', 'K', 'X', 'Y', 'L']); 22 | 23 | values = makeValues(Math.pow(BRANCH_FACTOR, 3)); 24 | var expected = values.slice(); 25 | expected[0] = 'J'; 26 | expected[BRANCH_FACTOR*2] = 'K'; 27 | expected[expected.length - 1] = 'L'; 28 | 29 | list1 = fromArray(values); 30 | list2 = set(0, 'J', list1); 31 | list2 = set(BRANCH_FACTOR*2, 'K', list2); 32 | list2 = set(expected.length - 1, 'L', list2); 33 | assert.deepEqual(arrayFrom(list1), values); 34 | assert.deepEqual(arrayFrom(list2), expected); 35 | 36 | list1 = fromArray(values); 37 | list2 = set(expected.length - 1, 'L', list1); 38 | list2 = set(BRANCH_FACTOR*2, 'K', list2); 39 | list2 = set(0, 'J', list2); 40 | assert.deepEqual(arrayFrom(list1), values); 41 | assert.deepEqual(arrayFrom(list2), expected); 42 | 43 | list1 = fromArray(values); 44 | list2 = set(BRANCH_FACTOR*2, 'K', list1); 45 | list2 = set(expected.length - 1, 'L', list2); 46 | list2 = set(0, 'J', list2); 47 | assert.deepEqual(arrayFrom(list1), values); 48 | assert.deepEqual(arrayFrom(list2), expected); 49 | }); 50 | 51 | test('updates the value at a location relative to the end of the list if the specified index is negative', () => { 52 | var list1 = fromArray(['A', 'B', 'C', 'X', 'Y', 'Z']); 53 | var list2 = set(-2, 'J', list1); 54 | assert.deepEqual(arrayFrom(list1), ['A', 'B', 'C', 'X', 'Y', 'Z']); 55 | assert.deepEqual(arrayFrom(list2), ['A', 'B', 'C', 'X', 'J', 'Z']); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/set.ts: -------------------------------------------------------------------------------- 1 | import {log} from '../internals/debug'; // ## DEV ## 2 | import {PathNode} from '../internals'; 3 | import {isImmutable} from '@collectable/core'; 4 | import { 5 | RedBlackTree, 6 | RedBlackTreeImpl, 7 | findPath, 8 | BRANCH, 9 | createNode, 10 | editable, 11 | rebalance, 12 | setChild, 13 | cloneAsMutable, 14 | doneMutating, 15 | assignValue, 16 | checkInvalidNilAssignment // ## DEV ## 17 | } from '../internals'; 18 | 19 | /** 20 | * Adds a new key and value to the tree, or updates the value if the key was previously absent from the tree. If the new 21 | * value is the equal to a value already associated with the specified key, no change is made, and the original tree is 22 | * returned. 23 | * 24 | * @export 25 | * @template K The type of keys in the tree 26 | * @template V The type of values in the tree 27 | * @param {K} key The key of the entry to be updated or inserted 28 | * @param {V} value The value that should be associated with the key 29 | * @param {RedBlackTree} tree The tree to be updated 30 | * @returns {RedBlackTree} An updated copy of the tree, or the same tree if the input tree was already mutable 31 | */ 32 | export function set(key: K, value: V, tree: RedBlackTree): RedBlackTree; 33 | export function set(key: K, value: V, tree: RedBlackTreeImpl): RedBlackTree { 34 | log(`[set (#${key})] insert: ${key}`); // ## DEV ## 35 | var immutable = isImmutable(tree._owner); 36 | var oldTree = tree; 37 | if(immutable) { 38 | tree = >cloneAsMutable(tree); 39 | } 40 | 41 | if(tree._size === 0) { 42 | tree._root = createNode(tree._group, false, key, value); 43 | tree._size = 1; 44 | return immutable ? doneMutating(tree) : tree; 45 | } 46 | 47 | tree._root = editable(tree._group, tree._root); 48 | var p = findPath(key, tree._root, tree._compare, tree._group); 49 | checkInvalidNilAssignment(); // ## DEV ## 50 | 51 | if(p.next === BRANCH.NONE) { 52 | var replaced = assignValue(value, p.node); 53 | PathNode.release(p, tree._root); 54 | if(!replaced) { 55 | tree = oldTree; 56 | } 57 | } 58 | else { 59 | var node = createNode(tree._group, true, key, value); 60 | 61 | setChild(p.next, p.node, node); 62 | log(`[set (#${key})] ${node._red ? 'red' : 'black'}`); // ## DEV ## 63 | log(tree, false, `[set (#${key})] Pre-insert`); // ## DEV ## 64 | 65 | rebalance(p, node, p.node, tree); 66 | tree._size++; 67 | } 68 | checkInvalidNilAssignment(); // ## DEV ## 69 | 70 | log(`[set (${key})] insertion complete.`); // ## DEV ## 71 | 72 | return immutable ? doneMutating(tree) : tree; 73 | } 74 | -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/unwrap.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {arrayFrom, fromPairs, unwrap} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {empty, createTree, sortedValues, toPair} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, 7 | emptyTree: RedBlackTreeImpl, 8 | deepTree: RedBlackTreeImpl; 9 | 10 | suite('[RedBlackTree]', () => { 11 | setup(() => { 12 | emptyTree = empty(); 13 | tree = createTree(); 14 | deepTree = >fromPairs([ 15 | [2, 'X'], 16 | [6, 'Y'], 17 | [4, fromPairs([ 18 | [5, 'X'], 19 | [1, fromPairs([ 20 | ['A', 6] 21 | ])], 22 | [3, 'B'] 23 | ])], 24 | [10, 'C'] 25 | ]); 26 | }); 27 | 28 | suite('arrayFrom()', () => { 29 | test('returns an empty array if the tree is empty', () => { 30 | assert.deepEqual(arrayFrom(emptyTree), []); 31 | }); 32 | 33 | test('returns an array of pairs of all entries in a single-node tree', () => { 34 | var tree = fromPairs([[3, 5]]); 35 | assert.deepEqual(arrayFrom(tree).map(toPair), [[3, 5]]); 36 | }); 37 | 38 | test('returns an array of pairs of all entries in a populated tree', () => { 39 | assert.deepEqual(arrayFrom(tree).map(toPair), sortedValues.map(n => [n, `#${n}`])); 40 | }); 41 | 42 | test('returns an array of transformed values if a mapping function is provided', () => { 43 | const map = (v, k, i) => `${k};${v};${i}`; 44 | const expected = sortedValues.map((v, i) => map(`#${v}`, v, i)); 45 | assert.deepEqual(arrayFrom(map, tree), expected); 46 | }); 47 | }); 48 | 49 | suite('unwrap()', () => { 50 | test('returns an empty array if the tree is empty', () => { 51 | assert.deepEqual(unwrap(false, emptyTree), {}); 52 | }); 53 | 54 | test('also unwraps embedded collections if deep == true', () => { 55 | const expected = { 56 | '2': 'X', 57 | '4': { 58 | '1': { 59 | 'A': 6 60 | }, 61 | '3': 'B', 62 | '5': 'X' 63 | }, 64 | '6': 'Y', 65 | '10': 'C' 66 | }; 67 | assert.deepEqual(unwrap(true, deepTree), expected); 68 | }); 69 | 70 | test('returns an array of all entries in a single-node tree', () => { 71 | var tree = fromPairs([[3, 5]]); 72 | assert.deepEqual(unwrap(false, tree), {'3': 5}); 73 | }); 74 | 75 | test('returns an array of all entries in a populated tree', () => { 76 | assert.deepEqual(unwrap(false, tree), sortedValues.reduce((o, n) => (o[n] = `#${n}`, o), {})); 77 | }); 78 | }); 79 | }); -------------------------------------------------------------------------------- /packages/map/tests/remove.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, thaw, isThawed, updateMap, has, set, remove, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('remove()', () => { 9 | test('returns a new map is the original map is immutable', () => { 10 | var map = set('x', 3, empty()); 11 | var map1 = remove('x', map); 12 | 13 | assert.notStrictEqual(map, map1); 14 | }); 15 | 16 | test('returns the same map is the original map is mutable', () => { 17 | var map = thaw(set('x', 3, empty())); 18 | var map1 = remove('x', map); 19 | 20 | assert.strictEqual(map, map1); 21 | }); 22 | 23 | test('removes the specified value from a new map each time it is called on an immutable map', () => { 24 | var map = updateMap(m => { 25 | set('x', 1, m); 26 | set('y', 3, m); 27 | set('z', 5, m); 28 | }, empty()); 29 | 30 | var map1 = remove('x', map); 31 | var map2 = remove('y', map1); 32 | var map3 = remove('z', map1); 33 | 34 | assert.notStrictEqual(map, map1); 35 | assert.notStrictEqual(map, map2); 36 | assert.notStrictEqual(map, map3); 37 | assert.notStrictEqual(map1, map2); 38 | assert.notStrictEqual(map1, map3); 39 | assert.notStrictEqual(map2, map3); 40 | 41 | assert.isFalse(isThawed(map)); 42 | assert.isFalse(isThawed(map1)); 43 | assert.isFalse(isThawed(map2)); 44 | assert.isFalse(isThawed(map3)); 45 | 46 | assert.deepEqual(toJS(map), {x: 1, y: 3, z: 5}); 47 | assert.deepEqual(toJS(map1), {y: 3, z: 5}); 48 | assert.deepEqual(toJS(map2), {z: 5}); 49 | assert.deepEqual(toJS(map3), {y: 3}); 50 | }); 51 | 52 | test('removes the specified value from the same map each time it is called on a mutable map', () => { 53 | var map = thaw(updateMap(m => { 54 | set('x', 1, m); 55 | set('y', 3, m); 56 | set('z', 5, m); 57 | }, empty())); 58 | 59 | var map1 = remove('x', map); 60 | var map2 = remove('y', map1); 61 | 62 | assert.strictEqual(map, map1); 63 | assert.strictEqual(map, map2); 64 | assert.isTrue(isThawed(map)); 65 | assert.deepEqual(toJS(map), {z: 5}); 66 | }); 67 | 68 | test('returns the same map if the specified key is missing', () => { 69 | var map = set('y', 2, set('x', 3, empty())); 70 | var map1 = remove('z', map); 71 | 72 | assert.strictEqual(map, map1); 73 | assert.deepEqual(toJS(map), {x: 3, y: 2}); 74 | assert.isFalse(has('z', map1)); 75 | }); 76 | }); 77 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/styles.styl: -------------------------------------------------------------------------------- 1 | @import './palette.styl' 2 | 3 | html, body 4 | height: 100% 5 | body 6 | font-family: 'Lato', sans-serif 7 | font-size: 12px 8 | margin: 0 9 | color: #333 10 | 11 | .header 12 | position: sticky 13 | background-color: white 14 | top: 0 15 | left: 0 16 | right: 350px 17 | display: flex 18 | flex-direction: column 19 | border-bottom: 1px solid #e7e7e7 20 | 21 | .title 22 | font-size: 24px 23 | font-weight: bold 24 | white-space: nowrap 25 | padding 10px 20px 26 | color: $palette-4 27 | 28 | .info 29 | border-top: 1px solid #e7e7e7 30 | background-color: #f9f9f9 31 | padding 0 20px 32 | color: #999 33 | 34 | .instructions 35 | margin: 0 36 | padding: 0 37 | li 38 | list-style-type: none 39 | display: inline-flex 40 | align-items: center 41 | margin: 5px 20px 5px 0 42 | .ctl 43 | margin-right: 10px 44 | code 45 | color: #333 46 | display: inline-block 47 | text-align: center 48 | box-sizing: border-box 49 | box-sizing: border-box 50 | background-color: #e7e7e7 51 | padding: 2px 6px 52 | margin: 0 2px 53 | 54 | .main 55 | position: fixed 56 | right: 350px 57 | left: 0 58 | top: 0 59 | overflow-x: auto 60 | box-sizing: border-box 61 | height: 100vh 62 | background-image: url(http://cdn.backgroundhost.com/backgrounds/subtlepatterns/dust.png) 63 | background-attachment: local 64 | 65 | .diagram 66 | padding: 20px 67 | svg 68 | margin-right: 20px 69 | 70 | .nav 71 | position: relative 72 | z-index: 2 73 | float: right 74 | box-shadow: 0 0 20px rgba(black, 0.15) 75 | .versions 76 | width: 350px 77 | background-color: white 78 | .list-item 79 | .link 80 | display: block 81 | padding: 4px 10px 5px; 82 | text-decoration: none 83 | color: #333 84 | + .list-item 85 | .link 86 | border-top: 1px dotted #ccc 87 | &.active 88 | .link 89 | border-top: 1px solid $palette-5 90 | &.active 91 | .link 92 | cursor: default 93 | pointer-events: none 94 | background-color: $palette-5 95 | color: $palette-2 96 | + .list-item 97 | .link 98 | border-top: 1px solid $palette-5 99 | &.done 100 | .link 101 | font-weight: 600 102 | &.done:not(.active) 103 | .link 104 | color: $palette-4 105 | background-color: $palette-3t 106 | &:not(.active):hover 107 | &.done:not(.active):hover 108 | .link 109 | background-color: $palette-2 110 | color: $palette-5 -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/versions.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import CJ from 'circular-json'; 3 | import {create} from '@most/create'; 4 | import {isInstanceOfVisualisedType, createModel, setCallback} from '../data'; 5 | 6 | function addNext(add) { 7 | let last = null, ref = null, label = null, done = false, logs = [], log = []; 8 | 9 | setCallback((args) => { 10 | if(args.length === 0) { 11 | return; 12 | } 13 | 14 | function saveAndClear() { 15 | if(!ref) return; 16 | if(log.length) { 17 | logs.push(log); 18 | log = []; 19 | } 20 | add({ 21 | ref, 22 | model: createModel(ref), 23 | done, 24 | label: label || `Collection size: ${ref._size}`, 25 | logs 26 | }); 27 | ref = null; 28 | last = null; 29 | done = false; 30 | label = null; 31 | logs = []; 32 | } 33 | 34 | function addLog(arg) { 35 | if(isInstanceOfVisualisedType(arg)) { 36 | saveAndClear(); 37 | ref = arg; 38 | last = 'instance'; 39 | return; 40 | } 41 | else { 42 | log.push(arg && typeof arg === 'object' && !(arg instanceof Error) ? CJ.parse(CJ.stringify(arg)) : arg); 43 | } 44 | } 45 | 46 | for(let i = 0; i < args.length; i++) { 47 | let arg = args[i]; 48 | switch(last) { 49 | case 'instance': 50 | switch(typeof arg) { 51 | case 'boolean': 52 | done = arg; 53 | last = 'done'; 54 | break; 55 | 56 | case 'string': 57 | label = arg; 58 | saveAndClear(); 59 | break; 60 | 61 | default: 62 | addLog(arg); 63 | break; 64 | } 65 | break; 66 | 67 | case 'done': 68 | if(typeof arg === 'string') { 69 | label = arg; 70 | saveAndClear(); 71 | } 72 | else { 73 | saveAndClear(); 74 | addLog(arg); 75 | } 76 | break; 77 | 78 | default: 79 | addLog(arg); 80 | break; 81 | } 82 | } 83 | 84 | if(log.length > 0) { 85 | logs.push(log); 86 | log = []; 87 | } 88 | 89 | saveAndClear(); 90 | }); 91 | } 92 | 93 | function appendToHistory(history, entry) { 94 | return history.push(entry); 95 | } 96 | 97 | export function VersionList(sources) { 98 | const versions$ = create(addNext) 99 | .scan(appendToHistory, Immutable.List()) 100 | .multicast(); 101 | return { 102 | versions$ 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /packages/core/src/array.ts: -------------------------------------------------------------------------------- 1 | export function copyArray(values: T[]): T[] { 2 | if(values.length > 7) { 3 | var arr = new Array(values.length); 4 | for(var i = 0; i < values.length; i++) { 5 | arr[i] = values[i]; 6 | } 7 | return arr; 8 | } 9 | switch(values.length) { 10 | case 0: return []; 11 | case 1: return [values[0]]; 12 | case 2: return [values[0], values[1]]; 13 | case 3: return [values[0], values[1], values[2]]; 14 | case 4: return [values[0], values[1], values[2], values[3]]; 15 | case 5: return [values[0], values[1], values[2], values[3], values[4]]; 16 | case 6: return [values[0], values[1], values[2], values[3], values[4], values[5]]; 17 | case 7: return [values[0], values[1], values[2], values[3], values[4], values[5], values[6]]; 18 | default: return values.slice(); // never reached, but seems to trigger optimization in V8 for some reason 19 | } 20 | } 21 | 22 | export type MappingFunction = (value: T, index: number) => U; 23 | export type KeyedMappingFunction = (value: V, key: K, index: number) => U; 24 | 25 | export function concatArray(left: T[], right: T[]): T[] { 26 | var arr = new Array(left.length + right.length); 27 | for(var i = 0; i < left.length; i++) { 28 | arr[i] = left[i]; 29 | } 30 | for(var j = 0; j < right.length; i++, j++) { 31 | arr[i] = right[j]; 32 | } 33 | return arr; 34 | } 35 | 36 | export function blockCopyMapped(mapper: MappingFunction, sourceValues: T[], targetValues: U[], sourceIndex: number, targetIndex: number, count: number): void { 37 | if(sourceValues === targetValues && sourceIndex < targetIndex) { 38 | for(var i = sourceIndex + count - 1, j = targetIndex + count - 1, c = 0; c < count; i--, j--, c++) { 39 | targetValues[j] = mapper(sourceValues[i], j); 40 | } 41 | } 42 | else { 43 | for(var i = sourceIndex, j = targetIndex, c = 0; c < count; i++, j++, c++) { 44 | targetValues[j] = mapper(sourceValues[i], j); 45 | } 46 | } 47 | } 48 | 49 | export function blockCopy(sourceValues: T[], targetValues: T[], sourceIndex: number, targetIndex: number, count: number): void { 50 | if(sourceValues === targetValues && sourceIndex < targetIndex) { 51 | for(var i = sourceIndex + count - 1, j = targetIndex + count - 1, c = 0; c < count; i--, j--, c++) { 52 | targetValues[j] = sourceValues[i]; 53 | } 54 | } 55 | else { 56 | for(var i = sourceIndex, j = targetIndex, c = 0; c < count; i++, j++, c++) { 57 | targetValues[j] = sourceValues[i]; 58 | } 59 | } 60 | } 61 | 62 | export function truncateLeft(values: T[], start: number): T[] { 63 | var array = new Array(values.length - start); 64 | for(var i = 0, j = start; j < values.length; i++, j++) { 65 | array[i] = values[j]; 66 | } 67 | return array; 68 | } -------------------------------------------------------------------------------- /packages/list/tests/functions/mutability.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {fromArray, freeze, thaw, set, take, append, appendArray, isFrozen} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | 5 | suite('[List]', () => { 6 | suite('freeze()', () => { 7 | test('should return the same list if already frozen', () => { 8 | const list = fromArray(['X', 'Y', 'Z']); 9 | assert.strictEqual(freeze(list), list); 10 | }); 11 | 12 | test('should return a new list if not frozen', () => { 13 | const list = thaw(fromArray(['X', 'Y', 'Z'])); 14 | assert.notStrictEqual(freeze(list), list); 15 | }); 16 | 17 | test('should cause common operations to avoid mutating the input list', () => { 18 | const values = ['X', 'Y', 'Z']; 19 | const list = freeze(thaw(fromArray(values))); 20 | const list1 = append('K', list); 21 | const list2 = set(1, 'K', list); 22 | const list3 = take(2, list); 23 | assert.notStrictEqual(list1, list); 24 | assert.notStrictEqual(list2, list); 25 | assert.notStrictEqual(list3, list); 26 | assert.deepEqual(arrayFrom(list), values); 27 | assert.deepEqual(arrayFrom(list1), values.concat('K')); 28 | assert.deepEqual(arrayFrom(list2), [values[0], 'K', values[2]]); 29 | assert.deepEqual(arrayFrom(list3), values.slice(0, 2)); 30 | }); 31 | }); 32 | 33 | suite('thaw()', () => { 34 | test('should return the same list if already unfrozen', () => { 35 | const list = thaw(fromArray(['X', 'Y', 'Z'])); 36 | assert.strictEqual(thaw(list), list); 37 | }); 38 | 39 | test('should return a new list if frozen', () => { 40 | const list = fromArray(['X', 'Y', 'Z']); 41 | assert.notStrictEqual(thaw(list), list); 42 | }); 43 | 44 | test('should cause common operations to directly mutate the input list', () => { 45 | const values = ['X', 'Y', 'Z']; 46 | const list = thaw(fromArray(values)); 47 | const list1 = appendArray(['A', 'B', 'C', 'D', 'E', 'F'], list); 48 | const list2 = set(5, 'K', list); 49 | const list3 = take(6, list); 50 | assert.strictEqual(list, list1); 51 | assert.strictEqual(list, list2); 52 | assert.strictEqual(list, list3); 53 | assert.deepEqual(arrayFrom(list), ['X', 'Y', 'Z', 'A', 'B', 'K']); 54 | }); 55 | }); 56 | 57 | suite('isFrozen()', () => { 58 | test('should return true if the list is frozen', () => { 59 | const list = fromArray(['X', 'Y', 'Z']); 60 | assert.isTrue(isFrozen(list)); 61 | assert.isTrue(isFrozen(freeze(thaw(list)))); 62 | }); 63 | 64 | test('should return false if the list is unfrozen', () => { 65 | const list = thaw(fromArray(['X', 'Y', 'Z'])); 66 | assert.isFalse(isFrozen(list)); 67 | assert.isFalse(isFrozen(thaw(freeze(list)))); 68 | }); 69 | }); 70 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/get.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from '@collectable/core'; 2 | import {RedBlackTree, RedBlackTreeImpl, RedBlackTreeIterator, isNone, findPathToNodeByKey} from '../internals'; 3 | 4 | /** 5 | * Retrieves the value associated with the specified key 6 | * 7 | * @export 8 | * @template K The type of keys in the tree 9 | * @template V The type of values in the tree 10 | * @param {K} key The key of the entry to retrieve 11 | * @param {RedBlackTree} tree The input tree 12 | * @returns {(V|undefined)} The value associated with the specified key, or undefined if the key does not exist in the tree 13 | */ 14 | export function get(key: K, tree: RedBlackTree): V|undefined; 15 | export function get(key: K, tree: RedBlackTreeImpl): V|undefined { 16 | var node = tree._root, 17 | compare = tree._compare, 18 | found = false, 19 | value: V|undefined; 20 | do { 21 | if(isNone(node)) { 22 | found = true; 23 | } 24 | else { 25 | var c = compare(key, node.key); 26 | if(c === 0) { 27 | value = node.value; 28 | found = true; 29 | } 30 | else if(c > 0) { 31 | node = node._right; 32 | } 33 | else { 34 | node = node._left; 35 | } 36 | } 37 | } while(!found); 38 | return value; 39 | } 40 | 41 | /** 42 | * Determines whether or not a given key exists in the tree 43 | * 44 | * @export 45 | * @template K The type of keys in the tree 46 | * @template V The type of values in the tree 47 | * @param {K} key The key to look for 48 | * @param {RedBlackTree} tree The input tree 49 | * @returns {boolean} True if the there is an entry for the specified key, otherwise false 50 | */ 51 | export function has(key: K, tree: RedBlackTree): boolean { 52 | return isDefined(get(key, tree)); 53 | } 54 | 55 | /** 56 | * Creates an iterator for which the first entry has the specified index in the tree. If the key does not exist in the 57 | * tree, an empty iterator is returned. 58 | * 59 | * @export 60 | * @template K The type of keys in the tree 61 | * @template V The type of values in the tree 62 | * @param {boolean} reverse If true, the iterator will iterate backward toward the first entry in the tree 63 | * @param {K} key The key to look for 64 | * @param {RedBlackTree} tree The input tree 65 | * @returns {RedBlackTreeIterator} An iterator that retrieves each successive entry in the tree, starting from the specified key 66 | */ 67 | export function iterateFromKey(reverse: boolean, key: K, tree: RedBlackTree): RedBlackTreeIterator; 68 | export function iterateFromKey(reverse: boolean, key: K, tree: RedBlackTreeImpl): RedBlackTreeIterator { 69 | const path = findPathToNodeByKey(key, tree._root, tree._compare); 70 | return new RedBlackTreeIterator(path, reverse); 71 | } -------------------------------------------------------------------------------- /packages/map/README.md: -------------------------------------------------------------------------------- 1 | # Collectable.js: Immutable Map 2 | 3 | > An persistent hash map (dictionary) data structure 4 | 5 | [![Build Status](https://travis-ci.org/frptools/collectable.svg?branch=master)](https://travis-ci.org/frptools/collectable) 6 | [![NPM version](https://badge.fury.io/js/%40collectable%2Fmap.svg)](http://badge.fury.io/js/%40collectable%2Fmap) 7 | [![GitHub version](https://badge.fury.io/gh/frptools%2Fcollectable.svg)](https://badge.fury.io/gh/frptools%2Fcollectable) 8 | [![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/FRPTools/Lobby) 9 | 10 | A Clojure-style hash-array-mapped trie, adapted by [TylorS](https://github.com/TylorS) from [Matt Bierner's HAMT](https://github.com/mattbierner/hamt_plus) implementation. 11 | 12 | *This documentation is under construction. The list of functions, descriptions and examples are pending.* 13 | 14 | ## Installation 15 | 16 | ``` 17 | # via NPM 18 | npm install --save @collectable/map 19 | 20 | # or Yarn 21 | yarn add @collectable/map 22 | ``` 23 | 24 | If you intend to use other data structures as well, install the main collectable package instead. It takes a dependency on each of these data structures, and so they will become available implicitly, after installation. 25 | 26 | ``` 27 | # via NPM 28 | npm install --save collectable 29 | 30 | # or Yarn 31 | yarn add collectable 32 | ``` 33 | 34 | TypeScript type definitions are included by default. 35 | 36 | ## Usage 37 | 38 | Import and use the functions you need: 39 | 40 | ```js 41 | import {fromObject, objectFrom} from '@collectable/map'; 42 | 43 | const map = fromObject({foo: 'bar'}); // => <{foo: 'bar'}> 44 | const pojo = objectFrom(list); // => {foo: 'bar'} 45 | ``` 46 | 47 | Pre-curried versions of functions for a given data structure are available by appending `/curried` to the import path, like so: 48 | 49 | ```ts 50 | import {empty, set} from '@collectable/map/curried'; 51 | 52 | const setFoo = set('foo'); 53 | const map = setFoo('bar', empty()); // => <{foo: 'bar'}> 54 | 55 | const setFooBar = set('foo', 'bar'); 56 | const map = setFooBar(empty()); // => <{foo: 'bar'}> 57 | ``` 58 | 59 | Use a modern bundler such as Webpack 2 or Rollup in order to take advantage of tree shaking capabilities, giving you maximum flexbility to use what you need while excluding anything else from the final build. 60 | 61 | ## API 62 | 63 | All map-manipulation functions are available from module `@collectable/map`. 64 | 65 | Curried versions of each of these (where applicable) are available from module `@collectable/map/curried`. The curried versions of each function will suffer a minor performance hit due to the additional layers of indirection required to provide a curried interface. In most cases this is not worth worrying about, but if maximum performance is desired, consider using the non-curried API instead. 66 | 67 | ---- 68 | 69 | *Documentation pending* -------------------------------------------------------------------------------- /packages/list/src/functions/append.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable} from '@collectable/core'; 2 | import {log} from '../internals/debug'; // ## DEV ## 3 | import {CONST, OFFSET_ANCHOR, List, cloneAsMutable, appendValues, ensureImmutable} from '../internals'; 4 | 5 | /** 6 | * Appends a new value to the end of a list, growing the size of the list by one. 7 | * 8 | * @template T - The type of value contained by the list 9 | * @param value - The value to append to the list 10 | * @param list - The list to which the value should be appended 11 | * @returns A list containing the appended value 12 | */ 13 | export function append(value: T, list: List): List { 14 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 15 | var tail = list._right; 16 | var slot = tail.slot; 17 | log(`Begin append of value "${value}" to list of size ${list._size}`); // ## DEV ## 18 | if(tail.group !== 0 && tail.offset === 0 && slot.group !== 0 && slot.size < CONST.BRANCH_FACTOR) { 19 | list._lastWrite = OFFSET_ANCHOR.RIGHT; 20 | list._size++; 21 | if(slot.group === list._group) { 22 | slot.adjustRange(0, 1, true); 23 | } 24 | else { 25 | slot = slot.cloneWithAdjustedRange(list._group, 0, 1, true, true); 26 | if(tail.group !== list._group) { 27 | tail = tail.cloneToGroup(list._group); 28 | list._right = tail; 29 | } 30 | tail.slot = slot; 31 | } 32 | tail.sizeDelta++; 33 | tail.slotsDelta++; 34 | slot.slots[slot.slots.length - 1] = arguments[0]; 35 | } 36 | else { 37 | appendValues(list, [value]); 38 | } 39 | return immutable ? ensureImmutable(list, true) : list; 40 | } 41 | 42 | /** 43 | * Appends an array of values to the end of a list, growing the size of the list by the number of 44 | * elements in the array. 45 | * 46 | * @template T - The type of value contained by the list 47 | * @param value - The values to append to the list 48 | * @param list - The list to which the values should be appended 49 | * @returns A list containing the appended values 50 | */ 51 | export function appendArray(values: T[], list: List): List { 52 | if(values.length === 0) return list; 53 | var immutable = isImmutable(list._owner) && (list = cloneAsMutable(list), true); 54 | appendValues(list, values); 55 | return immutable ? ensureImmutable(list, true) : list; 56 | } 57 | 58 | /** 59 | * Appends a set of values to the end of a list, growing the size of the list by the number of 60 | * elements iterated over. 61 | * 62 | * @template T - The type of value contained by the list 63 | * @param value - The values to append to the list 64 | * @param list - The list to which the values should be appended 65 | * @returns A list containing the appended values 66 | */ 67 | export function appendIterable(values: Iterable, list: List): List { 68 | return appendArray(Array.from(values), list); 69 | } 70 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/internals/node.ts: -------------------------------------------------------------------------------- 1 | export interface Node { 2 | _group: number; 3 | key: K; 4 | value: V; 5 | _red: boolean; 6 | _left: Node; 7 | _right: Node; 8 | _count: number; 9 | } 10 | 11 | /** A read-only reference to an entry in a RedBlackTree instance. */ 12 | export type RedBlackTreeEntry = { 13 | /** Read only. The hash key of this entry in the tree. */ 14 | readonly key: K; 15 | /** Read only. The value of this entry in the tree. */ 16 | readonly value: V; 17 | }; 18 | 19 | export /* ## PROD [[ const ]] ## */ enum BRANCH { 20 | NONE = 0, 21 | LEFT = 1, 22 | RIGHT = 2 23 | }; 24 | 25 | export const NONE: Node = { 26 | _group: 0, 27 | key: void 0, 28 | value: void 0, 29 | _red: false, 30 | _left: void 0, 31 | _right: void 0, 32 | _count: 0 33 | }; 34 | NONE._left = NONE; 35 | NONE._right = NONE; 36 | 37 | // ## DEV [[ 38 | export function checkInvalidNilAssignment() { 39 | if(NONE._left !== NONE) throw new Error(`Invalid assignment of ${NONE._left.key} to left child of NIL node`); // ## DEV 40 | if(NONE._right !== NONE) throw new Error(`Invalid assignment of ${NONE._right.key} to right child of NIL node`); // ## DEV 41 | } 42 | // ]] ## 43 | 44 | export function createNode(group: number, red: boolean, key: K, value: V): Node { 45 | return {_group: group, key, value, _red: red, _left: NONE, _right: NONE, _count: 1}; 46 | } 47 | 48 | export function cloneNode(group: number, node: Node): Node { 49 | return { 50 | _group: group, 51 | key: node.key, 52 | value: node.value, 53 | _red: node._red, 54 | _left: node._left, 55 | _right: node._right, 56 | _count: node._count 57 | }; 58 | } 59 | 60 | export function isNone(node: Node): boolean { 61 | return node === NONE; 62 | } 63 | 64 | export function editable(group: number, node: Node): Node { 65 | return isNone(node) || node._group === group ? node : cloneNode(group, node); 66 | } 67 | 68 | export function editRightChild(group: number, node: Node): Node { 69 | var child = node._right; 70 | return isNone(child) || child._group === group ? child 71 | : (node._right = (child = cloneNode(group, child)), child); 72 | } 73 | 74 | export function editLeftChild(group: number, node: Node): Node { 75 | var child = node._left; 76 | return isNone(child) || child._group === group ? child 77 | : (node._left = (child = cloneNode(group, child)), child); 78 | } 79 | 80 | export function assignValue(value: V, node: Node): boolean { 81 | const v = node.value; 82 | // Note the double-equals below is used to correctly compare Symbol() with Object(Symbol()) 83 | if(v === value || (v !== null && typeof v === 'object' && v == value)) { // tslint:disable-line:triple-equals 84 | return false; 85 | } 86 | node.value = value; 87 | return true; 88 | } 89 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/last.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from '@collectable/core'; 2 | import {RedBlackTree, RedBlackTreeImpl, RedBlackTreeIterator, RedBlackTreeEntry, PathNode, BRANCH, isNone} from '../internals'; 3 | 4 | /** 5 | * Retrieves the last entry in the tree. 6 | * 7 | * @export 8 | * @template K The type of keys in the tree 9 | * @template V The type of values in the tree 10 | * @param {RedBlackTree} tree The input tree 11 | * @returns {([K, V]|undefined)} A key/value tuple for the last entry in the tree, or undefined if the tree was empty 12 | */ 13 | export function last(tree: RedBlackTree): RedBlackTreeEntry|undefined; 14 | export function last(tree: RedBlackTreeImpl): RedBlackTreeEntry|undefined { 15 | if(tree._size === 0) return void 0; 16 | var node = tree._root; 17 | while(!isNone(node._right)) { 18 | node = node._right; 19 | } 20 | return node; 21 | } 22 | 23 | /** 24 | * Retrieves the last key in the tree. 25 | * 26 | * @export 27 | * @template K The type of keys in the tree 28 | * @template V The type of values in the tree 29 | * @param {RedBlackTree} tree The input tree 30 | * @returns {([K, V]|undefined)} The key of the last entry in the tree, or undefined if the tree was empty 31 | */ 32 | export function lastKey(tree: RedBlackTree): K|undefined { 33 | var node = last(tree); 34 | return isDefined(node) ? node.key : void 0; 35 | } 36 | 37 | /** 38 | * Retrieves the value of the last entry in the tree. 39 | * 40 | * @export 41 | * @template K The type of keys in the tree 42 | * @template V The type of values in the tree 43 | * @param {RedBlackTree} tree The input tree 44 | * @returns {([K, V]|undefined)} The value of the last entry in the tree, or undefined if the tree was empty 45 | */ 46 | export function lastValue(tree: RedBlackTree): V|undefined { 47 | var node = last(tree); 48 | return isDefined(node) ? node.value : void 0; 49 | } 50 | 51 | /** 52 | * Returns an iterator that starts from the last entry in the tree and iterates toward the start of the tree. Emissions 53 | * are references to nodes in the tree, exposed directly to allow Collectable.RedBlackTree to be efficiently consumed as 54 | * a backing structure for other data structures. Do not modify the returned node. 55 | * 56 | * @export 57 | * @template K The type of keys in the tree 58 | * @template V The type of values in the tree 59 | * @param {RedBlackTree} tree The input tree 60 | * @returns {RedBlackTreeIterator} An iterator for entries in the tree 61 | */ 62 | export function iterateFromLast(tree: RedBlackTree): RedBlackTreeIterator; 63 | export function iterateFromLast(tree: RedBlackTreeImpl): RedBlackTreeIterator { 64 | var path: PathNode = PathNode.NONE; 65 | var node = tree._root; 66 | while(!isNone(node)) { 67 | path = PathNode.next(node, path, BRANCH.RIGHT); 68 | node = node._right; 69 | } 70 | return new RedBlackTreeIterator(path, true); 71 | } 72 | -------------------------------------------------------------------------------- /packages/map/tests/set.ts: -------------------------------------------------------------------------------- 1 | import {curry2} from '@typed/curry'; 2 | import {assert} from 'chai'; 3 | import {empty, thaw, isThawed, has, set, unwrap} from '../src'; 4 | 5 | const toJS = curry2(unwrap)(false); 6 | 7 | suite('Map', () => { 8 | suite('set()', () => { 9 | test('returns a new map is the original map is immutable', () => { 10 | var map = set('x', 3, empty()); 11 | var map1 = set('y', 2, map); 12 | 13 | assert.notStrictEqual(map, map1); 14 | }); 15 | 16 | test('returns the same map is the original map is mutable', () => { 17 | var map = thaw(set('x', 3, empty())); 18 | var map1 = set('y', 2, map); 19 | 20 | assert.strictEqual(map, map1); 21 | }); 22 | 23 | test('assigns the specified value to a new map each time it is called on an immutable map', () => { 24 | var map = empty(); 25 | var map1 = set('x', 3, map); 26 | var map2 = set('y', 2, map1); 27 | var map3 = set('x', 1, map2); 28 | 29 | assert.notStrictEqual(map, map1); 30 | assert.notStrictEqual(map, map2); 31 | assert.notStrictEqual(map, map3); 32 | assert.notStrictEqual(map1, map2); 33 | assert.notStrictEqual(map1, map3); 34 | assert.notStrictEqual(map2, map3); 35 | 36 | assert.isFalse(isThawed(map)); 37 | assert.isFalse(isThawed(map1)); 38 | assert.isFalse(isThawed(map2)); 39 | assert.isFalse(isThawed(map3)); 40 | 41 | assert.deepEqual(toJS(map), {}); 42 | assert.deepEqual(toJS(map1), {x: 3}); 43 | assert.deepEqual(toJS(map2), {x: 3, y: 2}); 44 | assert.deepEqual(toJS(map3), {x: 1, y: 2}); 45 | }); 46 | 47 | test('assigns the specified value to the same map each time it is called on a mutable map', () => { 48 | var map = thaw(empty()); 49 | var map1 = set('x', 3, map); 50 | var map2 = set('y', 2, map1); 51 | var map3 = set('x', 1, map2); 52 | 53 | assert.strictEqual(map, map1); 54 | assert.strictEqual(map, map2); 55 | assert.strictEqual(map, map3); 56 | assert.isTrue(isThawed(map)); 57 | assert.deepEqual(toJS(map), {x: 1, y: 2}); 58 | }); 59 | 60 | test('returns the same map if the specified value is unchanged', () => { 61 | var map = set('y', 2, set('x', 3, empty())); 62 | 63 | assert.deepEqual(toJS(map), {x: 3, y: 2}); 64 | 65 | var map1 = set('x', 4, map); 66 | var map2 = set('x', 4, map1); 67 | 68 | assert.notStrictEqual(map, map1); 69 | assert.strictEqual(map1, map2); 70 | }); 71 | 72 | test('removes the specified key if the value is undefined', () => { 73 | var map = set('y', 2, set('x', 3, empty())); 74 | 75 | assert.deepEqual(toJS(map), {x: 3, y: 2}); 76 | 77 | var map1 = set('x', void 0, map); 78 | 79 | assert.notStrictEqual(map, map1); 80 | assert.isFalse(has('x', map1)); 81 | assert.deepEqual(toJS(map), {x: 3, y: 2}); 82 | assert.deepEqual(toJS(map1), {y: 2}); 83 | }); 84 | }); 85 | }); -------------------------------------------------------------------------------- /packages/set/src/functions/stopgaps.ts: -------------------------------------------------------------------------------- 1 | import {isDefined, batch, isMutable, isImmutable} from '@collectable/core'; 2 | import {HashSet, cloneSet, createSet, emptySet} from '../internals'; 3 | 4 | export type UpdateSetCallback = (map: HashSet) => HashSet|void; 5 | 6 | const EMPTY = emptySet(); 7 | 8 | function prep(set: HashSet): HashSet { 9 | return isMutable(set.owner) ? set : cloneSet(set); 10 | } 11 | 12 | export function empty(): HashSet { 13 | return batch.active ? createSet() : EMPTY; 14 | } 15 | 16 | export function fromArray(values: T[]): HashSet { 17 | if(!Array.isArray(values)) { 18 | throw new Error('First argument must be an array of values'); 19 | } 20 | return createSet(values); 21 | } 22 | 23 | export function fromIterable(values: Iterable): HashSet { 24 | return createSet(values); 25 | } 26 | 27 | export function getSize(set: HashSet): number { 28 | return set.values.size; 29 | } 30 | 31 | export function isEmpty(set: HashSet): boolean { 32 | return set.values.size === 0; 33 | } 34 | 35 | export function isFrozen(set: HashSet): boolean { 36 | return isImmutable(set.owner); 37 | } 38 | 39 | export function isThawed(set: HashSet): boolean { 40 | return isMutable(set.owner); 41 | } 42 | 43 | export function updateSet(callback: UpdateSetCallback, set: HashSet): HashSet { 44 | batch.start(); 45 | set = thaw(set); 46 | set = callback(set) || set; 47 | if(batch.end()) { 48 | set.owner = 0; 49 | } 50 | return set; 51 | } 52 | 53 | export function thaw(set: HashSet): HashSet { 54 | return isMutable(set.owner) ? set : cloneSet(set, true); 55 | } 56 | 57 | export function freeze(set: HashSet): HashSet { 58 | return isMutable(set.owner) ? cloneSet(set, false) : set; 59 | } 60 | 61 | export function add(value: T, set: HashSet): HashSet { 62 | if(has(value, set)) return set; 63 | set = prep(set); 64 | if(isDefined(value)) { 65 | set.values.add(value); 66 | } 67 | else { 68 | set.values.delete(value); 69 | } 70 | return set; 71 | } 72 | 73 | export function has(value: T, set: HashSet): boolean { 74 | return set.values.has(value); 75 | } 76 | 77 | export function remove(value: T, set: HashSet): HashSet { 78 | if(!has(value, set)) return set; 79 | set = prep(set); 80 | set.values.delete(value); 81 | return set; 82 | } 83 | 84 | export function values(set: HashSet): IterableIterator { 85 | return set.values.values(); 86 | } 87 | 88 | export function iterate(set: HashSet): IterableIterator { 89 | return set.values[Symbol.iterator](); 90 | } 91 | 92 | export function isEqual(set: HashSet, other: HashSet): boolean { 93 | var it = set.values.values(); 94 | var current: IteratorResult; 95 | while(!(current = it.next()).done) { 96 | if(!other.values.has(current.value)) { 97 | return false; 98 | } 99 | } 100 | return true; 101 | } 102 | -------------------------------------------------------------------------------- /packages/list/tests/functions/update.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, fromArray, appendArray, isFrozen, set, updateList, update, thaw} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | 5 | suite('[List]', () => { 6 | suite('updateList()', () => { 7 | // test('returns the same list if no changes are made', () => { 8 | // const list = empty(); 9 | // const list1 = updateList(list => {}, list); 10 | // assert.strictEqual(list1, list); 11 | // }); 12 | 13 | test('treats the inner list as mutable', () => { 14 | const list = empty(); 15 | const list1 = updateList(list => { 16 | assert.isFalse(isFrozen(list)); 17 | appendArray(['X', 'Y', 'Z'], list); 18 | set(1, 'K', list); 19 | }, list); 20 | assert.isTrue(isFrozen(list1)); 21 | assert.deepEqual(arrayFrom(list1), ['X', 'K', 'Z']); 22 | }); 23 | }); 24 | 25 | suite('update()', () => { 26 | test('returns the same list if no changes are made to the specified value', () => { 27 | const list = fromArray(['X', {foo: 'bar'}, 123]); 28 | assert.strictEqual(update(0, c => 'X', list), list); 29 | assert.strictEqual(update(1, o => o, list), list); 30 | assert.strictEqual(update(2, n => 123, list), list); 31 | }); 32 | 33 | test('returns a new list if the new value is different to the existing value', () => { 34 | const list = fromArray(['X', {foo: 'bar'}, 123]); 35 | const list1 = update(0, c => 'K', list); 36 | const list2 = update(1, o => ({foo: 'baz'}), list); 37 | const list3 = update(2, n => 42, list); 38 | assert.notStrictEqual(list, list1); 39 | assert.notStrictEqual(list, list2); 40 | assert.notStrictEqual(list, list3); 41 | assert.deepEqual(arrayFrom(list1), ['K', {foo: 'bar'}, 123]); 42 | assert.deepEqual(arrayFrom(list2), ['X', {foo: 'baz'}, 123]); 43 | assert.deepEqual(arrayFrom(list3), ['X', {foo: 'bar'}, 42]); 44 | }); 45 | 46 | test('returns the same list, modified with the updated value, if the list was not currently frozen', () => { 47 | const list = thaw(fromArray(['X', {foo: 'bar'}, 123])); 48 | const list1 = update(0, c => 'K', list); 49 | const list2 = update(1, o => ({foo: 'baz'}), list); 50 | const list3 = update(2, n => 42, list); 51 | assert.strictEqual(list, list1); 52 | assert.strictEqual(list, list2); 53 | assert.strictEqual(list, list3); 54 | assert.deepEqual(arrayFrom(list), ['K', {foo: 'baz'}, 42]); 55 | }); 56 | 57 | test('treats a negative index as an offset from the end of the list', () => { 58 | const list = fromArray(['X', {foo: 'bar'}, 123, 'xyz']); 59 | const list1 = update(-2, n => 42, list); 60 | assert.deepEqual(arrayFrom(list1), ['X', {foo: 'bar'}, 42, 'xyz']); 61 | }); 62 | 63 | test('throws an error if the index is out of range', () => { 64 | assert.throws(() => update(0, c => 'X', empty())); 65 | assert.throws(() => update(2, c => 'X', fromArray(['X', 'Y']))); 66 | }); 67 | }); 68 | }); -------------------------------------------------------------------------------- /packages/list/tests/functions/insert.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {fromArray, insert, insertArray} from '../../src'; 3 | import {arrayFrom} from '../../src/internals'; 4 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 5 | 6 | suite('[List]', () => { 7 | suite('#insert()', () => { 8 | test('appends to the list when using index === list.size', () => { 9 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 10 | var list1 = fromArray(values); 11 | var list2 = insert(list1._size, 'J', list1); 12 | assert.deepEqual(arrayFrom(list1), values); 13 | assert.deepEqual(arrayFrom(list2), values.concat(['J'])); 14 | }); 15 | 16 | test('prepends to the list when using index 0', () => { 17 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 18 | var list1 = fromArray(values); 19 | var list2 = insert(0, 'J', list1); 20 | assert.deepEqual(arrayFrom(list2), ['J'].concat(values)); 21 | }); 22 | 23 | test('inserts the arguments in their respective order before the specified index', () => { 24 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 25 | var list1 = fromArray(values); 26 | var index = BRANCH_FACTOR + (BRANCH_FACTOR >>> 1); 27 | var list2 = insert(index, 'J', list1); 28 | assert.deepEqual(arrayFrom(list1), values); 29 | assert.deepEqual(arrayFrom(list2), values.slice(0, index).concat(['J']).concat(values.slice(index))); 30 | }); 31 | }); 32 | 33 | suite('#insertArray()', () => { 34 | test('returns the same list if the value array is empty', () => { 35 | var values = ['A', 'B', 'C', 'X', 'Y', 'Z']; 36 | var list1 = fromArray(values); 37 | var list2 = insertArray(0, [], list1); 38 | assert.strictEqual(list1, list2); 39 | assert.deepEqual(arrayFrom(list2), values); 40 | }); 41 | 42 | test('appends to the list when using index === list.size', () => { 43 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 44 | var list1 = fromArray(values); 45 | var list2 = insertArray(list1._size, ['J', 'K'], list1); 46 | assert.deepEqual(arrayFrom(list1), values); 47 | assert.deepEqual(arrayFrom(list2), values.concat(['J', 'K'])); 48 | }); 49 | 50 | test('prepends to the list when using index 0', () => { 51 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 52 | var list1 = fromArray(values); 53 | var list2 = insertArray(0, ['J', 'K'], list1); 54 | assert.deepEqual(arrayFrom(list2), ['J', 'K'].concat(values)); 55 | }); 56 | 57 | test('inserts the elements of the array in their respective order before the specified index', () => { 58 | var values = makeValues(Math.pow(BRANCH_FACTOR, 2)); 59 | var list1 = fromArray(values); 60 | var index = BRANCH_FACTOR + (BRANCH_FACTOR >>> 1); 61 | var list2 = insertArray(index, ['J', 'K'], list1); 62 | assert.deepEqual(arrayFrom(list1), values); 63 | assert.deepEqual(arrayFrom(list2), values.slice(0, index).concat(['J', 'K']).concat(values.slice(index))); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/mutability.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {freeze, thaw, set, isFrozen, values} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {empty, createTree, sortedValues} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, emptyTree; 7 | 8 | suite('[RedBlackTree]', () => { 9 | setup(() => { 10 | emptyTree = empty(); 11 | tree = createTree(); 12 | }); 13 | 14 | suite('freeze()', () => { 15 | test('should return the same tree if already frozen', () => { 16 | assert.strictEqual(freeze(tree), tree); 17 | }); 18 | 19 | test('should return a new tree if not frozen', () => { 20 | var tree0 = thaw(tree); 21 | assert.notStrictEqual(freeze(tree0), tree0); 22 | }); 23 | 24 | test('should cause common operations to avoid mutating the input tree', () => { 25 | const values1 = sortedValues.concat(9450).sort((a, b) => a - b).map(n => `#${n}`); 26 | const values2 = sortedValues.concat(572).sort((a, b) => a - b).map(n => `#${n}`); 27 | const values3 = sortedValues.concat([9450, 1]).sort((a, b) => a - b).map(n => `#${n}`); 28 | const tree0 = freeze(thaw(tree)); 29 | const tree1 = set(9450, '#9450', tree0); 30 | const tree2 = set(572, '#572', tree0); 31 | const tree3 = set(1, '#1', tree1); 32 | assert.notStrictEqual(tree, tree0); 33 | assert.notStrictEqual(tree1, tree0); 34 | assert.notStrictEqual(tree2, tree0); 35 | assert.notStrictEqual(tree3, tree1); 36 | assert.deepEqual(values(tree0), sortedValues.map(n => `#${n}`)); 37 | assert.deepEqual(values(tree1), values1); 38 | assert.deepEqual(values(tree2), values2); 39 | assert.deepEqual(values(tree3), values3); 40 | }); 41 | }); 42 | 43 | suite('thaw()', () => { 44 | test('should return the same tree if already unfrozen', () => { 45 | const tree0 = thaw(tree); 46 | assert.strictEqual(thaw(tree0), tree0); 47 | }); 48 | 49 | test('should return a new tree if frozen', () => { 50 | assert.notStrictEqual(thaw(tree), tree); 51 | }); 52 | 53 | test('should cause common operations to directly mutate the input tree', () => { 54 | const tree0 = thaw(tree); 55 | const tree1 = set(9450, '#9450', tree0); 56 | const tree2 = set(572, '#572', tree0); 57 | const tree3 = set(1, '#1', tree1); 58 | assert.strictEqual(tree0, tree1); 59 | assert.strictEqual(tree0, tree2); 60 | assert.strictEqual(tree0, tree3); 61 | assert.deepEqual(values(tree0), sortedValues.concat([1, 572, 9450]).sort((a, b) => a - b).map(n => `#${n}`)); 62 | }); 63 | }); 64 | 65 | suite('isFrozen()', () => { 66 | test('should return true if the tree is frozen', () => { 67 | assert.isTrue(isFrozen(tree)); 68 | assert.isTrue(isFrozen(freeze(thaw(tree)))); 69 | }); 70 | 71 | test('should return false if the tree is unfrozen', () => { 72 | const tree0 = thaw(tree); 73 | assert.isFalse(isFrozen(tree0)); 74 | assert.isFalse(isFrozen(thaw(freeze(tree0)))); 75 | }); 76 | }); 77 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/first.ts: -------------------------------------------------------------------------------- 1 | import {isDefined} from '@collectable/core'; 2 | import {RedBlackTree, RedBlackTreeImpl, RedBlackTreeIterator, RedBlackTreeEntry, PathNode, BRANCH, isNone} from '../internals'; 3 | 4 | /** 5 | * Retrieves the first entry in the tree, or undefined if the tree is empty. 6 | * 7 | * @export 8 | * @template K The type of keys in the tree 9 | * @template V The type of values in the tree 10 | * @param {RedBlackTree} tree The input tree 11 | * @returns {(RedBlackTreeEntry|undefined)} The first entry in the tree, or undefined if the tree is empty 12 | */ 13 | export function first(tree: RedBlackTree): RedBlackTreeEntry|undefined; 14 | export function first(tree: RedBlackTreeImpl): RedBlackTreeEntry|undefined { 15 | if(tree._size === 0) return void 0; 16 | var node = tree._root; 17 | while(!isNone(node._left)) { 18 | node = node._left; 19 | } 20 | return node; 21 | } 22 | 23 | /** 24 | * Retrieves the first key in the tree, or undefined if the tree is empty. 25 | * 26 | * @export 27 | * @template K The type of keys in the tree 28 | * @template V The type of values in the tree 29 | * @param {RedBlackTree} tree The input tree 30 | * @returns {(K|undefined)} The first key in the tree, or undefined if the tree is empty 31 | */ 32 | export function firstKey(tree: RedBlackTree): K|undefined { 33 | var node = first(tree); 34 | return isDefined(node) ? node.key : void 0; 35 | } 36 | 37 | /** 38 | * Retrieves the value of the first entry in the tree, or undefined if the tree is empty. 39 | * 40 | * @export 41 | * @template K The type of keys in the tree 42 | * @template V The type of values in the tree 43 | * @param {RedBlackTree} tree The input tree 44 | * @returns {(K|undefined)} The value of the first entry in the tree, or undefined if the tree is empty 45 | */ 46 | export function firstValue(tree: RedBlackTree): V|undefined { 47 | var node = first(tree); 48 | return isDefined(node) ? node.value : void 0; 49 | } 50 | 51 | /** 52 | * Returns an iterator that starts from the first entry in the tree and iterates toward the end of the tree. Emissions 53 | * are references to nodes in the tree, exposed directly to allow Collectable.RedBlackTree to be efficiently consumed as 54 | * a backing structure for other data structures. Do not modify the returned node. 55 | * 56 | * @export 57 | * @template K The type of keys in the tree 58 | * @template V The type of values in the tree 59 | * @param {RedBlackTree} tree The input tree 60 | * @returns {RedBlackTreeIterator} An iterator for entries in the tree 61 | */ 62 | export function iterateFromFirst(tree: RedBlackTree): RedBlackTreeIterator; 63 | export function iterateFromFirst(tree: RedBlackTreeImpl): RedBlackTreeIterator { 64 | var path: PathNode = PathNode.NONE; 65 | var node = tree._root; 66 | while(!isNone(node)) { 67 | path = PathNode.next(node, path, BRANCH.LEFT); 68 | node = node._left; 69 | } 70 | return new RedBlackTreeIterator(path, false); 71 | } 72 | -------------------------------------------------------------------------------- /packages/list/tests/functions/composite.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, append, prepend, get, unwrap} from '../../src'; 3 | import {BRANCH_FACTOR} from '../test-utils'; 4 | 5 | suite('[List: composite operations]', () => { 6 | const m = BRANCH_FACTOR*BRANCH_FACTOR*(BRANCH_FACTOR + 2); 7 | test(`(append(1) + get(n/2)) x ${m >>> 1}`, function() { 8 | this.timeout(30000); // tslint:disable-line 9 | var list = empty(); 10 | var values: number[] = []; 11 | for(var i = 0; i < m; i++) { 12 | list = append(i, list); 13 | values.push(i); 14 | var index = values.length >>> 1; 15 | assert.strictEqual(get(index, list), values[index]); 16 | } 17 | assert.deepEqual(unwrap(false, list), values); 18 | }); 19 | 20 | test(`(append(1) + prepend(1)) x ${m >>> 1}`, function() { 21 | this.timeout(30000); // tslint:disable-line 22 | var list = empty(); 23 | var values: string[] = []; 24 | for(var i = 0; i < m; i++) { 25 | list = append(i.toString(), list); 26 | values.push((i++).toString()) 27 | list = prepend(i.toString(), list); 28 | values.unshift(i.toString()); 29 | } 30 | assert.deepEqual(unwrap(false, list), values); 31 | }); 32 | 33 | test(`(append(1) + prepend(1) + (get(mid) x 2)) x ${m >>> 1}`, function() { 34 | this.timeout(30000); // tslint:disable-line 35 | var list = empty(); 36 | var values: string[] = []; 37 | var offset = BRANCH_FACTOR + (BRANCH_FACTOR >>> 2); 38 | for(var i = 0; i < m; i++) { 39 | var value = `+${i+1}`; 40 | list = append(value, list); 41 | values.push(value); 42 | value = `-${(++i)+1}`; 43 | list = prepend(value, list); 44 | values.unshift(value); 45 | if(offset + 1 < list._size) { 46 | assert.strictEqual(get(offset, list), values[offset], `get(${offset}), size:${list._size}`); 47 | assert.strictEqual(get(-offset, list), values[values.length - offset], `get(${-offset}), size:${list._size}`); 48 | } 49 | } 50 | assert.deepEqual(unwrap(false, list), values); 51 | }); 52 | 53 | test(`(append(1) + prepend(1) + get(mid)) x ${m >>> 1}`, function() { 54 | this.timeout(30000); // tslint:disable-line 55 | var list = empty(); 56 | var values: string[] = []; 57 | var offset = BRANCH_FACTOR + (BRANCH_FACTOR >>> 2); 58 | for(var i = 0; i < m; i++) { 59 | var value = `+${i+1}`; 60 | list = append(value, list); 61 | values.push(value); 62 | var rightExpected = values[values.length - offset]; 63 | var rightActual = get(-offset, list); 64 | value = `-${(++i)+1}`; 65 | list = prepend(value, list); 66 | values.unshift(value); 67 | var leftExpected = values[offset]; 68 | var leftActual = get(offset, list); 69 | if(offset + 1 < list._size) { 70 | assert.strictEqual(rightActual, rightExpected, `get(${offset}), size:${list._size}`); 71 | assert.strictEqual(leftActual, leftExpected, `get(${-offset}), size:${list._size}`); 72 | } 73 | } 74 | assert.deepEqual(unwrap(false, list), values); 75 | }); 76 | }); -------------------------------------------------------------------------------- /packages/list/tests/functions/unwrap.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {empty, mapToArray, fromArray, unwrap, set} from '../../src'; 3 | import {BRANCH_FACTOR, makeValues} from '../test-utils'; 4 | 5 | function setX(values: string[]): string[] { 6 | values = Array.from(values); 7 | values[BRANCH_FACTOR + 1] = 'X'; 8 | return values; 9 | } 10 | 11 | const values1 = ['X', 'Y']; 12 | const values2 = makeValues(BRANCH_FACTOR*4); 13 | const values3 = makeValues(Math.pow(BRANCH_FACTOR, 2) + BRANCH_FACTOR); 14 | const values3x = setX(values3); 15 | const values4 = makeValues(Math.pow(BRANCH_FACTOR, 3) + BRANCH_FACTOR); 16 | const values4x = setX(values4); 17 | 18 | suite('[List]', () => { 19 | suite('mapToArray()', () => { 20 | function transform(value: string): string { 21 | return '#' + value; 22 | } 23 | 24 | test('returns an empty array if the list is empty', () => { 25 | assert.deepEqual(mapToArray(transform, empty()), []); 26 | }); 27 | 28 | test('returns a mapped array of all values in a single-node list', () => { 29 | var list = fromArray(values1); 30 | assert.deepEqual(mapToArray(transform, list), values1.map(transform)); 31 | }); 32 | 33 | test('returns a mapped array of all values in a two-level list', () => { 34 | var values = values2; 35 | assert.deepEqual(mapToArray(transform, fromArray(values)), values.map(transform)); 36 | }); 37 | 38 | test('returns a mapped array of all values in a three-level list', () => { 39 | var list = set(BRANCH_FACTOR + 1, 'X', fromArray(values3)); 40 | assert.deepEqual(mapToArray(transform, list), values3x.map(transform)); 41 | }); 42 | 43 | test('returns a mapped array of all values in a four-level list', () => { 44 | var list = set(BRANCH_FACTOR + 1, 'X', fromArray(values4)); 45 | assert.deepEqual(mapToArray(transform, list), values4x.map(transform)); 46 | }); 47 | }); 48 | 49 | suite('unwrap()', () => { 50 | test('returns an empty array if the list is empty', () => { 51 | assert.deepEqual(unwrap(false, empty()), []); 52 | }); 53 | 54 | test('also unwraps1 embedded collections if deep == true', () => { 55 | var list = fromArray(['X', 'Y', fromArray([fromArray(['A']), 'B']), 'C']); 56 | assert.deepEqual(unwrap(true, list), ['X', 'Y', [['A'], 'B'], 'C']); 57 | }); 58 | 59 | test('returns an array of all values in a single-node list', () => { 60 | var list = fromArray(values1); 61 | assert.deepEqual(unwrap(false, list), values1); 62 | }); 63 | 64 | test('returns an array of all values in a two-level list', () => { 65 | assert.deepEqual(unwrap(false, fromArray(values2)), values2); 66 | }); 67 | 68 | test('returns an array of all values in a three-level list', () => { 69 | var list = set(BRANCH_FACTOR + 1, 'X', fromArray(values3)); 70 | assert.deepEqual(unwrap(false, list), values3x); 71 | }); 72 | 73 | test('returns an array of all values in a four-level list', () => { 74 | var list = set(BRANCH_FACTOR + 1, 'X', fromArray(values4)); 75 | assert.deepEqual(unwrap(false, list), values4x); 76 | }); 77 | }); 78 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/src/internals/red-black-tree.ts: -------------------------------------------------------------------------------- 1 | import {Collection, IndexableCollectionTypeInfo, nextId, batch, isDefined} from '@collectable/core'; 2 | import {Node, RedBlackTreeEntry, NONE} from './node'; 3 | import {unwrap, iterateFromFirst, set, update, get, has, isEqual} from '../functions'; 4 | 5 | export const DEFAULT_COMPARATOR: Comparator = function(a: any, b: any): number { 6 | return a < b ? -1 : a > b ? 1 : 0; 7 | }; 8 | 9 | /** 10 | * A function that compares two keys and returns a value less than 0 if the first is smaller than the second, a value 11 | * greater than 0 if the second is smaller than the first, or 0 if they're equal. 12 | */ 13 | export type Comparator = (a: K, b: K) => number; 14 | 15 | const REDBLACKTREE_TYPE: IndexableCollectionTypeInfo = { 16 | type: Symbol('Collectable.RedBlackTree'), 17 | indexable: true, 18 | 19 | equals(other: RedBlackTree, tree: RedBlackTreeImpl): any { 20 | return isEqual(tree, other); 21 | }, 22 | 23 | unwrap(tree: RedBlackTreeImpl): any { 24 | return unwrap(true, tree); 25 | }, 26 | 27 | get(key: any, tree: RedBlackTreeImpl): any { 28 | return get(key, tree); 29 | }, 30 | 31 | has(key: any, tree: RedBlackTreeImpl): boolean { 32 | return has(key, tree); 33 | }, 34 | 35 | set(key: any, value: any, tree: RedBlackTreeImpl): any { 36 | return set(key, value, tree); 37 | }, 38 | 39 | update(key: any, updater: (value) => any, tree: RedBlackTreeImpl): any { 40 | return update(updater, key, tree); 41 | }, 42 | 43 | verifyKey(key: any, tree: RedBlackTreeImpl): boolean { 44 | return isDefined(key); 45 | } 46 | }; 47 | 48 | export interface RedBlackTree extends Collection> {} 49 | 50 | export class RedBlackTreeImpl implements RedBlackTree { 51 | readonly '@@type' = REDBLACKTREE_TYPE; 52 | 53 | constructor( 54 | public _owner: number, 55 | public _group: number, 56 | public _compare: Comparator, 57 | public _root: Node, 58 | public _size: number 59 | ) {} 60 | 61 | [Symbol.iterator](): IterableIterator> { 62 | return iterateFromFirst(this); 63 | } 64 | } 65 | 66 | export function createTree(mutable: boolean, comparator?: Comparator): RedBlackTree { 67 | return new RedBlackTreeImpl(batch.owner(mutable), nextId(), comparator || DEFAULT_COMPARATOR, NONE, 0); 68 | } 69 | 70 | export function cloneTree(tree: RedBlackTreeImpl, group: number, mutable: boolean): RedBlackTree { 71 | return new RedBlackTreeImpl(batch.owner(mutable), group, tree._compare, tree._root, tree._size); 72 | } 73 | 74 | export function cloneAsMutable(tree: RedBlackTreeImpl): RedBlackTree { 75 | return cloneTree(tree, nextId(), true); 76 | } 77 | 78 | export function cloneAsImmutable(tree: RedBlackTreeImpl): RedBlackTree { 79 | return cloneTree(tree, nextId(), false); 80 | } 81 | 82 | export function doneMutating(tree: RedBlackTreeImpl): RedBlackTree { 83 | if(tree._owner === -1) { 84 | tree._owner = 0; 85 | } 86 | return tree; 87 | } 88 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/from.ts: -------------------------------------------------------------------------------- 1 | import {Associative} from './unwrap'; 2 | import {set} from './set'; 3 | import {freeze} from './freeze'; 4 | import {RedBlackTree, createTree, Comparator, DEFAULT_COMPARATOR} from '../internals'; 5 | 6 | /** 7 | * Creates a new `RedBlackTree` from an array of key/value pairs (tuples). If no comparator function is supplied, keys 8 | * are compared using logical less-than and greater-than operations, which will generally only be suitable for numeric 9 | * or string keys. 10 | * 11 | * @export 12 | * @template K The type of keys in the tree 13 | * @template V The type of values in the tree 14 | * @param {[K, V][]} pairs An array of pairs (tuples), each being a two-element array of [key, value] 15 | * @param {Comparator} [comparator] A comparison function, taking two keys, and returning a value less than 0 if the 16 | * first key is smaller than the second, a value greater than 0 if the first key is 17 | * greater than the second, or 0 if they're the same. 18 | * @returns {RedBlackTree} A tree populated with an entry for each pair in the input array 19 | */ 20 | export function fromPairs(pairs: [K, V][], comparator?: Comparator): RedBlackTree; 21 | export function fromPairs(pairs: Iterable<[K, V]>, comparator?: Comparator): RedBlackTree; 22 | export function fromPairs(pairs: [K, V][]|Iterable<[K, V]>, comparator = DEFAULT_COMPARATOR): RedBlackTree { 23 | const tree = createTree(true, comparator); 24 | var pair: [K, V]; 25 | if(Array.isArray(pairs)) { 26 | for(var i = 0; i < pairs.length; i++) { 27 | pair = pairs[i]; 28 | set(pair[0], pair[1], tree); 29 | } 30 | } 31 | else { 32 | const it = pairs[Symbol.iterator](); 33 | var current: IteratorResult<[K, V]>; 34 | while(!(current = it.next()).done) { 35 | pair = current.value; 36 | set(pair[0], pair[1], tree); 37 | } 38 | } 39 | return freeze(tree); 40 | } 41 | 42 | /** 43 | * Creates a new `RedBlackTree` from a plain input object. If no comparator function is supplied, keys are compared 44 | * using logical less-than and greater-than operations, which will generally only be suitable for numeric or string keys. 45 | * 46 | * @export 47 | * @template V The type of values in the tree 48 | * @param {Associative} obj The input object from which to create a new tree 49 | * @param {Comparator} [comparator] A comparison function, taking two keys, and returning a value less than 0 if the 50 | * first key is smaller than the second, a value greater than 0 if the first key is 51 | * greater than the second, or 0 if they're the same. 52 | * @returns {RedBlackTree} A tree populated with the keys and values of the input object 53 | */ 54 | export function fromObject(obj: Associative, comparator?: Comparator): RedBlackTree { 55 | const tree = createTree(true, comparator); 56 | const keys = Object.keys(obj); 57 | for(var i = 0; i < keys.length; i++) { 58 | var key = keys[i]; 59 | set(key, obj[key], tree); 60 | } 61 | return freeze(tree); 62 | } -------------------------------------------------------------------------------- /packages/list/src/internals/common.ts: -------------------------------------------------------------------------------- 1 | import {Slot} from './slot'; 2 | import {min, max} from '@collectable/core'; 3 | 4 | export const enum CONST { 5 | // Branch factor means the number of slots (branches) that each node can contain (2^5=32). Each level of the tree 6 | // represents a different order of magnitude (base 32) of a given index in the list. The branch factor bit count and 7 | // mask are used to isolate each different order of magnitude (groups of 5 bits in the binary representation of a 8 | // given list index) in order to descend the tree to the leaf node containing the value at the specified index. 9 | BRANCH_INDEX_BITCOUNT = /* ## DEV [[ */ 3 /* ]] ELSE [[5]] ## */, 10 | BRANCH_FACTOR = 1 << BRANCH_INDEX_BITCOUNT, 11 | BRANCH_INDEX_MASK = BRANCH_FACTOR - 1, 12 | MAX_OFFSET_ERROR = (BRANCH_INDEX_BITCOUNT >>> 2) + 1, // `e` in the RRB paper 13 | } 14 | 15 | /** 16 | * An offset value is relative to either the left or the right of the list. Flipping the offset and anchor of an 17 | * intermediate view can allow the referenced node to be size-adjusted without affecting the offset values of other 18 | * views. 19 | * 20 | * @export 21 | * @enum {number} 22 | */ 23 | export const enum OFFSET_ANCHOR { 24 | LEFT = 0, 25 | RIGHT = 1 26 | } 27 | 28 | export const enum COMMIT_MODE { 29 | NO_CHANGE = 0, 30 | RESERVE = 1, 31 | RELEASE = 2, 32 | RELEASE_DISCARD = 3, 33 | } 34 | 35 | /** 36 | * Flips an inward-facing offset value so that it is equal to the distance from the other end of the list to the 37 | * opposite bound of a given slot 38 | * 39 | * @param {number} offset The original internal offset value, relative to one end of the list 40 | * @param {number} slotSize The size of the slot that the offset is relative to 41 | * @param {number} listSize The size of the list 42 | * @returns {number} The inverted offset value 43 | */ 44 | export function invertOffset(offset: number, slotSize: number, listSize: number): number { 45 | return listSize - offset - slotSize; 46 | } 47 | 48 | export function invertAnchor(anchor: OFFSET_ANCHOR): OFFSET_ANCHOR { 49 | return anchor === OFFSET_ANCHOR.RIGHT ? OFFSET_ANCHOR.LEFT : OFFSET_ANCHOR.RIGHT; 50 | } 51 | 52 | export function verifyIndex(size: number, index: number): number { 53 | index = normalizeIndex(size, index); 54 | return index === size ? -1 : index; 55 | } 56 | 57 | export function normalizeIndex(size: number, index: number): number { 58 | return max(-1, min(size, index < 0 ? size + index : index)); 59 | } 60 | 61 | export function shiftDownRoundUp(value: number, shift: number): number { 62 | var a = value >>> shift; 63 | return a + ((a << shift) < value ? 1 : 0); 64 | } 65 | 66 | export function modulo(value: number, shift: number): number { 67 | return value & ((CONST.BRANCH_FACTOR << shift) - 1); 68 | } 69 | 70 | export function concatSlotsToNewArray(left: Slot[], right: Slot[]): Slot[] { 71 | var arr = new Array>(left.length + right.length); 72 | var sum = 0; 73 | for(var i = 0; i < left.length; i++) { 74 | arr[i] = left[i]; 75 | arr[i].sum = (sum += left[i].size); 76 | } 77 | for(var j = 0; j < right.length; i++, j++) { 78 | arr[i] = right[j]; 79 | arr[i].sum = (sum += right[j].size); 80 | } 81 | return arr; 82 | } 83 | -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/app.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import {div, span, ul, li, code} from '@motorcycle/dom'; 3 | import {periodic, just, multicast, scan} from 'most'; 4 | 5 | import {VersionList} from './versions'; 6 | import {Input, getSavedValue, saveValue} from './input'; 7 | import {Navigation} from './navigation'; 8 | import {Renderer} from './renderer'; 9 | 10 | import {start} from '../data'; 11 | 12 | function updateAlignment(alignment) { 13 | return function(vn0, {element: el}) { 14 | switch(alignment) { 15 | case 0: el.scrollLeft = 0; break; 16 | case 1: el.scrollLeft = el.scrollWidth/2 - el.offsetWidth/2; break; 17 | case 2: el.scrollLeft = el.scrollWidth; break; 18 | } 19 | } 20 | } 21 | 22 | function render(state, nav, diagram) { 23 | const postpatch = updateAlignment(state.alignment); 24 | const view = div('.container', {}, [ 25 | nav, 26 | div('.main', {attrs: {'data-alignment': state.alignment}, postpatch}, [ 27 | div('.header', [ 28 | div('.title', `${state.index + 1}. ${state.versions.get(state.index).label}`), 29 | div('.info', [ 30 | ul('.instructions', [ 31 | li([ 32 | span('.ctl', [code('['), ' and ', code(']')]), 33 | span('.text', ' Horizontal scroll') 34 | ]), 35 | li([ 36 | span('.ctl', [code({innerHTML: '←'}), ' and ', code({innerHTML: '→'})]), 37 | span('.text', ' Previous/next version') 38 | ]), 39 | li([ 40 | span('.ctl', [code('PGUP'), ' and ', code('PGDN')]), 41 | span('.text', ' Previous/next "done"') 42 | ]), 43 | li([ 44 | span('.ctl', [code('HOME'), ' and ', code('END')]), 45 | span('.text', ' First/last version') 46 | ]), 47 | li([ 48 | span('.ctl', [code('S')]), 49 | span('.text', ' Save selected index') 50 | ]), 51 | li([ 52 | span('.ctl', [code('X')]), 53 | span('.text', ' Clear console') 54 | ]), 55 | ]) 56 | ]) 57 | ]), 58 | div('.diagram', [diagram]) 59 | ]), 60 | ]); 61 | return view; 62 | } 63 | 64 | function actionFromVersions(versions) { 65 | return state => state.versions = versions; 66 | } 67 | 68 | export function App({dom}) { 69 | const {versions$} = VersionList({dom}); 70 | const {action$} = Input({dom}); 71 | const state$ = action$ 72 | .merge(versions$.map(actionFromVersions)) 73 | .scan((state, fn) => { 74 | fn(state); 75 | if(state.pendingIndex !== -1 && state.versions.size > state.pendingIndex) { 76 | state.index = state.pendingIndex; 77 | state.pendingIndex = -1; 78 | } 79 | return state; 80 | }, { 81 | alignment: getSavedValue('alignment'), 82 | zoom: getSavedValue('zoom'), 83 | index: 0, 84 | pendingIndex: getSavedValue('index', -1), 85 | versions: Immutable.List(), 86 | }) 87 | .skip(1) 88 | .multicast(); 89 | 90 | const kickstart$ = periodic(100).skip(1).take(1).map(start).filter(() => false); 91 | const nav = Navigation({dom, state$}); 92 | const output = Renderer({dom, state$}); 93 | const view$ = state$.merge(kickstart$).combine(render, nav.view$, output.view$); 94 | return {view$}; 95 | } 96 | -------------------------------------------------------------------------------- /packages/red-black-tree/src/functions/update.ts: -------------------------------------------------------------------------------- 1 | import {isImmutable, isDefined, isEqual} from '@collectable/core'; 2 | import {set, remove, isEqual as isTreeEqual} from './index'; 3 | import {RedBlackTree, RedBlackTreeImpl, cloneAsMutable, doneMutating, findNodeByKey} from '../internals'; 4 | 5 | export type UpdateTreeCallback = (tree: RedBlackTree) => RedBlackTree|void; 6 | export type UpdateTreeEntryCallback = (value: V) => V; 7 | 8 | /** 9 | * Passes a mutable instance of a tree to a callback function so that batches of changes can be applied without creating 10 | * additional intermediate copies of the tree, which would waste resources unnecessarily. If the input tree is mutable, 11 | * it is modified and returned as-is, instead of being cloned beforehand. 12 | * 13 | * @export 14 | * @template K The type of keys in the tree 15 | * @template V The type of values in the tree 16 | * @param {UpdateTreeCallback} callback A callback that will be passed a mutable version of the tree 17 | * @param {RedBlackTree} tree The tree to be updated 18 | * @returns {RedBlackTree} An updated version of the tree, with changes applied 19 | */ 20 | export function updateTree(callback: UpdateTreeCallback, tree: RedBlackTree): RedBlackTree; 21 | export function updateTree(callback: UpdateTreeCallback, tree: RedBlackTreeImpl): RedBlackTree { 22 | var newTree: RedBlackTreeImpl; 23 | var immutable = isImmutable(tree._owner) ? (newTree = >cloneAsMutable(tree), true) : (newTree = tree, false); 24 | newTree = >callback(newTree) || newTree; 25 | return immutable && !isTreeEqual(tree, newTree) ? doneMutating(newTree) : tree; 26 | } 27 | 28 | /** 29 | * Locates a value in the tree and passes it to a callback function that should return an updated value. If the value 30 | * returned is equal to the old value, then the original tree is returned, otherwise a modified copy of the original 31 | * tree is returned instead. If the specified key does not exist in the tree, undefined is passed to the callback 32 | * function, and if a defined value is returned, it is inserted into the tree. If the input tree is mutable, it is 33 | * modified and returned as-is, instead of being cloned beforehand. 34 | * 35 | * @export 36 | * @template K The type of keys in the tree 37 | * @template V The type of values in the tree 38 | * @param {(UpdateTreeEntryCallback)} callback A callback that will be passed 39 | * @param {K} key The key of the entry to be updated or inserted 40 | * @param {RedBlackTree} tree The tree to be updated 41 | * @returns {RedBlackTree} An updated copy of the tree, or the same tree if the input tree was already mutable 42 | */ 43 | export function update(callback: UpdateTreeEntryCallback, key: K, tree: RedBlackTree): RedBlackTree; 44 | export function update(callback: UpdateTreeEntryCallback, key: K, tree: RedBlackTreeImpl): RedBlackTree { 45 | var node = findNodeByKey(key, tree); 46 | var oldValue = isDefined(node) ? node.value : void 0; 47 | var newValue = callback(oldValue); 48 | if(isEqual(oldValue, newValue)) return tree; 49 | if(isDefined(newValue)) { 50 | tree = >set(key, newValue, tree); 51 | } 52 | else { 53 | tree = >remove(key, tree); 54 | } 55 | return tree; 56 | } 57 | -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/remove.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {remove, get} from '../../src'; 3 | import {RedBlackTreeImpl, Node, isNone} from '../../src/internals'; 4 | import {createTree, getKeys, unsortedValues, verifyRedBlackAdjacencyInvariant, verifyBlackHeightInvariant} from '../test-utils'; 5 | 6 | suite('[RedBlackTree]', () => { 7 | suite('remove()', () => { 8 | test('should return the same tree if the key was not found', () => { 9 | var tree = createTree(); 10 | var tree2 = remove(-1, tree); 11 | assert.strictEqual(tree, tree2); 12 | }); 13 | 14 | test('should not find the removed key after it is removed', () => { 15 | var tree = createTree(); 16 | console.log(unsortedValues.length); 17 | for(var i = 0; i < unsortedValues.length; i++) { 18 | assert.isDefined(get(unsortedValues[i], tree), `The key "${unsortedValues[i]}" is unexpectedly missing prior to removal`); 19 | tree = >remove(unsortedValues[i], tree); 20 | assert.isUndefined(get(unsortedValues[i], tree), `The key "${unsortedValues[i]}" was not removed`); 21 | } 22 | }); 23 | 24 | test('should reduce the size of the tree by one if the key was found', () => { 25 | var tree = createTree(); 26 | for(var i = 0, count = unsortedValues.length; i < unsortedValues.length; i++) { 27 | tree = >remove(unsortedValues[i], tree); 28 | assert.strictEqual(tree._size, --count, `The tree size was not decremented after the "${unsortedValues[i]}" was removed`); 29 | } 30 | }); 31 | 32 | test('should preserve the order of the rest of the tree after a key is removed', () => { 33 | var tree = createTree(); 34 | for(var i = 0; i < unsortedValues.length; i++) { 35 | tree = >remove(unsortedValues[i], tree); 36 | var values = getKeys(tree._root); 37 | var sorted = values.slice().sort((a, b) => a - b); 38 | assert.deepEqual(values, sorted, `The order of keys in the tree is incorrect after removing key "${unsortedValues[i]}"`); 39 | } 40 | }); 41 | 42 | test('should preserve the red-black adjacency invariant required for red-black trees', () => { 43 | var tree = createTree(); 44 | verifyRedBlackAdjacencyInvariant(tree); 45 | }); 46 | 47 | test('should preserve the black height invariant required for red-black trees', () => { 48 | var tree = createTree(); 49 | verifyBlackHeightInvariant(tree); 50 | }); 51 | 52 | test('should maintain the correct subtree count at each updated node', () => { 53 | var tree = createTree(); 54 | 55 | for(var i = 0; i < unsortedValues.length; i++) { 56 | tree = >remove(unsortedValues[i], tree); 57 | assert.strictEqual(tree._root._count, tree._size, `root count: ${tree._root._count} is incorrect for tree size ${tree}`); 58 | walk(tree._root); 59 | } 60 | 61 | function walk(node: Node): void { 62 | const msg = `node count: ${node._count} is incorrect for node #${node.key} at tree size ${tree} (count left: ${node._left._count}, right: ${node._right._count})`; 63 | const expectedCount = isNone(node) ? 0 : node._left._count + node._right._count + 1; 64 | assert.strictEqual(node._count, expectedCount, msg); 65 | if(!isNone(node._left)) walk(node._left); 66 | if(!isNone(node._right)) walk(node._right); 67 | } 68 | }); 69 | }); 70 | }); -------------------------------------------------------------------------------- /packages/red-black-tree/visualiser/app/core/console.js: -------------------------------------------------------------------------------- 1 | var colors = { 2 | 0: ['white', '#7fbad8', '#0075b2'], 3 | 1: ['white', '#91a0ce', '#24429e'], 4 | 2: ['white', '#ab86e0', '#570ec1'], 5 | 3: ['white', '#c693cb', '#8d2798'], 6 | 4: ['white', '#e17fa2', '#c30045'], 7 | 5: ['white', '#ee8c7f', '#de1900'], 8 | 6: ['white', '#eeb27f', '#de6500'], 9 | 7: ['black', '#6f4900', '#de9200'], 10 | 8: ['black', '#6f5f00', '#debe00'], 11 | 9: ['black', '#6c7200', '#d9e400'], 12 | 10: ['white', '#b8e08d', '#72c11b'], 13 | 11: ['white', '#94d4a9', '#2aaa54'], 14 | 12: ['black', '#797a7a', '#f2f4f4'], 15 | 13: ['black', '#333339', '#676773'], 16 | main: i => colors[Math.abs(i)][2], 17 | inverse: i => colors[Math.abs(i)][0], 18 | mid: i => colors[Math.abs(i)][1], 19 | }; 20 | 21 | function colorText(i, mid = false) { 22 | return {color: mid ? colors.mid(i) : colors.main(i)}; 23 | } 24 | 25 | function colorFill(i, mid = false) { 26 | return {color: mid ? colors.mid(i) : colors.inverse(i), 'background-color': colors.main(i)}; 27 | } 28 | 29 | function colorFillInv(i, mid = false) { 30 | return {color: mid ? colors.mid(i) : colors.main(i), 'background-color': colors.inverse(i)}; 31 | } 32 | 33 | function hashString(str) { 34 | var hash = 5381, i = str.length; 35 | while(i) hash = (hash * 33) ^ str.charCodeAt(--i); 36 | return hash >>> 0; 37 | } 38 | 39 | function safe(id) { 40 | return typeof id === 'symbol' 41 | ? id.toString().substr(7, id.toString().length - 8) 42 | : id; 43 | } 44 | 45 | const colorWheelSize = 12; 46 | function chooseStyle(id, textOnly) { 47 | var number; 48 | if(id === null || id === void 0) number = 0; 49 | else if(typeof id !== 'number') number = hashString(safe(id)); 50 | else number = id; 51 | var index = number % colorWheelSize; 52 | var isMid = number % (colorWheelSize*2) > colorWheelSize; 53 | return textOnly ? colorText(index, isMid) : colorFill(index, isMid); 54 | } 55 | 56 | export function writeLogs(state) { 57 | if(state.logs.length > 0) { 58 | state.logs.forEach(logs => { 59 | if(typeof logs[0] === 'string') { 60 | var match = /^\[([A-Za-z0-9]+)(([#\.])\s*([_A-Za-z0-9]+)?)?\s*(\([^\)]+\))?\]/.exec(logs[0]); 61 | if(match) { 62 | var msg = '%c] '; 63 | var prmLogs = []; 64 | var tail = logs[0].substr(match[0].length); 65 | var prmsRx = /^\s*([a-z0-9]+?): ([a-z0-9_]+)(, )?/i; 66 | var prmsMatch = prmsRx.exec(tail); 67 | if(prmsMatch) { 68 | do { 69 | tail = tail.substr(prmsMatch[0].length); 70 | msg += `%c${prmsMatch[1]}%c: %c${prmsMatch[2]}%c${prmsMatch[3]||' '}`; 71 | prmLogs.push('color: #999', '' , 'color: white', ''); 72 | prmsMatch = prmsRx.exec(tail); 73 | } 74 | while(prmsMatch); 75 | } 76 | logs = [''].concat(prmLogs, logs.slice(1)); 77 | msg += tail.replace(/^\s+/, ''); 78 | if(match[5]) { 79 | msg = ` %c${match[5]}${msg}`; 80 | logs.unshift('color: white'); 81 | } 82 | if(match[4]) { 83 | msg = `%c${match[3]}%c${match[4]}${msg}`; 84 | logs.unshift('color: #999', 'color: #f06'); 85 | } 86 | msg = `[%c${match[1]}${msg}`; 87 | logs.unshift(msg, match[3] ? 'color: orange' : 'color: #f06'); 88 | } 89 | } 90 | console.log.apply(console, logs); 91 | }); 92 | } 93 | console.debug(`# VERSION INDEX ${state.index}${state.label ? `: ${state.label}` : ''}`); 94 | } -------------------------------------------------------------------------------- /packages/red-black-tree/src/internals/iterator.ts: -------------------------------------------------------------------------------- 1 | import {Node, RedBlackTreeEntry, NONE, isNone, BRANCH} from './node'; 2 | import {PathNode} from './path'; 3 | 4 | export class RedBlackTreeIterator implements IterableIterator> { 5 | private _begun = false; 6 | constructor( 7 | private _current: PathNode, 8 | private _reversed = false 9 | ) { 10 | if(_current.isActive()) { 11 | _current.next = _reversed ? BRANCH.RIGHT : BRANCH.LEFT; // indicates the already-traversed branch 12 | } 13 | } 14 | 15 | private _next(reverse: boolean): IteratorResult> { 16 | var current = this._current; 17 | var result = {done: false, value: NONE}; 18 | 19 | if(current.isNone()) { 20 | result.done = true; 21 | return result; 22 | } 23 | 24 | if(!this._begun) { 25 | this._begun = true; 26 | result.value = current.node; 27 | return result; 28 | } 29 | 30 | var done = false, 31 | node: Node, 32 | canEmit = false; 33 | 34 | do { 35 | switch(current.next) { 36 | case BRANCH.NONE: 37 | if(reverse) { 38 | node = current.node._right; 39 | current.next = BRANCH.RIGHT; 40 | } 41 | else { 42 | node = current.node._left; 43 | current.next = BRANCH.LEFT; 44 | } 45 | if(!isNone(node)) { 46 | current = PathNode.next(node, current, BRANCH.NONE /* ## DEV [[ */, current.tree /* ]] ## */); 47 | } 48 | break; 49 | 50 | case BRANCH.LEFT: 51 | if(reverse) { 52 | current = current.release(); 53 | } 54 | else if(canEmit) { 55 | result.value = current.node; 56 | done = true; 57 | } 58 | else { 59 | node = current.node._right; 60 | if(isNone(node)) { 61 | current = current.release(); 62 | } 63 | else { 64 | current.next = BRANCH.RIGHT; 65 | current = PathNode.next(node, current, BRANCH.NONE /* ## DEV [[ */, current.tree /* ]] ## */); 66 | } 67 | } 68 | break; 69 | 70 | case BRANCH.RIGHT: 71 | if(!reverse) { 72 | current = current.release(); 73 | } 74 | else if(canEmit) { 75 | result.value = current.node; 76 | done = true; 77 | } 78 | else { 79 | node = current.node._left; 80 | if(isNone(node)) { 81 | current = current.release(); 82 | } 83 | else { 84 | current.next = BRANCH.LEFT; 85 | current = PathNode.next(node, current, BRANCH.NONE /* ## DEV [[ */, current.tree /* ]] ## */); 86 | } 87 | } 88 | break; 89 | } 90 | 91 | if(current.isNone()) { 92 | result.done = true; 93 | result.value = NONE; 94 | done = true; 95 | } 96 | else { 97 | canEmit = true; 98 | } 99 | } 100 | while(!done); 101 | 102 | this._current = current; 103 | 104 | return result; 105 | } 106 | 107 | next(): IteratorResult> { 108 | return this._next(this._reversed); 109 | } 110 | 111 | previous(): IteratorResult> { 112 | return this._next(!this._reversed); 113 | } 114 | 115 | [Symbol.iterator](): IterableIterator> { 116 | return this; 117 | } 118 | } -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/update.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {size, empty, isFrozen, has, set, updateTree, update, thaw, values, fromPairs} from '../../src'; 3 | import {RedBlackTreeImpl} from '../../src/internals'; 4 | import {createTree, sortedValues, arrayFrom} from '../test-utils'; 5 | 6 | var tree: RedBlackTreeImpl, 7 | emptyTree: RedBlackTreeImpl, 8 | mixedTree: RedBlackTreeImpl; 9 | 10 | suite('[RedBlackTree]', () => { 11 | setup(() => { 12 | emptyTree = >empty(); 13 | tree = createTree(); 14 | mixedTree = >fromPairs([[2, 'X'], [4, {foo: 'bar'}], [6, 123]]); 15 | }); 16 | 17 | suite('updateTree()', () => { 18 | test('returns the same tree if no changes are made', () => { 19 | const tree1 = updateTree(tree => {}, tree); 20 | assert.strictEqual(tree1, tree); 21 | }); 22 | 23 | test('treats the inner tree as mutable', () => { 24 | const tree1 = updateTree(tree => { 25 | assert.isFalse(isFrozen(tree)); 26 | set(1, '#1', tree); 27 | }, tree); 28 | assert.isTrue(isFrozen(tree1)); 29 | assert.deepEqual(values(tree1), [1].concat(sortedValues).map(n => `#${n}`)); 30 | }); 31 | }); 32 | 33 | suite('update()', () => { 34 | test('returns the same tree if no changes are made to the specified value', () => { 35 | assert.strictEqual(update(c => 'X', 2, mixedTree), mixedTree); 36 | assert.strictEqual(update(o => o, 4, mixedTree), mixedTree); 37 | assert.strictEqual(update(n => 123, 6, mixedTree), mixedTree); 38 | }); 39 | 40 | test('if the returned value is not undefined and the key was previously absent from the tree, the new key and value is inserted', () => { 41 | const tree0 = update(v => { 42 | assert.isUndefined(v); 43 | return `#1`; 44 | }, 1, tree); 45 | assert.isTrue(has(1, tree0)); 46 | assert.strictEqual(size(tree0), size(tree) + 1); 47 | }); 48 | 49 | test('if the returned value is undefined and the key was previously in the tree, it is removed from the tree', () => { 50 | const tree0 = update(v => { 51 | assert.isDefined(v); 52 | return void 0; 53 | }, sortedValues[1], tree); 54 | assert.isFalse(has(sortedValues[1], tree0)); 55 | assert.strictEqual(size(tree0), size(tree) - 1); 56 | }); 57 | 58 | test('returns a new tree if the new value is different to the existing value', () => { 59 | const tree1 = update(c => 'K', 2, mixedTree); 60 | const tree2 = update(o => ({foo: 'baz'}), 4, mixedTree); 61 | const tree3 = update(n => 42, 6, mixedTree); 62 | assert.notStrictEqual(mixedTree, tree1); 63 | assert.notStrictEqual(mixedTree, tree2); 64 | assert.notStrictEqual(mixedTree, tree3); 65 | assert.deepEqual(arrayFrom(tree1), [[2, 'K'], [4, {foo: 'bar'}], [6, 123]]); 66 | assert.deepEqual(arrayFrom(tree2), [[2, 'X'], [4, {foo: 'baz'}], [6, 123]]); 67 | assert.deepEqual(arrayFrom(tree3), [[2, 'X'], [4, {foo: 'bar'}], [6, 42]]); 68 | }); 69 | 70 | test('returns the same tree, modified with the updated value, if the tree was not currently frozen', () => { 71 | const tree0 = thaw(mixedTree); 72 | const tree1 = update(c => 'K', 2, tree0); 73 | const tree2 = update(o => ({foo: 'baz'}), 4, tree0); 74 | const tree3 = update(n => 42, 6, tree0); 75 | assert.strictEqual(tree0, tree1); 76 | assert.strictEqual(tree0, tree2); 77 | assert.strictEqual(tree0, tree3); 78 | assert.deepEqual(arrayFrom(tree0), [[2, 'K'], [4, {foo: 'baz'}], [6, 42]]); 79 | }); 80 | }); 81 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require(`gulp`); 2 | const tslint = require(`tslint`); 3 | const gtslint = require(`gulp-tslint`); 4 | const ts = require(`gulp-typescript`); 5 | const mocha = require(`gulp-mocha`); 6 | const sourcemaps = require(`gulp-sourcemaps`); 7 | const plumber = require(`gulp-plumber`); 8 | const transform = require(`gulp-transform`); 9 | const typedoc = require(`gulp-typedoc`); 10 | const rimraf = require(`rimraf`); 11 | const merge = require(`merge2`); 12 | const argv = require(`yargs`).argv; 13 | const {preprocess} = require(`tiny-preprocessor`); 14 | 15 | const path = typeof argv.pkg === `string` ? `./packages/${argv.pkg}` : `.`; 16 | const tsproj = { 17 | commonjs: ts.createProject(`./tsconfig.json`, {declaration: true}), 18 | module: ts.createProject(`./tsconfig.json`, {declaration: true, module: `es2015`}), 19 | tests: ts.createProject(`./tsconfig.json`, {rootDir: `${path}/`, noUnusedLocals: false}) 20 | }; 21 | 22 | function preprocessBuffer(buffer) { 23 | return preprocess(buffer.toString()); 24 | } 25 | 26 | function replace(a, b) { 27 | return function(buffer) { 28 | const src = buffer.toString(); 29 | return src.replace(a, b); 30 | } 31 | } 32 | 33 | function compile() { 34 | const src = gulp.src(`${path}/.build/ts/**/*.ts`) 35 | .pipe(plumber()) 36 | .pipe(sourcemaps.init()); 37 | const ts_commonjs = src.pipe(tsproj.commonjs()); 38 | const ts_module = src.pipe(tsproj.module()); 39 | return merge([ 40 | ts_commonjs.js 41 | .pipe(sourcemaps.write(`./`)) 42 | .pipe(gulp.dest(`${path}/lib/commonjs`)), 43 | 44 | ts_module.js 45 | .pipe(sourcemaps.write(`./`)) 46 | .pipe(gulp.dest(`${path}/lib/module`)), 47 | 48 | ts_module.dts 49 | .pipe(gulp.dest(`${path}/lib/typings`)), 50 | 51 | gulp.src(`${path}/.build/tests.ts/**/*.ts`) 52 | .pipe(plumber()) 53 | .pipe(sourcemaps.init()) 54 | .pipe(tsproj.tests()).js 55 | .pipe(transform(replace(/\.\.\/ts/g, `../../lib/commonjs`))) 56 | .pipe(sourcemaps.write(`./`)) 57 | .pipe(gulp.dest(`${path}/.build/tests`)) 58 | ]); 59 | } 60 | 61 | function runPreprocessor() { 62 | return merge([ 63 | gulp.src(`${path}/src/**/*.ts`) 64 | .pipe(plumber()) 65 | .pipe(transform(preprocessBuffer)) 66 | .pipe(gulp.dest(`${path}/.build/ts`)), 67 | 68 | gulp.src(`${path}/tests/**/*.ts`) 69 | .pipe(plumber()) 70 | .pipe(transform(preprocessBuffer)) 71 | .pipe(transform(replace(/\.\.\/src/g, `../ts`))) 72 | .pipe(gulp.dest(`${path}/.build/tests.ts`)), 73 | ]); 74 | } 75 | 76 | function lint() { 77 | return gulp.src(`${path}/.build/ts/**/*.ts`) 78 | .pipe(plumber()) 79 | .pipe(gtslint({formatter: "verbose"})) 80 | .pipe(gtslint.report()); 81 | } 82 | 83 | function runTests() { 84 | require(`source-map-support`).install(); 85 | return gulp 86 | .src([`${path}/.build/tests/**/*.js`], {read: false}) 87 | .pipe(plumber()) 88 | .pipe(mocha({timeout: 10000, ui: `tdd`})); 89 | } 90 | 91 | function clean() { 92 | const paths = [`${path}/lib`, `${path}/.build`]; 93 | return function(cb) { 94 | const next = (i = 0) => rimraf(paths[i], done(i + 1)); 95 | const done = i => err => err ? cb(err) : i === paths.length ? cb() : next(i); 96 | return next(); 97 | } 98 | } 99 | 100 | gulp.task(`clean`, clean()); 101 | gulp.task(`preprocess`, runPreprocessor); 102 | gulp.task(`compile`, [`preprocess`], compile); 103 | gulp.task(`watch`, () => gulp.watch([`${path}/src/**/*.ts`, `${path}/tests/**/*.ts`], [`build`])); 104 | gulp.task(`test`, [`compile`, `lint`], runTests); 105 | gulp.task(`lint`, [`preprocess`, `compile`], lint); 106 | gulp.task(`build`, [`preprocess`, `compile`, `test`, `lint`]); 107 | gulp.task(`dev`, [`build`, `watch`]); 108 | gulp.task(`default`, [`build`]); -------------------------------------------------------------------------------- /packages/red-black-tree/src/curried.ts: -------------------------------------------------------------------------------- 1 | import {curry2, curry3, curry4} from '@typed/curry'; 2 | import {KeyedMappingFunction} from '@collectable/core'; 3 | import {RedBlackTree, RedBlackTreeEntry, RedBlackTreeIterator} from './internals'; 4 | import { 5 | UpdateTreeCallback, UpdateTreeEntryCallback, FindOp, Associative, 6 | remove as _remove, 7 | set as _set, 8 | updateTree as _updateTree, 9 | update as _update, 10 | get as _get, 11 | has as _has, 12 | iterateFromKey as _iterateFromKey, 13 | at as _at, 14 | keyAt as _keyAt, 15 | valueAt as _valueAt, 16 | indexOf as _indexOf, 17 | iterateFromIndex as _iterateFromIndex, 18 | isEqual as _isEqual, 19 | find as _find, 20 | findKey as _findKey, 21 | findValue as _findValue, 22 | iterateFrom as _iterateFrom, 23 | arrayFrom as _arrayFrom, 24 | unwrap as _unwrap, 25 | } from './functions'; 26 | 27 | export interface RemoveFn { (key: K, tree: RedBlackTree): RedBlackTree; } 28 | export const remove: RemoveFn = curry2(_remove); 29 | export interface SetFn { (key: K, value: V, tree: RedBlackTree): RedBlackTree; } 30 | export const set: SetFn = curry3(_set); 31 | export interface UpdateTreeFn { (callback: UpdateTreeCallback, tree: RedBlackTree): RedBlackTree; } 32 | export const updateTree: UpdateTreeFn = curry2(_updateTree); 33 | export interface UpdateFn { (callback: UpdateTreeEntryCallback, key: K, tree: RedBlackTree): RedBlackTree; } 34 | export const update: UpdateFn = curry3(_update); 35 | export interface GetFn { (key: K, tree: RedBlackTree): V|undefined; } 36 | export const get: GetFn = curry2(_get); 37 | export interface HasFn { (key: K, tree: RedBlackTree): boolean; } 38 | export const has: HasFn = curry2(_has); 39 | export interface IterateFromKeyFn { (reverse: boolean, key: K, tree: RedBlackTree): RedBlackTreeIterator; } 40 | export const iterateFromKey: IterateFromKeyFn = curry3(_iterateFromKey); 41 | export interface AtFn { (index: number, tree: RedBlackTree): RedBlackTreeEntry|undefined; } 42 | export const at: AtFn = curry2(_at); 43 | export interface KeyAtFn { (index: number, tree: RedBlackTree): K|undefined; } 44 | export const keyAt: KeyAtFn = curry2(_keyAt); 45 | export interface ValueAtFn { (index: number, tree: RedBlackTree): V|undefined; } 46 | export const valueAt: ValueAtFn = curry2(_valueAt); 47 | export interface IndexOfFn { (key: K, tree: RedBlackTree): number; } 48 | export const indexOf: IndexOfFn = curry2(_indexOf); 49 | export interface IterateFromIndexFn { (reverse: boolean, index: number, tree: RedBlackTree): RedBlackTreeIterator; } 50 | export const iterateFromIndex: IterateFromIndexFn = curry3(_iterateFromIndex); 51 | export interface IsEqualFn { (tree: RedBlackTree, other: RedBlackTree): boolean; } 52 | export const isEqual: IsEqualFn = curry2(_isEqual); 53 | export interface FindFn { (op: FindOp, key: K, tree: RedBlackTree): RedBlackTreeEntry|undefined; } 54 | export const find: FindFn = curry3(_find); 55 | export interface FindKeyFn { (op: FindOp, key: K, tree: RedBlackTree): K|undefined; } 56 | export const findKey: FindKeyFn = curry3(_findKey); 57 | export interface FindValueFn { (op: FindOp, key: K, tree: RedBlackTree): V|undefined; } 58 | export const findValue: FindValueFn = curry3(_findValue); 59 | export interface IterateFromFn { (op: FindOp, reverse: boolean, key: K, tree: RedBlackTree): RedBlackTreeIterator; } 60 | export const iterateFrom: IterateFromFn = curry4(_iterateFrom); 61 | export interface ArrayFromFn { (mapper: KeyedMappingFunction, tree: RedBlackTree): U[]; } 62 | export const arrayFrom: ArrayFromFn = curry2(_arrayFrom); 63 | export interface UnwrapFn { (deep: boolean, tree: RedBlackTree): Associative; } 64 | export const unwrap: UnwrapFn = curry2(_unwrap); -------------------------------------------------------------------------------- /packages/red-black-tree/tests/functions/set.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {set} from '../../src'; 3 | import {RedBlackTreeImpl, Node, isNone} from '../../src/internals'; 4 | import {empty, represent, sortedValues, unsortedValues, getValues, createTree, verifyRedBlackAdjacencyInvariant, verifyBlackHeightInvariant} from '../test-utils'; 5 | 6 | suite('[RedBlackTree]', () => { 7 | suite('set()', () => { 8 | test('should replace the root of an empty list', () => { 9 | const tree = >set(1, 1, empty()); 10 | assert.strictEqual(tree._size, 1); 11 | assert.strictEqual(tree._root.value, 1); 12 | }); 13 | 14 | test('should add the second entry as a child of the root', () => { 15 | const tree0 = >set(1, 1, empty()); 16 | const tree1 = >set(2, 2, tree0); 17 | assert.notStrictEqual(tree0, tree1); 18 | assert.notStrictEqual(tree0._root, tree1._root); 19 | assert.deepEqual(represent(tree0), [['black', 1]]); 20 | assert.deepEqual(represent(tree1), [['black', 1], [['red', 2]]]); 21 | }); 22 | 23 | test('should insert successive values in the correct sort order', () => { 24 | var tree = >createTree(); 25 | var values = getValues(tree._root); 26 | assert.deepEqual(values, sortedValues.map(v => `#${v}`)); 27 | }); 28 | 29 | test('should return the original if a replacement value is unchanged', () => { 30 | var tree = createTree(); 31 | var tree1 = set(sortedValues[2], `#${sortedValues[2]}`, tree); 32 | assert.strictEqual(tree, tree1); 33 | }); 34 | 35 | test('should replace an existing value if it has changed', () => { 36 | var tree = >createTree(); 37 | var expectedValues1 = sortedValues.map(n => `#${n}`); 38 | expectedValues1[0] = '#foo'; 39 | var expectedValues2 = expectedValues1.slice(); 40 | expectedValues2[10] = '#bar'; 41 | var tree1 = >set(sortedValues[0], expectedValues1[0], tree); 42 | var tree2 = >set(sortedValues[10], expectedValues2[10], tree1); 43 | var values = getValues(tree._root); 44 | var values1 = getValues(tree1._root); 45 | var values2 = getValues(tree2._root); 46 | assert.notStrictEqual(tree, tree1); 47 | assert.notStrictEqual(tree, tree2); 48 | assert.notStrictEqual(tree1, tree2); 49 | assert.deepEqual(values, sortedValues.map(v => `#${v}`)); 50 | assert.deepEqual(values1, expectedValues1); 51 | assert.deepEqual(values2, expectedValues2); 52 | }); 53 | 54 | test('should preserve the red-black adjacency invariant required for red-black trees', () => { 55 | var tree = createTree(); 56 | verifyRedBlackAdjacencyInvariant(tree); 57 | }); 58 | 59 | test('should preserve the black height invariant required for red-black trees', () => { 60 | var tree = createTree(); 61 | verifyBlackHeightInvariant(tree); 62 | }); 63 | 64 | test('should maintain the correct subtree count at each updated node', () => { 65 | var tree = >empty(); 66 | 67 | for(var i = 0; i < unsortedValues.length; i++) { 68 | tree = >set(unsortedValues[i], unsortedValues[i], tree); 69 | assert.strictEqual(tree._root._count, tree._size, `root count: ${tree._root._count} is incorrect for tree size ${tree._size}`); 70 | walk(tree._root); 71 | } 72 | 73 | function walk(node: Node): void { 74 | const msg = `node count: ${node._count} is incorrect for node #${node.key} at tree size ${tree._size} (count left: ${node._left._count}, right: ${node._right._count})`; 75 | assert.strictEqual(node._count, node._left._count + node._right._count + 1, msg); 76 | if(!isNone(node._left)) walk(node._left); 77 | if(!isNone(node._right)) walk(node._right); 78 | } 79 | }); 80 | }); 81 | }); -------------------------------------------------------------------------------- /packages/map/src/functions/stopgaps.ts: -------------------------------------------------------------------------------- 1 | import {isDefined, isMutable, batch} from '@collectable/core'; 2 | import {HashMap, cloneMap, createMap} from '../internals'; 3 | 4 | export type UpdateMapCallback = (map: HashMap) => HashMap|void; 5 | export type UpdateEntryCallback = (value: V|undefined) => V|undefined; 6 | 7 | function prep(map: HashMap): HashMap { 8 | return isMutable(map._owner) ? map : cloneMap(map); 9 | } 10 | 11 | var EMPTY: HashMap|undefined; 12 | 13 | export function empty(): HashMap { 14 | return batch.active ? createMap() : isDefined(EMPTY) ? EMPTY : (EMPTY = createMap()); 15 | } 16 | 17 | export function fromPairs(pairs: [K, V][]): HashMap { 18 | return createMap(new Map(pairs)); 19 | } 20 | 21 | export function fromIterable(iterable: Iterable<[K, V]>): HashMap { 22 | return createMap(new Map(iterable)); 23 | } 24 | 25 | export function isEmpty(map: HashMap): boolean { 26 | return map._values.size === 0; 27 | } 28 | 29 | export function isEqual(a: HashMap, b: HashMap): boolean { 30 | if(a === b) return true; 31 | if(getSize(a) !== getSize(b)) return false; 32 | var bvalues = b._values; 33 | var it = a._values.entries(); 34 | for(var current = it.next(); !current.done; current = it.next()) { 35 | var entry = current.value; 36 | if(!bvalues.has(entry[0])) return false; 37 | if(entry[1] !== bvalues.get(entry[0])) return false; 38 | } 39 | return true; 40 | } 41 | 42 | export function getSize(map: HashMap): number { 43 | return map._values.size; 44 | } 45 | 46 | export function isThawed(map: HashMap): boolean { 47 | return isMutable(map._owner); 48 | } 49 | 50 | export function updateMap(callback: UpdateMapCallback, map: HashMap): HashMap { 51 | batch.start(); 52 | map = thaw(map); 53 | map = callback(map) || map; 54 | if(batch.end()) { 55 | map._owner = 0; 56 | } 57 | return map; 58 | } 59 | 60 | export function update(key: K, callback: UpdateEntryCallback, map: HashMap): HashMap { 61 | var oldv = get(key, map); 62 | var newv = callback(oldv); 63 | return newv === oldv ? map 64 | : newv === void 0 ? remove(key, map) 65 | : set(key, newv, map); 66 | } 67 | 68 | export function thaw(map: HashMap): HashMap { 69 | return isMutable(map._owner) ? map : cloneMap(map, true); 70 | } 71 | 72 | export function freeze(map: HashMap): HashMap { 73 | return isMutable(map._owner) ? cloneMap(map, false) : map; 74 | } 75 | 76 | export function get(key: K, map: HashMap): V|undefined { 77 | return map._values.get(key); 78 | } 79 | 80 | export function set(key: K, value: V, map: HashMap): HashMap { 81 | if(get(key, map) === value) return map; 82 | map = prep(map); 83 | if(isDefined(value)) { 84 | map._values.set(key, value); 85 | } 86 | else { 87 | map._values.delete(key); 88 | } 89 | return map; 90 | } 91 | export {set as assoc}; 92 | 93 | export function has(key: K, map: HashMap): boolean { 94 | return map._values.has(key); 95 | } 96 | 97 | export function remove(key: K, map: HashMap): HashMap { 98 | if(!has(key, map)) return map; 99 | map = prep(map); 100 | map._values.delete(key); 101 | return map; 102 | } 103 | 104 | export function keys(map: HashMap): IterableIterator { 105 | return map._values.keys(); 106 | } 107 | 108 | export function values(map: HashMap): IterableIterator { 109 | return map._values.values(); 110 | } 111 | 112 | export function entries(map: HashMap): IterableIterator<[K, V]> { 113 | return map._values.entries(); 114 | } 115 | 116 | export function iterate(map: HashMap): IterableIterator<[K, V]> { 117 | return map._values[Symbol.iterator](); 118 | } 119 | --------------------------------------------------------------------------------