@labshare/data-structures
Advanced tools
Comparing version 2.0.0 to 3.0.0
@@ -0,1 +1,41 @@ | ||
# [3.0.0](https://github.com/LabShare/data-structures.git/compare/v2.0.0...v3.0.0) (2018-10-30) | ||
### Bug Fixes | ||
* assign to variable ([7f4dcdc](https://github.com/LabShare/data-structures.git/commit/7f4dcdc)) | ||
* check by id instead of content ([fa1d5db](https://github.com/LabShare/data-structures.git/commit/fa1d5db)) | ||
* improve error message for duplicated id ([2e8c314](https://github.com/LabShare/data-structures.git/commit/2e8c314)) | ||
* improve get method ([b21826d](https://github.com/LabShare/data-structures.git/commit/b21826d)) | ||
* return false when id is absent ([725c42c](https://github.com/LabShare/data-structures.git/commit/725c42c)) | ||
* return false when id is absent ([4f4397e](https://github.com/LabShare/data-structures.git/commit/4f4397e)) | ||
* update error message ([af3ab10](https://github.com/LabShare/data-structures.git/commit/af3ab10)) | ||
* update error message ([bccaab5](https://github.com/LabShare/data-structures.git/commit/bccaab5)) | ||
* update test ([2f37d8a](https://github.com/LabShare/data-structures.git/commit/2f37d8a)) | ||
* update toposort related methods ([d9276c9](https://github.com/LabShare/data-structures.git/commit/d9276c9)) | ||
* use hash related methods for performance ([a861ccd](https://github.com/LabShare/data-structures.git/commit/a861ccd)) | ||
### Features | ||
* add findHash and findAndDifferenceHash ([4a8a1dc](https://github.com/LabShare/data-structures.git/commit/4a8a1dc)) | ||
* add findOneHash ([f8192c4](https://github.com/LabShare/data-structures.git/commit/f8192c4)) | ||
* add removeHash and removeHashes methods ([e55e055](https://github.com/LabShare/data-structures.git/commit/e55e055)) | ||
* add removeMany and orderTriples methods ([4b63925](https://github.com/LabShare/data-structures.git/commit/4b63925)) | ||
* add, update and expose hash-related methods ([6844c99](https://github.com/LabShare/data-structures.git/commit/6844c99)) | ||
* create StringSet, improve PointerMap ([4c97df1](https://github.com/LabShare/data-structures.git/commit/4c97df1)) | ||
* export UUID ([0045082](https://github.com/LabShare/data-structures.git/commit/0045082)) | ||
* expose getIndex and getMetadata methods ([dccd086](https://github.com/LabShare/data-structures.git/commit/dccd086)) | ||
* implement bulk operations ([7d31561](https://github.com/LabShare/data-structures.git/commit/7d31561)) | ||
* import StringSet, create utils functions ([e1f05d0](https://github.com/LabShare/data-structures.git/commit/e1f05d0)) | ||
* improve MultiMap with bulk methods ([69dbf99](https://github.com/LabShare/data-structures.git/commit/69dbf99)) | ||
* include hash information for iterators ([df7281c](https://github.com/LabShare/data-structures.git/commit/df7281c)) | ||
* update plugins ([3a14747](https://github.com/LabShare/data-structures.git/commit/3a14747)) | ||
### BREAKING CHANGES | ||
* plugins now must handle onAdd and onRemove as | ||
multi-items operations | ||
# [2.0.0](https://github.com/LabShare/data-structures/compare/v1.1.5...v2.0.0) (2018-09-25) | ||
@@ -2,0 +42,0 @@ |
{ | ||
"name": "@labshare/data-structures", | ||
"main": "./src/index.ts", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"description": "", | ||
@@ -6,0 +6,0 @@ "contributors": "https://github.com/LabShare/data-structures/graphs/contributors", |
import {Collection} from './collection' | ||
import { SSet } from '../sset/sset'; | ||
import {StringSet} from '../string-set/string-set'; | ||
@@ -73,2 +74,39 @@ describe('collection', () => { | ||
}) | ||
describe('filter collection hashes', () => { | ||
it('should create new collection from filtered hashes', () => { | ||
let gw = { | ||
name: 'George Washington', | ||
year: 1732 | ||
}; | ||
let ja = { | ||
name: 'John Adams', | ||
year: 1735 | ||
}; | ||
let tj = { | ||
name: 'Thomas Jefferson', | ||
year: 1743 | ||
}; | ||
let c0 = Collection.fromArray([ | ||
gw, | ||
ja, | ||
tj | ||
]); | ||
let c; | ||
expect(() => c = c0.filterHashes(StringSet.fromArray([ | ||
SSet.hashOf(gw), | ||
SSet.hashOf(ja) | ||
]))).not.toThrow(); | ||
}) | ||
it('should copy metadata from collection', () => { | ||
}) | ||
}) | ||
}) | ||
@@ -224,2 +262,3 @@ | ||
name: 'Luna', | ||
id: 'luna', | ||
lastName: 'Lovegood' | ||
@@ -236,2 +275,3 @@ })).not.toThrow(); | ||
name: 'Luna', | ||
id: 'luna', | ||
lastName: 'Lovegood' | ||
@@ -591,4 +631,130 @@ })).not.toThrow() | ||
describe('findHash', () => { | ||
it('should find property hash', () => { | ||
let c = Collection.fromArray([ | ||
{a: true}, | ||
{b: false} | ||
]) | ||
let r; | ||
expect(()=> r = c.findHash({ | ||
a: SSet.hashOf(true) | ||
})).not.toThrow(); | ||
expect(r.toArray()).toContain(jasmine.objectContaining({a: true})) | ||
expect(r.toArray().length).toBe(1); | ||
expect(()=> r = c.findHash({ | ||
a: [SSet.hashOf(true)] | ||
})).not.toThrow(); | ||
expect(r.toArray()).toContain(jasmine.objectContaining({a: true})) | ||
expect(r.toArray().length).toBe(1); | ||
expect(()=> r = c.findHash({ | ||
a: [] | ||
})).not.toThrow(); | ||
expect(r.toArray().length).toBe(0); | ||
expect(()=> r = c.findHash({ | ||
b: [SSet.hashOf(true)] | ||
})).not.toThrow(); | ||
expect(r.toArray().length).toBe(0); | ||
expect(()=> r = c.findHash({ | ||
b: [SSet.hashOf(false)] | ||
})).not.toThrow(); | ||
expect(r.toArray()).toContain(jasmine.objectContaining({b: false})) | ||
expect(r.toArray().length).toBe(1); | ||
c = Collection.fromArray([ | ||
{b: true}, | ||
{b: false} | ||
]) | ||
expect(()=> r = c.findHash({ | ||
b: [SSet.hashOf(false), SSet.hashOf(true)] | ||
})).not.toThrow(); | ||
expect(r.toArray()).toContain(jasmine.objectContaining({b: false})) | ||
expect(r.toArray()).toContain(jasmine.objectContaining({b: true})) | ||
expect(r.toArray().length).toBe(2); | ||
c = Collection.fromArray([ | ||
{y: 45, z: 54, id: 1} | ||
]) | ||
expect(()=> r = c.findHash({ | ||
y: SSet.hashOf(45), | ||
z: SSet.hashOf(54) | ||
})).not.toThrow(); | ||
expect(r.toArray()).toContain({y: 45, z: 54, id: 1}) | ||
expect(r.toArray().length).toBe(1) | ||
c = Collection.fromArray([ | ||
{y: 45, z: 54, id: 1} | ||
]) | ||
expect(()=> r = c.findHash({ | ||
y: SSet.hashOf(78), | ||
z: SSet.hashOf(54) | ||
})).not.toThrow(); | ||
expect(r.toArray().length).toBe(0) | ||
}) | ||
}) | ||
describe('findOneHash', () => { | ||
it('should find property hash', () => { | ||
let c = Collection.fromArray([ | ||
{a: true, id: 3, m: 123, n: 456}, | ||
{b: false, m: 234, n: 789} | ||
]) | ||
let r; | ||
expect(()=> r = c.findOneHash({ | ||
a: SSet.hashOf(true), | ||
m: SSet.hashOf(123) | ||
})).not.toThrow(); | ||
expect(r).toEqual({a: true, id: 3, m: 123, n: 456}) | ||
}) | ||
}) | ||
describe('findOneHashOrigin', () => { | ||
it ('should one out of many query keys', () => { | ||
let a = Collection.fromArray([ | ||
{name: 'Harry', id: 'harry', lastName: 'Potter'}, | ||
{name: 'Lilian', lastName: 'Potter'}, | ||
{name: 'James', lastName: 'Potter'} | ||
]); | ||
let r; | ||
expect(() => r = a.findOneHashOrigin({ | ||
lastName: SSet.hashOf('Potter'), | ||
name: SSet.hashOf('Harry') | ||
})).not.toThrow() | ||
expect(r).toEqual( | ||
SSet.hashOf({name: 'Harry', lastName: 'Potter', id: 'harry'}) | ||
) | ||
}) | ||
}) | ||
describe('removeHash', () => { | ||
it('should remove hash from collection', () => { | ||
let c = Collection.fromArray([ | ||
{name: 'Test', id: 'test'} | ||
]); | ||
expect(c.size()).toEqual(1); | ||
expect(() => c = c.removeHash( | ||
SSet.hashOf({name: 'Test', id: 'test'})) | ||
).not.toThrow(); | ||
expect(c.isEmpty()).toBeTruthy(); | ||
}) | ||
}) | ||
describe('getIndex', () => { | ||
it('should return index', () => { | ||
let c = Collection.fromArray([ | ||
{name: 'Test', id: 'test'} | ||
]); | ||
expect(c.getIndex()).toBeDefined(); | ||
}) | ||
}) | ||
/* TODO: ids must be added to Collection.fromArray([item]) items */ | ||
}) |
@@ -6,5 +6,25 @@ import {SSet} from '../sset/sset'; | ||
import {DefaultIndex} from '../default-index/default-index'; | ||
import {PointerMap} from '../pointer-map/pointer-map'; | ||
import {StringSet} from '../string-set/string-set'; | ||
import {createTriples} from './create-triples'; | ||
const performIndexLookup = (args, props) => { | ||
return props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(); | ||
}; | ||
const orOperator = (propName, values, props) => { | ||
const sets = values.reduce((acc, v) => { | ||
return [ | ||
...acc, | ||
performIndexLookup([propName, v], props), | ||
]; | ||
}, []); | ||
/* Perform union between StringSets */ | ||
const finalStringSet = sets[0] ? | ||
sets[0].union(...sets.splice(1)) : | ||
StringSet.fromEmpty(); | ||
return finalStringSet; | ||
}; | ||
export const indexPlugin = { | ||
@@ -44,42 +64,81 @@ onInit(state) { | ||
return { | ||
props: DefaultIndex.fromTriples( | ||
triples, | ||
), | ||
props: { | ||
index: DefaultIndex.fromTriples( | ||
triples, | ||
), | ||
/* TODO: Use pointerMap with nested pointerMaps for inverse. | ||
There is no need for multiple values in this case */ | ||
inverse: DefaultIndex.fromTriples( | ||
triples.map((triple) => [triple[2], triple[0], triple[1]]), | ||
), | ||
}, | ||
state, | ||
}; | ||
}, | ||
onRemove(item, itemHash, props, state) { | ||
let defaultIndex = props; | ||
onRemove(items, itemHashes, props, state) { | ||
/* TODO: foreach keys */ | ||
defaultIndex = _.reduce(item, (acc, value, key) => { | ||
return acc.remove(key, SSet.hashOf(value), itemHash); | ||
}, defaultIndex); | ||
return defaultIndex; | ||
}, | ||
onBeforeAdd(item, itemHash, props: DefaultIndex, state) { | ||
if (!item.id) { | ||
return { | ||
continue: true, | ||
message: null, | ||
value: { | ||
...item, | ||
id: uuid(), | ||
}, | ||
}; | ||
} | ||
const result = { | ||
index: [], | ||
inverse: [], | ||
}; | ||
items.forEach((item, i) => { | ||
const itemHash = itemHashes[i]; | ||
_.forEach(item, (value, key) => { | ||
const valueHash = props.inverse.get(itemHash, key); | ||
const indexPath = [key, valueHash, itemHash]; | ||
const inversePath = [itemHash, key, valueHash]; | ||
/* Using mutable for performance improvement */ | ||
result.index.push(indexPath); | ||
result.inverse.push(inversePath); | ||
}); | ||
}); | ||
return { | ||
continue: true, | ||
message: null, | ||
value: item, | ||
index: props.index.removeMany(result.index), | ||
inverse: props.inverse.removeMany(result.inverse), | ||
}; | ||
}, | ||
onAdd(item, itemHash, props, state) { | ||
let defaultIndex = props; | ||
onBeforeAdd(items, itemHashes, props: DefaultIndex, state) { | ||
let result; | ||
items.forEach((item) => { | ||
if (!item.id) { | ||
result = { | ||
continue: true, | ||
message: null, | ||
value: { | ||
...item, | ||
id: uuid(), | ||
}, | ||
}; | ||
} else { | ||
result = { | ||
continue: true, | ||
message: null, | ||
value: item, | ||
}; | ||
} | ||
}); | ||
return result; | ||
}, | ||
onAdd(items, itemHashes, props, state) { | ||
/* TODO: foreach keys */ | ||
defaultIndex = _.reduce(item, (acc, value, key) => { | ||
return acc.add(key, SSet.hashOf(value), itemHash); | ||
}, defaultIndex); | ||
return defaultIndex; | ||
let result = props; | ||
items.forEach((item, i) => { | ||
const itemHash = itemHashes[i]; | ||
result = _.reduce(item, (acc, value, key) => { | ||
const valueHash = SSet.hashOf(value); | ||
const newIndex = acc.index.add(key, valueHash, itemHash); | ||
const newInverse = acc.inverse.add(itemHash, key, valueHash); | ||
return { | ||
index: newIndex, | ||
inverse: newInverse, | ||
}; | ||
}, result); | ||
}); | ||
return result; | ||
}, | ||
API(state, props: DefaultIndex) { | ||
API(state, props, set, [collection]) { | ||
return { | ||
@@ -89,3 +148,3 @@ findOne(query) { | ||
/* Run sub-queries for each property key */ | ||
const results: PointerMap[] = _.reduce( | ||
const results: StringSet[] = _.reduce( | ||
query, | ||
@@ -96,5 +155,5 @@ (result, value, propName) => { | ||
...result, | ||
props.has(args[0], args[1]) ? | ||
props.from(args[0], args[1]) : | ||
PointerMap.fromObject({}), | ||
props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(), | ||
]; | ||
@@ -104,7 +163,55 @@ }, | ||
); | ||
let firstKey: string; | ||
if (results.length > 1) { | ||
firstKey = results[0].intersection(...results.slice(1)).first(); | ||
} else { | ||
firstKey = results[0].first(); | ||
} | ||
return state[firstKey]; | ||
}, | ||
findOneHashOrigin(query) { | ||
const results: StringSet[] = _.reduce( | ||
query, | ||
(result, value, propName) => { | ||
const args = [propName, value]; | ||
return [ | ||
...result, | ||
props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(), | ||
]; | ||
}, | ||
[], | ||
); | ||
let firstKey; | ||
if (results.length > 1) { | ||
firstKey = results[0].intersection(...results.slice(1)).first(); | ||
} else { | ||
firstKey = results[0].first(); | ||
} | ||
return firstKey; | ||
}, | ||
findOneHash(query) { | ||
/* TODO: separate props in indexed and not-indexed */ | ||
/* Run sub-queries for each property key */ | ||
const results: StringSet[] = _.reduce( | ||
query, | ||
(result, value, propName) => { | ||
const args = [propName, value]; | ||
return [ | ||
...result, | ||
props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(), | ||
]; | ||
}, | ||
[], | ||
); | ||
let firstKey; | ||
if (results.length > 1) { | ||
firstKey = results[0].keysIntersection(...results.slice(1)).firstKey(); | ||
firstKey = results[0].intersection(...results.slice(1)).first(); | ||
} else { | ||
firstKey = results[0].firstKey(); | ||
firstKey = results[0].first(); | ||
} | ||
@@ -114,6 +221,6 @@ return state[firstKey]; | ||
find(query) { | ||
find(query): Collection { | ||
/* TODO: refactor/simplify with findOne */ | ||
/* Run sub-queries for each property key */ | ||
const results: PointerMap[] = _.reduce( | ||
const results: StringSet[] = _.reduce( | ||
query, | ||
@@ -124,5 +231,5 @@ (r, value, propName) => { | ||
...r, | ||
props.has(args[0], args[1]) ? | ||
props.from(args[0], args[1]) : | ||
PointerMap.fromObject({}), | ||
props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(), | ||
]; | ||
@@ -134,14 +241,51 @@ }, | ||
if (results.length > 1) { | ||
result = results[0].keysIntersection(...results.slice(1)); | ||
result = results[0].intersection(...results.slice(1)); | ||
} else { | ||
result = results[0]; | ||
} | ||
result = result.toPairs().map(([k]) => state[k]); | ||
return Collection.fromArray(result); | ||
return collection.filterHashes(result); | ||
}, | ||
findHash(query): Collection { | ||
/* TODO: refactor/simplify with findOne */ | ||
/* Run sub-queries for each property key */ | ||
const results: StringSet[] = _.reduce( | ||
query, | ||
(r, value, propName) => { | ||
/* arrays work as "OR" clause */ | ||
/* TODO: Add tests */ | ||
if (_.isArray(value)) { | ||
return [ | ||
...r, | ||
orOperator(propName, value, props), | ||
]; | ||
} else { | ||
const args = [propName, value]; | ||
return [ | ||
...r, | ||
props.index.has(args[0], args[1]) ? | ||
props.index.from(args[0], args[1]) : | ||
StringSet.fromEmpty(), | ||
]; | ||
} | ||
}, | ||
[], | ||
); | ||
let result: StringSet; | ||
if (results.length > 1) { | ||
result = results[0].intersection(...results.slice(1)); | ||
} else { | ||
result = results[0]; | ||
} | ||
return collection.filterHashes(result); | ||
}, | ||
getIndex() { | ||
return props; | ||
return props.index; | ||
}, | ||
getMetadata() { | ||
return props.inverse; | ||
}, | ||
}; | ||
@@ -166,10 +310,26 @@ }, | ||
public filterHashes(hashes: StringSet): Collection { | ||
return this.filter((item, hash) => hashes.has(hash)); | ||
} | ||
public findOne(query) { | ||
return this.internal.set.$('indexPlugin').findOne(query); | ||
return this.internal.set.$('indexPlugin', [this]).findOne(query); | ||
} | ||
public findOneHash(query) { | ||
return this.internal.set.$('indexPlugin', [this]).findOneHash(query); | ||
} | ||
public findOneHashOrigin(query) { | ||
return this.internal.set.$('indexPlugin', [this]).findOneHashOrigin(query); | ||
} | ||
public find(query) { | ||
return this.internal.set.$('indexPlugin').find(query); | ||
return this.internal.set.$('indexPlugin', [this]).find(query); | ||
} | ||
public findHash(query) { | ||
return this.internal.set.$('indexPlugin', [this]).findHash(query); | ||
} | ||
public add(item) { | ||
@@ -215,3 +375,2 @@ const newSet = this.internal.set.add(item); | ||
{ | ||
...this.internal, | ||
set: newSet, | ||
@@ -240,2 +399,16 @@ }, | ||
public removeHash(item) { | ||
return this.removeHashes([item]); | ||
} | ||
public removeHashes(item) { | ||
const newSet = this.internal.set.removeHashes(item); | ||
return new Collection( | ||
{ | ||
...this.internal, | ||
set: newSet, | ||
}, | ||
); | ||
} | ||
public getOne() { | ||
@@ -258,8 +431,17 @@ return this.internal.set.getOne(); | ||
public forEach(fn) { | ||
this.internal.set.forEach(fn); | ||
/* For collection, item metadata is included by default */ | ||
this.internal.set.forEach((item, hash) => { | ||
const metadata = this.getMetadata().get(hash); | ||
fn(item, hash, metadata); | ||
}); | ||
} | ||
public filter(fn) { | ||
/* For collection, item metadata is included by default */ | ||
return new Collection({ | ||
set: this.internal.set.filter(fn), | ||
...this.internal, | ||
set: this.internal.set.filter((item, hash) => { | ||
const metadata = this.getMetadata().get(hash); | ||
return fn(item, hash, metadata); | ||
}), | ||
}); | ||
@@ -286,2 +468,10 @@ } | ||
public getIndex() { | ||
return this.internal.set.$('indexPlugin').getIndex(); | ||
} | ||
public getMetadata() { | ||
return this.internal.set.$('indexPlugin').getMetadata(); | ||
} | ||
public changesFrom(c2: Collection) { | ||
@@ -294,9 +484,16 @@ return c2.changesTo(this); | ||
let comparingCollection = c2; | ||
const removeItems = []; | ||
/* Algorithm should track items by 'id' property */ | ||
this.forEach((item) => { | ||
this.forEach((item, hash, metadata) => { | ||
const id = item.id; | ||
const c2Item = comparingCollection.findOne({id}); | ||
/* TODO: Create wrapper for info */ | ||
const c2Item = comparingCollection.findOneHash({ | ||
id: metadata.getOne('id'), | ||
}); | ||
const c2ItemHash = comparingCollection.findOneHashOrigin({ | ||
id: metadata.getOne('id'), | ||
}); | ||
if (c2Item) { | ||
/* TODO: Avoid rehashing to improve performance */ | ||
if (SSet.hashOf(c2Item) !== SSet.hashOf(item)) { | ||
if (c2ItemHash !== hash) { | ||
changesList = changesList.add({ | ||
@@ -309,3 +506,3 @@ after: c2Item, | ||
} | ||
comparingCollection = comparingCollection.remove(c2Item); | ||
removeItems.push(c2ItemHash); | ||
} else { | ||
@@ -319,2 +516,3 @@ changesList = changesList.add({ | ||
}); | ||
comparingCollection = comparingCollection.removeHashes(removeItems); | ||
comparingCollection.forEach((item) => { | ||
@@ -321,0 +519,0 @@ const id = item.id; |
@@ -14,5 +14,6 @@ /* TODO: Maybe add array support? */ | ||
keys.forEach((k) => { | ||
triples = [...triples, [k, SSet.hashOf(item[k]), itemHash] ]; | ||
const valueHash = SSet.hashOf(item[k]); | ||
triples = [...triples, [k, valueHash, itemHash] ]; | ||
}); | ||
return triples; | ||
}; |
@@ -6,2 +6,14 @@ import {MultiMap} from '../multi-map/multi-map'; | ||
type orderedTriplesArray = any[]; | ||
interface IMultiMapRemoveManyQuery { | ||
[s: string]: string[]; | ||
} | ||
interface IDefaultIndexRemoveManyQuery { | ||
[s: string]: IMultiMapRemoveManyQuery; | ||
} | ||
type separatedFirstKeys = (i: string) => [string, IMultiMapRemoveManyQuery]; | ||
/* TODO: Generalize for n dimensions */ | ||
@@ -53,3 +65,5 @@ export class DefaultIndex { | ||
public get(propName, valueHash) { | ||
return this.internal.get(propName).getOne(valueHash); | ||
return (valueHash !== undefined) ? | ||
this.internal.get(propName).getOne(valueHash) : | ||
this.internal.get(propName); | ||
} | ||
@@ -93,19 +107,61 @@ | ||
public remove(k1, k2, k3) { | ||
return this.removeMany([[k1, k2, k3]]); | ||
} | ||
public orderTriples(items: string[][]): IDefaultIndexRemoveManyQuery { | ||
/* Using mutable for performance improvement */ | ||
const acc = {}; | ||
items.forEach((item: string[]) => { | ||
acc[item[0]] = acc[item[0]] || {}; | ||
acc[item[0]][item[1]] = acc[item[0]][item[1]] || []; | ||
acc[item[0]][item[1]].push(item[2]); | ||
}); | ||
return acc; | ||
} | ||
public removeMany(items: string[][]) { | ||
/* Separate subKeys by same first key*/ | ||
const ordered = this.orderTriples(items); | ||
let outerMap = this.internal; | ||
const errorMsg = `Could not remove from DefaultIndex: ` + | ||
`path '${[k1, k2, k3].join(', ')}' does not exist`; | ||
if (!outerMap.has(k1)) { | ||
throw new Error(errorMsg); | ||
} | ||
const innerMap = outerMap.get(k1); | ||
if (!innerMap.has(k2, k3)) { | ||
throw new Error(errorMsg); | ||
} | ||
const newInnerMap = innerMap.remove(k2, k3); | ||
outerMap = outerMap.set(k1, newInnerMap); | ||
if (newInnerMap.size() === 0) { | ||
outerMap = outerMap.remove(k1); | ||
} | ||
const keys = Object.keys(ordered); | ||
const removeKeys = []; | ||
const setKeys = {}; | ||
const errorMsg = (k1, k2, k3) => `Could not remove from DefaultIndex: ` + | ||
`path '${[k1, k2, k3].join(', ')}' does not exist`; | ||
const checkIfValid = ([k1, k2, k3]) => { | ||
if (!outerMap.has(k1)) { | ||
throw new Error(errorMsg(k1, k2, k3)); | ||
} | ||
const innerMap = outerMap.get(k1); | ||
if (!innerMap.has(k2, k3)) { | ||
throw new Error(errorMsg(k1, k2, k3)); | ||
} | ||
}; | ||
items.forEach(checkIfValid); | ||
const separateFirstKeys: separatedFirstKeys = (i) => [i, ordered[i]]; | ||
const orderedArray: orderedTriplesArray = keys.map(separateFirstKeys); | ||
/* Loop through first keys and update them respectively */ | ||
type removeInnerKeysType = (a: [string, IMultiMapRemoveManyQuery]) => void; | ||
const removeInnerKeys: removeInnerKeysType = ([key, inner]) => { | ||
let innerMap: MultiMap = outerMap.get(key); | ||
innerMap = innerMap.removeMany(inner); | ||
if (innerMap.size() === 0) { | ||
removeKeys.push(key); | ||
} else { | ||
setKeys[key] = innerMap; | ||
} | ||
}; | ||
orderedArray.forEach(removeInnerKeys); | ||
outerMap = outerMap.removeMany(removeKeys).setMany(setKeys); | ||
return new DefaultIndex(outerMap); | ||
} | ||
} |
@@ -607,2 +607,12 @@ import {Graph} from './graph' | ||
}) | ||
it('should return false when id is undefined', () => { | ||
let g = Graph.fromObject({ | ||
edges: [], | ||
nodes: [{ | ||
id: 'node0' | ||
}] | ||
}); | ||
expect(g.nodes.hasId()).toEqual(false); | ||
}) | ||
}) | ||
@@ -751,2 +761,108 @@ | ||
describe('edges findAndDifference', () => { | ||
it('should find and remove edges', () => { | ||
let g = Graph.fromObject({ | ||
nodes: [], | ||
edges: [ | ||
{id: 1, from: 0, to: 0, color: 'red'} | ||
] | ||
}); | ||
expect(g.edges.size()).toBe(1) | ||
let r; | ||
expect(() => r = g.edges.findAndDifference({ | ||
color: 'red' | ||
})).not.toThrow(); | ||
expect(r.edges.size()).toBe(0) | ||
}) | ||
}) | ||
describe('edges findOne', () => { | ||
it('should find one edge', () => { | ||
let g = Graph.fromObject({ | ||
nodes: [], | ||
edges: [ | ||
{id: 1, from: 0, to: 0, color: 'red'} | ||
] | ||
}); | ||
expect(g.edges.size()).toBe(1) | ||
let r; | ||
expect(() => r = g.edges.findOne({ | ||
color: 'red' | ||
})).not.toThrow(); | ||
expect(r).toEqual( | ||
{id: 1, from: 0, to: 0, color: 'red'} | ||
) | ||
}) | ||
}) | ||
describe('edges find', () => { | ||
let g; | ||
beforeEach(() => { | ||
g = Graph.fromObject({ | ||
edges: [ | ||
{ | ||
id: 'n1', | ||
from: 0, | ||
to: 0 | ||
}, { | ||
id: 'n2', | ||
name: 'Node 2', | ||
from: 0, | ||
to: 0 | ||
}, { | ||
id: 1, | ||
from: 0, | ||
to: 0 | ||
} | ||
], | ||
nodes: [] | ||
}); | ||
}) | ||
it('should find edge by id', () => { | ||
expect(g.edges.find({ | ||
id: 'n1' | ||
}).toObject().edges).toEqual([{ | ||
id: 'n1', | ||
from:0, | ||
to: 0 | ||
}]); | ||
expect(g.edges.find({ | ||
id: '1' | ||
}).toObject().edges).toEqual([]) | ||
expect(g.edges.find({ | ||
id: 1 | ||
}).toObject().edges).toEqual([{ | ||
id: 1, | ||
from: 0, | ||
to: 0 | ||
}]) | ||
}) | ||
it('should not fail for inexistent key', () => { | ||
let result; | ||
expect(() => result = g.edges.find({ | ||
anyKey: 'anyValue' | ||
})).not.toThrow(); | ||
expect(result.toObject().edges).toEqual([]); | ||
}) | ||
it('should not fail for keys that exist in only one object', () => { | ||
let result; | ||
expect(() => result = g.edges.find({ | ||
name: 'Node 2' | ||
})).not.toThrow(); | ||
expect(result.toObject().edges).toEqual([{ | ||
id: 'n2', | ||
name: 'Node 2', | ||
from: 0, | ||
to: 0 | ||
}]); | ||
}) | ||
}) | ||
describe('nodes filter', () => { | ||
@@ -998,2 +1114,3 @@ it('should filter the nodes', () => { | ||
let toArray = (arr) => arr.map(l => l.toObject().nodes); | ||
expect(toArray(g.nodes.topologicalSort())).toEqual([ | ||
@@ -1193,3 +1310,3 @@ [{id: 1}], | ||
})).toThrowError( | ||
`Could not add Edge to Graph: Edge already exists` | ||
`Could not add Edge to Graph: Edge Id already exists` | ||
) | ||
@@ -1196,0 +1313,0 @@ }) |
@@ -63,3 +63,3 @@ import * as _ from 'lodash'; | ||
const {nodes, edges} = this.internal; | ||
if (nodes.has(item)) { | ||
if (this.nodes.hasId(item.id)) { | ||
throw new Error ( | ||
@@ -179,2 +179,5 @@ `Could not add Node to Graph: Node already exists`, | ||
const {nodes, edges} = this.internal; | ||
if (id === undefined) { | ||
return false; | ||
} | ||
const maybeNode = nodes.findOne({id}); | ||
@@ -248,7 +251,32 @@ return (typeof maybeNode !== 'undefined') ? true : false; | ||
getStartingNodes: (() => { | ||
return this.nodes.filter((i) => { | ||
return this.edges.findOne({ | ||
to: i.id, | ||
const nodesWithoutIncidentEdges = (i, hash, itemMetadata) => { | ||
const nodeIdHash = itemMetadata.getOne('id'); | ||
const itemHash = this.edges.findOneHash({ | ||
to: nodeIdHash, | ||
}) === undefined; | ||
return itemHash; | ||
}; | ||
return this.nodes.filter(nodesWithoutIncidentEdges); | ||
}).bind(this), | ||
removeEdgesFromNodes: ((graph, nodesGraph: Graph): Graph => { | ||
/* Remove levelNodes from graph */ | ||
const graphWithoutLevelNodes = graph.nodes.difference(nodesGraph); | ||
/* Remove edges attached to removed nodes */ | ||
/* TODO: Abstract step into custom method */ | ||
const nodesCollection = nodesGraph.nodes.getAll(); | ||
let graphWithoutEdges = graphWithoutLevelNodes; | ||
const hashes = []; | ||
const addToHashesQueue = (item, hash, itemMetadata) => { | ||
hashes.push(itemMetadata.getOne('id')); | ||
}; | ||
nodesCollection.forEach(addToHashesQueue); | ||
graphWithoutEdges = graphWithoutEdges.edges.findAndDifferenceHash({ | ||
from: hashes, | ||
}); | ||
return graphWithoutEdges; | ||
}).bind(this), | ||
@@ -267,14 +295,3 @@ | ||
const levelNodes = graph.nodes.getStartingNodes(); | ||
/* Remove levelNodes from graph */ | ||
const graphWithoutLevelNodes = graph.nodes.difference(levelNodes); | ||
/* Remove edges attached to removed nodes */ | ||
/* TODO: Abstract step into custom method */ | ||
const nodesCollection = Collection.fromArray(levelNodes.toObject().nodes); | ||
let graphWithoutLevelEdges = graphWithoutLevelNodes; | ||
nodesCollection.forEach((item) => { | ||
graphWithoutLevelEdges = graphWithoutLevelEdges.edges.findAndDifference({ | ||
from: item.id, | ||
}); | ||
}); | ||
const graphWithoutLevelEdges = this.nodes.removeEdgesFromNodes(graph, levelNodes); | ||
return step( | ||
@@ -378,2 +395,10 @@ graphWithoutLevelEdges, | ||
findHash: ((query) => { | ||
const {nodes, edges} = this.internal; | ||
return new Graph({ | ||
edges: edges.findHash(query), | ||
nodes: Collection.fromArray([]), | ||
}); | ||
}).bind(this), | ||
/** Shortcut method for piping Edges.find() => Edges.difference */ | ||
@@ -384,2 +409,10 @@ findAndDifference: ((query) => { | ||
/** Shortcut method for piping Edges.find() => Edges.difference */ | ||
findAndDifferenceHash: ((query) => { | ||
const z = this.edges.findHash(query); | ||
const d = this.edges.difference(z); | ||
return d; | ||
}).bind(this), | ||
/** Returns Edges as Collection Type instead of Graph */ | ||
@@ -433,5 +466,5 @@ getAll : (() => { | ||
const {nodes, edges} = this.internal; | ||
if (edges.has(item)) { | ||
if (this.edges.hasId(item.id)) { | ||
throw new Error ( | ||
`Could not add Edge to Graph: Edge already exists`, | ||
`Could not add Edge to Graph: Edge Id already exists`, | ||
); | ||
@@ -485,2 +518,5 @@ } | ||
const {nodes, edges} = this.internal; | ||
if (id === undefined) { | ||
return false; | ||
} | ||
const maybeNode = edges.findOne({id}); | ||
@@ -512,2 +548,8 @@ return (typeof maybeNode !== 'undefined') ? true : false; | ||
/** Finds one item in Edges that satifies the query provided */ | ||
findOneHash: ((query) => { | ||
const {nodes, edges} = this.internal; | ||
return edges.findOneHash(query); | ||
}).bind(this), | ||
/** Gets the number of Edges in the Graph */ | ||
@@ -514,0 +556,0 @@ size: (() => { |
@@ -0,1 +1,3 @@ | ||
import * as UUID from 'uuid/v4'; | ||
export {SSet} from './sset/sset'; | ||
@@ -7,1 +9,2 @@ export {SMap} from './smap/smap'; | ||
export {Graph} from './graph/graph'; | ||
export {UUID}; |
@@ -210,3 +210,3 @@ import { MultiMap } from "./multi-map"; | ||
expect(() => result = multiMap.getOne('b')).not.toThrow(); | ||
expect(result).toEqual('c'); | ||
expect(result).toEqual('c'); | ||
}) | ||
@@ -213,0 +213,0 @@ |
import { PointerMap } from '../pointer-map/pointer-map'; | ||
import { StringSet } from '../string-set/string-set'; | ||
@@ -15,3 +16,3 @@ interface IStateAndProps { | ||
if (!outerMap.has(k1)) { | ||
outerMap = outerMap.add(k1, PointerMap.fromObject({})); | ||
outerMap = outerMap.add(k1, StringSet.fromEmpty()); | ||
} | ||
@@ -38,3 +39,3 @@ const innerMap = outerMap.get(k1); | ||
* A MultiMap consists of a pointerMap with keys that point to | ||
* other pointerMaps. This allows one-to-many relationships | ||
* StringSets. This allows one-to-many relationships | ||
*/ | ||
@@ -82,3 +83,3 @@ export class MultiMap { | ||
public getOne(k1) { | ||
return this.internal.state.get(k1).firstKey(); | ||
return this.internal.state.get(k1).first(); | ||
} | ||
@@ -96,31 +97,57 @@ | ||
const {state: outerMap, props: {size}} = this.internal; | ||
if (!outerMap.has(k1)) { | ||
throw new Error(`Could not remove from MultiMap: key '${k1}' does not exist`); | ||
} | ||
if (typeof k2 === 'undefined') { | ||
return new MultiMap({ | ||
props: { | ||
size: size - outerMap.get(k1).size(), | ||
}, | ||
state: outerMap.remove(k1), | ||
}); | ||
return this.removeMany({[k1]: true}); | ||
} | ||
const innerMap = outerMap.get(k1); | ||
if (!innerMap.has(k2)) { | ||
throw new Error(`Could not remove from MultiMap: path '${k1}' -> '${k2}' does not exist`); | ||
} | ||
const newInnerMap = outerMap.get(k1).remove(k2); | ||
let newOuterMap; | ||
if (newInnerMap.size() === 0) { | ||
newOuterMap = outerMap.remove(k1); | ||
} else { | ||
newOuterMap = outerMap.set(k1, newInnerMap); | ||
} | ||
return this.removeMany({[k1]: [k2]}); | ||
} | ||
public removeMany(query: {[s: string]: string[] | boolean}): MultiMap { | ||
const outerMap = this.internal.state; | ||
const removeOuterKeys = []; | ||
const setOuterKeys = {}; | ||
let size = this.size(); | ||
const queryKeys = Object.keys(query); | ||
type keyValues = [string, string[] | boolean]; | ||
const toPairs: (k: string) => keyValues = (k) => [k, query[k]]; | ||
const orderedQuery: keyValues[] = queryKeys.map(toPairs); | ||
const removeInnerKeys = ([key, values]: keyValues) => { | ||
if (!outerMap.has(key)) { | ||
throw new Error(`Could not remove from MultiMap: key '${key}' does not exist`); | ||
} | ||
if (typeof values === "boolean") { | ||
size -= outerMap.get(key).size(), | ||
removeOuterKeys.push(key); | ||
} else { | ||
const innerMap = outerMap.get(key); | ||
values.forEach((k2) => { | ||
if (!innerMap.has(k2)) { | ||
throw new Error(`Could not remove from MultiMap: path '${key}' -> '${k2}' does not exist`); | ||
} | ||
}); | ||
size -= values.length; | ||
const newInnerMap = innerMap.removeMany(values); | ||
if (newInnerMap.size() === 0) { | ||
removeOuterKeys.push(key); | ||
} else { | ||
setOuterKeys[key] = newInnerMap; | ||
} | ||
} | ||
}; | ||
orderedQuery.forEach(removeInnerKeys); | ||
const newOuterMap = outerMap.removeMany(removeOuterKeys).setMany(setOuterKeys); | ||
return new MultiMap({ | ||
props: { | ||
size: size - 1, | ||
size, | ||
}, | ||
state: newOuterMap, | ||
}); | ||
} | ||
@@ -127,0 +154,0 @@ |
@@ -224,2 +224,17 @@ import { PointerMap } from "./pointer-map"; | ||
}) | ||
it('should clear values when specified', () => { | ||
let pm = PointerMap.fromObject({ | ||
10: '10', | ||
20: '20', | ||
21: '21', | ||
22: '22' | ||
}) | ||
let result; | ||
expect(() => result = pm.filter(i => i % 10 === 0, true)).not.toThrow() | ||
expect(result.toObject()).toEqual({ | ||
10: true, | ||
20: true | ||
}) | ||
}) | ||
}) | ||
@@ -360,2 +375,82 @@ | ||
describe('keysSet', () => { | ||
it('should return a set of keys', () => { | ||
let pMap = PointerMap.fromObject({}) | ||
let set; | ||
expect(() => set = pMap.keysSet()).not.toThrow(); | ||
expect(set.toArray()).toEqual([]) | ||
pMap = PointerMap.fromObject({ | ||
'a': 'b' | ||
}) | ||
expect(() => set = pMap.keysSet()).not.toThrow(); | ||
expect(set.toArray()).toEqual(['a']) | ||
pMap = PointerMap.fromObject({ | ||
'a': 'b', | ||
'c': 'd' | ||
}) | ||
expect(() => set = pMap.keysSet()).not.toThrow(); | ||
expect(set.toArray()).toEqual(['a', 'c']) | ||
}) | ||
}) | ||
describe('keysUnion', () => { | ||
it('should return keys union', () => { | ||
let p = PointerMap.fromPairs([ | ||
['a', 'b'], | ||
['c', 'd'] | ||
]) | ||
let q = PointerMap.fromPairs([ | ||
['e', 'f'], | ||
['g', 'h'], | ||
['i', 'j'] | ||
]) | ||
let result; | ||
expect(() => result = p.keysUnion(q)).not.toThrow(); | ||
expect(result.toPairs()).toEqual([ | ||
['a', 'b'], | ||
['c', 'd'], | ||
['e', 'f'], | ||
['g', 'h'], | ||
['i', 'j'] | ||
]) | ||
p = PointerMap.fromPairs([ | ||
['a', 'b'], | ||
['c', 'd'] | ||
]) | ||
q = PointerMap.fromPairs([ | ||
['a', 'b'], | ||
['g', 'h'], | ||
['i', 'j'] | ||
]) | ||
expect(() => result = p.keysUnion(q)).not.toThrow(); | ||
expect(result.toPairs()).toEqual([ | ||
['a', 'b'], | ||
['c', 'd'], | ||
['g', 'h'], | ||
['i', 'j'] | ||
]) | ||
}) | ||
}) | ||
describe('setMany', () => { | ||
it('should overwrite key value pair', () => { | ||
let p = PointerMap.fromObject({ | ||
a: '1234' | ||
}); | ||
expect(() => p = p.setMany({ | ||
a: 2345, | ||
b: true | ||
})).not.toThrow(); | ||
expect(p.toPairs()).toEqual([ | ||
['a', 2345], | ||
['b', true] | ||
]) | ||
}) | ||
}) | ||
}) |
import _ = require('lodash'); | ||
import { StringSet } from '../string-set/string-set'; | ||
@@ -6,3 +7,3 @@ interface IHashMadeMap { | ||
} | ||
type keyValuePair = [string, string]; | ||
type keyValuePair = [string, (string| boolean)]; | ||
type pairsArray = keyValuePair[]; | ||
@@ -53,2 +54,6 @@ | ||
public isEmpty() { | ||
return this.size() === 0; | ||
} | ||
public has(key: string, value?: string) { | ||
@@ -74,2 +79,17 @@ const {state} = this.internal; | ||
public keysUnion(...pointerMaps): PointerMap { | ||
const result = pointerMaps.reduce((acc, p) => { | ||
let r = acc; | ||
p.forEach((key, value) => { | ||
/* TODO: Implement merge */ | ||
if (!r.has(key)) { | ||
r = r.add(key, value); | ||
} | ||
}); | ||
return r; | ||
}, this); | ||
return result; | ||
} | ||
public set(key, value) { | ||
@@ -88,14 +108,43 @@ const isOverwrite = this.has(key); | ||
public remove(key) { | ||
if (!this.has(key)) { | ||
throw new Error(`Could not remove from PointerMap: key '${key}' does not exist`); | ||
} | ||
public setMany(items: {[s: string]: any}) { | ||
const newState = { | ||
...this.internal.state, | ||
}; | ||
let size = this.internal.props.size; | ||
_.forEach(items, (value, key) => { | ||
const isOverwrite = this.has(key); | ||
size += (isOverwrite ? 0 : 1); | ||
newState[key] = value; | ||
}); | ||
return new PointerMap({ | ||
props: { | ||
size: this.internal.props.size - 1, | ||
size, | ||
}, | ||
state: _.omit(this.internal.state, [key]), | ||
state: newState, | ||
}); | ||
} | ||
public remove(key) { | ||
return this.removeMany([key]); | ||
} | ||
public removeMany(keys: string[]) { | ||
const newState = Object.assign({}, this.internal.state); | ||
keys.forEach((key) => { | ||
if (!this.has(key)) { | ||
throw new Error(`Could not remove from PointerMap: key '${key}' does not exist`); | ||
} | ||
delete newState[key]; | ||
}); | ||
return new PointerMap({ | ||
props: { | ||
size: this.internal.props.size - keys.length, | ||
}, | ||
state: newState, | ||
}); | ||
} | ||
public get(key) { | ||
@@ -112,3 +161,3 @@ if (!this.has(key)) { | ||
public keysIntersection(...pointerMaps) { | ||
public keysIntersection(...pointerMaps: PointerMap[]): StringSet { | ||
pointerMaps = [this, ...pointerMaps]; | ||
@@ -121,10 +170,10 @@ const smallest = pointerMaps.reduce((acc, p) => { | ||
}, { | ||
item: -1, | ||
item: null, | ||
size: Infinity, | ||
}); | ||
return smallest.item.filter((i) => { | ||
return smallest.item.reduce((acc, value, i) => { | ||
/* check if every pointerMaps has key */ | ||
return PointerMap.keyInEvery(i, pointerMaps); | ||
}, true); | ||
return PointerMap.keyInEvery(i, pointerMaps) ? acc.add(i) : acc; | ||
}, StringSet.fromEmpty()); | ||
@@ -136,3 +185,3 @@ } | ||
/* TODO: Memoize object keys */ | ||
Object.keys(this.internal.state).forEach((k) => { | ||
this.forEach((k) => { | ||
if (fn(k) === true) { | ||
@@ -145,3 +194,7 @@ result = result.add(k, clearValues ? true : this.internal.state[k]); | ||
public firstKey() { | ||
public forEach(fn) { | ||
Object.keys(this.internal.state).forEach((key) => fn(key, this.internal.state[key])); | ||
} | ||
public firstKey(): string { | ||
const {state} = this.internal; | ||
@@ -164,2 +217,12 @@ let result; | ||
public keysArray(): string[] { | ||
const {state, items} = this.internal; | ||
const keys = Object.keys(state); | ||
return keys; | ||
} | ||
public keysSet(): StringSet { | ||
return StringSet.fromArray(this.keysArray()); | ||
} | ||
public reduce(fn: (acc, value, key) => any, acc: any) { | ||
@@ -166,0 +229,0 @@ const {state} = this.internal; |
@@ -55,3 +55,3 @@ import { SSet } from "./sset"; | ||
onDestroy?: () => void, | ||
API: (state, props) => any | ||
API: (state, props, set: SSet, args: any[]) => any | ||
} | ||
@@ -58,0 +58,0 @@ |
@@ -99,2 +99,3 @@ import {SSet} from './sset'; | ||
let sset = SSet.fromArray(arr); | ||
const sset2 = sset.map(item => item * 3); | ||
@@ -175,2 +176,26 @@ expect(sset.size()).toBe(4); | ||
}) | ||
it('should return same objects for two iterators', () => { | ||
let sset = SSet.fromArray([{a: true}, {b: true}, {c: true}, {d: true}]); | ||
let jsSet = new Set(); | ||
for (let item of sset) { | ||
jsSet.add(item) | ||
} | ||
for (let item of sset) { | ||
expect(jsSet.has(item)).toBeTruthy(); | ||
} | ||
expect(jsSet.size).toEqual(4); | ||
let sset2 = sset.add({y: 'z'}); | ||
jsSet = new Set(); | ||
for (let item of sset2) { | ||
jsSet.add(item) | ||
} | ||
for (let item of sset) { | ||
expect(jsSet.has(item)).toBeTruthy(); | ||
jsSet.add(item) | ||
} | ||
expect(jsSet.size).toEqual(5); | ||
}) | ||
}) | ||
@@ -1113,2 +1138,11 @@ | ||
}) | ||
describe('removeHashes', () => { | ||
it('should throw error if inexistent hash', () => { | ||
let sset = SSet.fromArray([1, 2, 3]); | ||
expect(() => sset.removeHashes(['inexistentHash'])).toThrowError( | ||
`Could not remove hash from SSet: hash 'inexistentHash' does not exist in the set` | ||
); | ||
}) | ||
}) | ||
}) |
@@ -22,3 +22,3 @@ import _ = require('lodash'); | ||
/* Remove any unsupported values for JSON */ | ||
array = JSON.parse(JSON.stringify(array)); | ||
JSON.parse(JSON.stringify(array)); | ||
@@ -102,20 +102,47 @@ const internalState = { | ||
type argsType = (a: SSet, b: any, c: boolean) => SSet; | ||
const lsDifference: [SSet, argsType] = | ||
[ largest, | ||
(acc: SSet, item: any, largestHasIt: boolean): SSet => largestHasIt ? | ||
acc.remove(item) : acc, | ||
type argsType = (a: {set: SSet, hashes?: string[]}, b: any, c: boolean, d: any, e: boolean) => {set: SSet}; | ||
const lsDifference: [{set: SSet, hashes: string[]}, argsType] = | ||
[ {set: largest, hashes: []}, | ||
(acc: {set: SSet, hashes?: string[]}, item: any, largestHasIt: boolean, itemHash, isLast: boolean) => { | ||
/* Using Mutable for performance gains */ | ||
if (largestHasIt) { | ||
acc.hashes.push(itemHash); | ||
} | ||
if (isLast === true) { | ||
return { | ||
set: acc.set.removeHashes(acc.hashes), | ||
}; | ||
} | ||
return acc; | ||
}, | ||
]; | ||
const slDifference: [SSet, argsType] = | ||
[ smallest, | ||
(acc: SSet, item: any, largestHasIt: boolean): SSet => largestHasIt ? | ||
acc.remove(item) : acc, | ||
const slDifference: [{set: SSet, hashes: string[]}, argsType] = | ||
[ {set: smallest, hashes: []}, | ||
(acc: {set: SSet, hashes?: string[]}, item: any, largestHasIt: boolean, itemHash, isLast: boolean) => { | ||
/* Using Mutable for performance gains */ | ||
if (largestHasIt) { | ||
acc.hashes.push(itemHash); | ||
} | ||
if (isLast === true) { | ||
return { | ||
set: acc.set.removeHashes(acc.hashes), | ||
}; | ||
} | ||
return acc; | ||
}, | ||
]; | ||
type reducer = (acc: SSet, item: any, c: boolean) => SSet; | ||
const unionReducer: opReducerFn = (acc: IOpReducerFnAcc, item, largestHasIt, itemHash) => ({ | ||
set: acc.set.mergeHash(itemHash, item), | ||
}); | ||
const reducers: { | ||
union: [SSet, reducer], | ||
difference: [SSet, reducer], | ||
oppositeDifference: [SSet, reducer], | ||
intersection: [SSet, reducer], | ||
union: [any, opReducerFn], | ||
difference: [any, opReducerFn], | ||
oppositeDifference: [any, opReducerFn], | ||
intersection: [any, opReducerFn], | ||
} = { | ||
@@ -127,5 +154,7 @@ /* Difference and oppositeDifference are being calculated respectively as | ||
difference: (set1 === largest) ? lsDifference : slDifference, | ||
intersection: [smallest, (acc, item, largestHasIt): SSet => largestHasIt ? acc : acc.remove(item)], | ||
intersection: [{set: smallest}, (acc, item, largestHasIt, itemHash) => ({ | ||
set: largestHasIt ? acc.set : acc.set.removeHash(itemHash), | ||
})], | ||
oppositeDifference: set1 === largest ? slDifference : lsDifference, | ||
union: [largest, (acc, item, largestHasIt): SSet => acc.merge(item)], | ||
union: [{set: largest}, unionReducer], | ||
}; | ||
@@ -137,5 +166,13 @@ | ||
type setsOps = 'union' | 'difference' | 'oppositeDifference' | 'intersection'; | ||
interface IOpReducerFnAcc {set: SSet; hashes?: string[]; } | ||
type opReducerFn = ( | ||
acc: IOpReducerFnAcc, | ||
item: any, | ||
largestHasIt: boolean, | ||
itemHash: any, | ||
isLast: boolean, | ||
) => SSet | IOpReducerFnAcc; | ||
type opReducer = [ | ||
SSet, | ||
(acc: SSet, item: any, largestHasIt: boolean) => SSet | ||
any, | ||
opReducerFn | ||
]; | ||
@@ -178,5 +215,5 @@ type operationArrayItem = [string, SSet, (acc: SSet, item: any, largestHasIt: boolean) => SSet]; | ||
public add(value: JSONCapable): SSet { | ||
/* Remove unsupported values for JSON */ | ||
value = JSON.parse(JSON.stringify(value)); | ||
const hash = SSet.hashOf(value); | ||
value = updatePlugins('onBeforeAdd', [value], [hash], this.statePropsPlugins).value; | ||
/* Check whether value is already contained in the set */ | ||
@@ -201,10 +238,14 @@ const hashedValue = SSet.hashOf(value); | ||
} | ||
/* Remove any unsupported values for JSON */ | ||
value = JSON.parse(JSON.stringify(value)); | ||
const hash = SSet.hashOf(value); | ||
return this.mergeHash(hash, value); | ||
} | ||
public mergeHash(hash, value) { | ||
const isNewItem = !(hash in this.statePropsPlugins.state); | ||
/* trigger onBeforeAdd if defined */ | ||
value = updatePlugins('onBeforeAdd', value, hash, this.statePropsPlugins).value; | ||
value = updatePlugins('onBeforeAdd', [value], [hash], this.statePropsPlugins).value; | ||
@@ -227,3 +268,3 @@ let newInternalState: SSetStatePropsPlugins = { | ||
if (isNewItem) { | ||
newInternalState = updatePlugins('onAdd', value, hash, newInternalState); | ||
newInternalState = updatePlugins('onAdd', [value], [hash], newInternalState); | ||
} | ||
@@ -249,4 +290,2 @@ | ||
public remove(value: JSONCapable): SSet { | ||
/* Remove any unsupported values for JSON */ | ||
value = JSON.parse(JSON.stringify(value)); | ||
@@ -268,3 +307,3 @@ const hashedValue = SSet.hashOf(value); | ||
/* trigger onRemove */ | ||
newInternalState = updatePlugins('onRemove', value, hashedValue, newInternalState); | ||
newInternalState = updatePlugins('onRemove', [value], [hashedValue], newInternalState); | ||
@@ -274,2 +313,38 @@ return new SSet(newInternalState); | ||
/** Remove item given its hash */ | ||
public removeHash(hashedValue: any): SSet { | ||
return this.removeHashes([hashedValue]); | ||
} | ||
public removeHashes(hashedValues: any[]): SSet { | ||
if (hashedValues.length === 0) { | ||
return this; | ||
} | ||
hashedValues.forEach((hashedValue) => { | ||
if (!(hashedValue in this.statePropsPlugins.state)) { | ||
throw new Error(`Could not remove hash from SSet: hash '${hashedValue}' does not exist in the set`); | ||
} | ||
}); | ||
let newInternalState: SSetStatePropsPlugins = { | ||
plugins: this.statePropsPlugins.plugins, | ||
props: { | ||
...this.statePropsPlugins.props, | ||
size: this.statePropsPlugins.props.size - hashedValues.length, | ||
}, | ||
state: _.omit(this.statePropsPlugins.state, hashedValues), | ||
}; | ||
/* trigger onRemove */ | ||
newInternalState = updatePlugins('onRemove', | ||
hashedValues.map((h) => this.statePropsPlugins.state[h]), | ||
hashedValues, | ||
newInternalState, | ||
); | ||
return new SSet(newInternalState); | ||
} | ||
/** Obtain the union between two SSets. | ||
@@ -292,7 +367,8 @@ */ | ||
public difference(set: SSet): SSet { | ||
/* TODO: Fix plugin issue for optimization */ | ||
const result = this.internalTwoSetsOperations(['difference'], this, set).difference; | ||
const probEquals = result.probEquals(this); | ||
/* Performance improvement: only return new Set | ||
if difference is not itself */ | ||
const result = this.internalTwoSetsOperations(['difference'], this, set).difference; | ||
/* TODO: Use fastEquals here */ | ||
if (result.equals(this)) { | ||
if (probEquals) { | ||
return this; | ||
@@ -340,9 +416,13 @@ } | ||
/* TODO: Add onFilter? plugin listener(s) */ | ||
public filter(fn: (JSONCapable) => boolean): SSet { | ||
public filter(fn: (JSONCapable, hash) => boolean): SSet { | ||
let newSet: SSet = this; | ||
this.forEach((item) => { | ||
if (!fn(item)) { | ||
newSet = newSet.remove(item); | ||
const remove = []; | ||
/* Using mutable for performance improvement */ | ||
this.forEach((item, hash) => { | ||
if (!fn(item, hash)) { | ||
remove.push(hash); | ||
} | ||
}); | ||
newSet = newSet.removeHashes(remove); | ||
return newSet; | ||
@@ -360,5 +440,5 @@ } | ||
*/ | ||
public $(name: string): any { | ||
public $(name: string, args: any[] = []): any { | ||
const {state, props, plugins} = this.statePropsPlugins; | ||
return plugins[name].API(state, props[name]); | ||
return plugins[name].API(state, props[name], this, args); | ||
} | ||
@@ -460,5 +540,2 @@ | ||
public has(value: JSONCapable): boolean { | ||
/* Remove any unsupported values for JSON */ | ||
value = JSON.parse(JSON.stringify(value)); | ||
return SSet.hashOf(value) in this.statePropsPlugins.state; | ||
@@ -486,4 +563,7 @@ } | ||
/** Loop through SSet items and perform a function on the item if desired */ | ||
public forEach(fn: (JSONCapable) => void): void { | ||
this.getSortedKeysArray().forEach((key) => fn(this.statePropsPlugins.state[key])); | ||
public forEach(fn: (JSONCapable, key?, isLast?: boolean) => void): void { | ||
const arr = this.getSortedKeysArray(); | ||
arr.forEach((key, i) => { | ||
fn(this.statePropsPlugins.state[key], key, i + 1 === arr.length); | ||
}); | ||
} | ||
@@ -578,5 +658,9 @@ | ||
public equals(set: SSet): boolean { | ||
return this.symmetricDifference(set).isEmpty(); | ||
return (this === set) || this.symmetricDifference(set).isEmpty(); | ||
} | ||
public probEquals(set) { | ||
return this === set; | ||
} | ||
/** This method is automatically used by JSON.stringify when converting to | ||
@@ -639,11 +723,11 @@ * JSON data | ||
smallest.forEach((item) => { | ||
const largestHasIt = largest.has(item); | ||
operationsQueue.forEach((opParams: any[]) => | ||
opParams[1] = opParams[2](opParams[1], item, largestHasIt), | ||
); | ||
smallest.forEach((item, hash, isLast) => { | ||
const largestHasIt = largest.hasHash(hash); | ||
operationsQueue.forEach((opParams: any[] ) => { | ||
opParams[1] = opParams[2](opParams[1], item, largestHasIt, hash, isLast); | ||
}); | ||
}); | ||
return operationsQueue.reduce((acc, value: any) => { | ||
acc[value[0]] = value[1]; | ||
acc[value[0]] = value[1].set; | ||
return acc; | ||
@@ -650,0 +734,0 @@ }, {}); |
@@ -6,4 +6,4 @@ import _ = require('lodash'); | ||
action: 'onBeforeAdd' | 'onAdd' | 'onRemove', | ||
item: any, | ||
hash: string, | ||
items: any[], | ||
hashes: string[], | ||
internalState: SSetStatePropsPlugins, | ||
@@ -17,8 +17,16 @@ ): SSetStatePropsPlugins | any => { | ||
message: null, | ||
value: item, | ||
value: items, | ||
}; | ||
return _.reduce(Object.keys(plugins), (acc, pluginName) => { | ||
/* TODO: remove workaround */ | ||
if (Object.keys(plugins).length === 0 && items.length === 1) { | ||
return { | ||
continue: true, | ||
message: null, | ||
value: items[0], | ||
}; | ||
} | ||
return Object.keys(plugins).reduce((acc, pluginName) => { | ||
let listener: ( | ||
item: any, | ||
hash: string, | ||
items: any[], | ||
hashes: string[], | ||
props: PluginDeclarationProperties, | ||
@@ -33,3 +41,3 @@ state: any, | ||
message: null, | ||
value: item, | ||
value: items, | ||
}; | ||
@@ -39,4 +47,3 @@ }; | ||
const result = listener(item, hash, props[pluginName], state); | ||
const result = listener(items, hashes, props[pluginName], state); | ||
if (result.continue !== true) { | ||
@@ -54,6 +61,6 @@ throw new Error( | ||
return _.reduce(Object.keys(plugins), (acc, pluginName) => { | ||
return Object.keys(plugins).reduce((acc, pluginName) => { | ||
const listener: ( | ||
item: any, | ||
hash: string, | ||
items: any[], | ||
hashes: string[], | ||
props: PluginDeclarationProperties, | ||
@@ -63,4 +70,3 @@ state: any, | ||
const result = listener(item, hash, props[pluginName], state); | ||
const result = listener(items, hashes, props[pluginName], state); | ||
return { | ||
@@ -67,0 +73,0 @@ plugins, |
239540
32
7684