diff --git a/package.json b/package.json index 3e1744d..0e1b7dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serializable-bptree", - "version": "1.1.0", + "version": "2.0.0", "description": "Store the B+tree flexibly, not only in-memory.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/src/BPTree.ts b/src/BPTree.ts index 0ec4c37..9df87f7 100644 --- a/src/BPTree.ts +++ b/src/BPTree.ts @@ -4,7 +4,22 @@ import { ValueComparator } from './ValueComparator' import { SerializeStrategy, SerializeStrategyHead } from './SerializeStrategy' type BPTreeNodeKey = number|K -type BPTreeCondition = { gt?: V, lt?: V }|{ equal: V }|{ notEqual: V} +type BPTreeCondition = Partial<{ + /** Searches for pairs greater than the given value. */ + gt: V + /** Searches for pairs less than the given value. */ + lt: V + /** Searches for pairs greater than or equal to the given value. */ + gte: V + /** Searches for pairs less than or equal to the given value. */ + lte: V + /** "Searches for pairs equal to the given value. */ + equal: V + /** Searches for pairs not equal to the given value. */ + notEqual: V + /** Searches for values matching the given pattern. '%' matches zero or more characters, and '_' matches exactly one character. */ + like: V +}> type BPTreePair = { key: K, value: V } export type BPTreeUnknownNode = BPTreeInternalNode|BPTreeLeafNode @@ -16,6 +31,7 @@ export interface BPTreeNode { leaf: boolean parent: number next: number + prev: number } export interface BPTreeInternalNode extends BPTreeNode { @@ -39,6 +55,54 @@ export class BPTree { private readonly _creates: Map> private readonly _updates: Map> private _updatedHead: SerializeStrategyHead|null + private readonly _verifierMap: Record< + keyof BPTreeCondition, + (nodeValue: V, value: V) => boolean + > = { + gt: (nv, v) => this.comparator.isHigher(nv, v), + gte: (nv, v) => this.comparator.isHigher(nv, v) || this.comparator.isSame(nv, v), + lt: (nv, v) => this.comparator.isLower(nv, v), + lte: (nv, v) => this.comparator.isLower(nv, v) || this.comparator.isSame(nv, v), + equal: (nv, v) => this.comparator.isSame(nv, v), + notEqual: (nv, v) => this.comparator.isSame(nv, v) === false, + like: (nv, v) => { + const nodeValue = (nv as any).toString() + const value = (v as any).toString() + const pattern = value.replace(/%/g, '.*').replace(/_/g, '.') + const regex = new RegExp(`^${pattern}$`, 'i') + return regex.test(nodeValue) + }, + } + private readonly _verifierStartNode: Record< + keyof BPTreeCondition, + (value: V) => BPTreeLeafNode + > = { + gt: (v) => this.insertableNode(v), + gte: (v) => this.insertableNode(v), // todo + lt: (v) => this.insertableNode(v), + lte: (v) => this.insertableNode(v), // todo + equal: (v) => this.insertableNode(v), + notEqual: (v) => this.leftestNode(), + like: (v) => this.leftestNode() // todo + } + private readonly _verifierDirection: Record, -1|1> = { + gt: 1, + gte: 1, + lt: -1, + lte: -1, + equal: 1, + notEqual: 1, + like: 1, + } + private readonly _verifierFullSearch: Record, boolean> = { + gt: false, + gte: false, + lt: false, + lte: false, + equal: false, + notEqual: true, + like: true, + } private _createNodeId(): number { const id = this.strategy.id() @@ -48,7 +112,7 @@ export class BPTree { return id } - private _createNode(keys: number[]|K[][], values: V[], leaf = false, parent = 0, next = 0): BPTreeUnknownNode { + private _createNode(keys: number[]|K[][], values: V[], leaf = false, parent = 0, next = 0, prev = 0): BPTreeUnknownNode { const id = this._createNodeId() const node = { id, @@ -56,7 +120,8 @@ export class BPTree { values, leaf, parent, - next + next, + prev, } as BPTreeUnknownNode this.nodes.set(id, node) return node @@ -157,7 +222,7 @@ export class BPTree { return node } - private _insertableNode(value: V): BPTreeLeafNode { + protected insertableNode(value: V): BPTreeLeafNode { let node = this.root while (!node.leaf) { for (let i = 0, len = node.values.length; i < len; i++) { @@ -186,7 +251,7 @@ export class BPTree { * @param value The value to search for. */ exists(key: K, value: V): boolean { - const node = this._insertableNode(value) + const node = this.insertableNode(value) for (let i = 0, len = node.values.length; i < len; i++) { const nValue = node.values[i] if (this.comparator.isSame(value, nValue)) { @@ -281,251 +346,63 @@ export class BPTree { } } - private _equalCondition(condition: unknown): condition is { equal: V } { - return Object.prototype.hasOwnProperty.call(condition, 'equal') - } - - private _notEqualCondition(condition: unknown): condition is { notEqual: V } { - return Object.prototype.hasOwnProperty.call(condition, 'notEqual') - } - - private _onlyGtCondition(condition: unknown): condition is { gt: V, notEqual?: V } { - return ( - Object.prototype.hasOwnProperty.call(condition, 'gt') && - !Object.prototype.hasOwnProperty.call(condition, 'lt') - ) - } - - private _onlyLtCondition(condition: unknown): condition is { lt: V, notEqual?: V } { - return ( - Object.prototype.hasOwnProperty.call(condition, 'lt') && - !Object.prototype.hasOwnProperty.call(condition, 'gt') - ) - } - - private _rangeCondition(condition: unknown): condition is { gt: V, lt: V, notEqual?: V } { - return ( - Object.prototype.hasOwnProperty.call(condition, 'gt') && - Object.prototype.hasOwnProperty.call(condition, 'lt') - ) - } - - private _getKeysFromValue(value: V): Set { - const keys = new Set() - const node = this._insertableNode(value) - const [start, end] = this.search.range(node.values, value) - if (start === -1) { - return keys - } - for (let i = start; i < end; i++) { - const pairKeys = node.keys[i] - for (const key of pairKeys) { - keys.add(key) - } - } - return keys - } - - private _getKeysFromNEValue(value: V): Set { - const keys = new Set() - let node = this.leftestNode() - let done = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const pairKeys = node.keys[i] - if (this.comparator.isSame(nValue, value) === false) { - for (const key of pairKeys) { - keys.add(key) - } - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode - } - return keys - } - - private _getKeysFromRange(gt: V, lt: V): Set { - const keys = new Set() - let node = this._insertableNode(gt) - let done = false - let found = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const localKeys = node.keys[i] - if ( - this.comparator.isHigher(nValue, gt) && - this.comparator.isLower(nValue, lt) - ) { - found = true - for (const key of localKeys) { - keys.add(key) - } - } - else if (found) { - done = true - break - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode - } - return keys - } - - private _getKeysFromGt(gt: V): Set { - const keys = new Set() - let node = this._insertableNode(gt) - let done = false - let found = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const localKeys = node.keys[i] - if (this.comparator.isHigher(nValue, gt)) { - found = true - for (const key of localKeys) { - keys.add(key) - } - } - else if (found) { - done = true - break - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode - } - return keys - } - - private _getKeysFromLt(lt: V): Set { - const keys = new Set() - let node = this.leftestNode() - let done = false - let found = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const localKeys = node.keys[i] - if (this.comparator.isLower(nValue, lt)) { - found = true - for (const key of localKeys) { - keys.add(key) - } - } - else if (found) { - done = true - break - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode - } - return keys - } - - private _getPairsFromValue(value: V): BPTreePair[] { - const node = this._insertableNode(value) - const [start, end] = this.search.range(node.values, value) - if (start === -1) { - return [] - } - const pairs = [] - for (let i = start; i < end; i++) { - const keys = node.keys[i] - for (const key of keys) { - pairs.push({ key, value }) - } - } - return pairs - } - - private _getPairsFromNEValue(value: V): BPTreePair[] { + private _getPairsRightToLeft( + value: V, + startNode: BPTreeLeafNode, + fullSearch: boolean, + comparator: (nodeValue: V, value: V) => boolean + ): BPTreePair[] { const pairs = [] - let node = this.leftestNode() - let done = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const keys = node.keys[i] - if (this.comparator.isSame(nValue, value) === false) { - for (const key of keys) { - pairs.push({ key, value: nValue }) - } - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode - } - return pairs - } - - private _getPairsFromRange(gt: V, lt: V): BPTreePair[] { - const pairs = [] - let node = this._insertableNode(gt) + let node = startNode let done = false let found = false while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { + let i = node.values.length + while (i--) { const nValue = node.values[i] const keys = node.keys[i] - if ( - this.comparator.isHigher(nValue, gt) && - this.comparator.isLower(nValue, lt) - ) { + if (comparator(nValue, value)) { found = true - for (const key of keys) { - pairs.push({ key, value: nValue }) + let j = keys.length + while (j--) { + pairs.push({ key: keys[j], value: nValue }) } } - else if (found) { + else if (found && !fullSearch) { done = true break } } - if (!node.next) { + if (!node.prev) { done = true break } - node = this.getNode(node.next) as BPTreeLeafNode + node = this.getNode(node.prev) as BPTreeLeafNode } - return pairs + return pairs.reverse() } - private _getPairsFromGt(gt: V): BPTreePair[] { + private _getPairsLeftToRight( + value: V, + startNode: BPTreeLeafNode, + fullSearch: boolean, + comparator: (nodeValue: V, value: V) => boolean + ): BPTreePair[] { const pairs = [] - let node = this._insertableNode(gt) + let node = startNode let done = false let found = false while (!done) { for (let i = 0, len = node.values.length; i < len; i++) { const nValue = node.values[i] const keys = node.keys[i] - if (this.comparator.isHigher(nValue, gt)) { + if (comparator(nValue, value)) { found = true for (const key of keys) { pairs.push({ key, value: nValue }) } } - else if (found) { + else if (found && !fullSearch) { done = true break } @@ -539,88 +416,69 @@ export class BPTree { return pairs } - private _getPairsFromLt(lt: V): BPTreePair[] { - const pairs = [] - let node = this.leftestNode() - let done = false - let found = false - while (!done) { - for (let i = 0, len = node.values.length; i < len; i++) { - const nValue = node.values[i] - const keys = node.keys[i] - if (this.comparator.isLower(nValue, lt)) { - found = true - for (const key of keys) { - pairs.push({ key, value: nValue }) - } - } - else if (found) { - done = true - break - } - } - if (!node.next) { - done = true - break - } - node = this.getNode(node.next) as BPTreeLeafNode + protected getPairs( + value: V, + startNode: BPTreeLeafNode, + fullSearch: boolean, + comparator: (nodeValue: V, value: V) => boolean, + direction: -1|1 + ): BPTreePair[] { + switch (direction) { + case -1: return this._getPairsRightToLeft(value, startNode, fullSearch, comparator) + case +1: return this._getPairsLeftToRight(value, startNode, fullSearch, comparator) + default: throw new Error(`Direction must be -1 or 1. but got a ${direction}`) } - return pairs } /** * It searches for a key within the tree. The result is returned as an array sorted in ascending order based on the value. - * The result is key set instance, and you can use the `gt`, `lt`, `equal`, `notEqual` condition statements. + * The result is key set instance, and you can use the `gt`, `lt`, `gte`, `lte`, `equal`, `notEqual`, `like` condition statements. * This method operates much faster than first searching with `where` and then retrieving only the key list. - * @param condition You can use the `gt`, `lt`, `equal`, `notEqual` condition statements. + * @param condition You can use the `gt`, `lt`, `gte`, `lte`, `equal`, `notEqual`, `like` condition statements. */ keys(condition: BPTreeCondition): Set { - if (this._equalCondition(condition)) { - return this._getKeysFromValue(condition.equal) - } - else if (this._notEqualCondition(condition)) { - return this._getKeysFromNEValue(condition.notEqual) - } - else if (this._rangeCondition(condition)) { - const { gt, lt } = condition - return this._getKeysFromRange(gt, lt) - } - else if (this._onlyGtCondition(condition)) { - return this._getKeysFromGt(condition.gt) - } - else if (this._onlyLtCondition(condition)) { - return this._getKeysFromLt(condition.lt) - } - else { - throw new Error(`The 'condition' parameter is invalid.`) + let result: K[]|null = null + for (const k in condition) { + const key = k as keyof BPTreeCondition + const value = condition[key] as V + const startNode = this._verifierStartNode[key](value) + const direction = this._verifierDirection[key] + const fullSearch = this._verifierFullSearch[key] + const comparator = this._verifierMap[key] + const pairs = this.getPairs(value, startNode, fullSearch, comparator, direction) + if (result === null) { + result = pairs.map((pair) => pair.key) + } + else { + result = result.filter((key) => pairs.find((p) => p.key === key)) + } } + return new Set(result ?? []) } /** * It searches for a value within the tree. The result is returned as an array sorted in ascending order based on the value. - * The result includes the key and value attributes, and you can use the `gt`, `lt`, `equal`, `notEqual` condition statements. - * @param condition You can use the `gt`, `lt`, `equal`, `notEqual` condition statements. + * The result includes the key and value attributes, and you can use the `gt`, `lt`, `gte`, `lte`, `equal`, `notEqual`, `like` condition statements. + * @param condition You can use the `gt`, `lt`, `gte`, `lte`, `equal`, `notEqual`, `like` condition statements. */ where(condition: BPTreeCondition): BPTreePair[] { - if (this._equalCondition(condition)) { - return this._getPairsFromValue(condition.equal) - } - else if (this._notEqualCondition(condition)) { - return this._getPairsFromNEValue(condition.notEqual) - } - else if (this._rangeCondition(condition)) { - const { gt, lt } = condition - return this._getPairsFromRange(gt, lt) - } - else if (this._onlyGtCondition(condition)) { - return this._getPairsFromGt(condition.gt) - } - else if (this._onlyLtCondition(condition)) { - return this._getPairsFromLt(condition.lt) - } - else { - throw new Error(`The 'condition' parameter is invalid.`) + let result: BPTreePair[]|null = null + for (const k in condition) { + const key = k as keyof BPTreeCondition + const value = condition[key] as V + const startNode = this._verifierStartNode[key](value) + const direction = this._verifierDirection[key] + const fullSearch = this._verifierFullSearch[key] + const comparator = this._verifierMap[key] + const pairs = this.getPairs(value, startNode, fullSearch, comparator, direction) + if (result === null) { + result = pairs + } + else { + result = result.filter((pair) => pairs.find((p) => p.key === pair.key)) + } } + return result ?? [] } /** @@ -630,7 +488,7 @@ export class BPTree { * @param value The value of the pair. */ insert(key: K, value: V): void { - const before = this._insertableNode(value) + const before = this.insertableNode(value) this._insertAtLeaf(before, key, value) if (before.values.length === this.order) { @@ -639,14 +497,21 @@ export class BPTree { [], true, before.parent, - before.next + before.next, + before.id, ) as BPTreeLeafNode const mid = Math.ceil(this.order/2)-1 + const beforeNext = before.next after.values = before.values.slice(mid+1) after.keys = before.keys.slice(mid+1) before.values = before.values.slice(0, mid+1) before.keys = before.keys.slice(0, mid+1) before.next = after.id + if (beforeNext) { + const node = this.getNode(beforeNext) + node.prev = after.id + this._setUpdates(node) + } this._insertInParent(before, after.values[0], after) this._setCreates(after) this._setUpdates(before) @@ -663,7 +528,7 @@ export class BPTree { * @param value The value of the pair. */ delete(key: K, value: V): void { - const node = this._insertableNode(value) + const node = this.insertableNode(value) let i = node.values.length while (i--) { const nValue = node.values[i] @@ -733,43 +598,46 @@ export class BPTree { let parentNode = this.getNode(node.parent) as BPTreeInternalNode let prevNode: BPTreeInternalNode|null = null let nextNode: BPTreeInternalNode|null = null - let prevK: V|null = null - let postK: V|null = null + let prevValue: V|null = null + let postValue: V|null = null for (let i = 0, len = parentNode.keys.length; i < len; i++) { const nKey = parentNode.keys[i] if (nKey === node.id) { if (i > 0) { prevNode = this.getNode(parentNode.keys[i-1]) as BPTreeInternalNode - prevK = parentNode.values[i-1] + prevValue = parentNode.values[i-1] } if (i < parentNode.keys.length-1) { nextNode = this.getNode(parentNode.keys[i+1]) as BPTreeInternalNode - postK = parentNode.values[i] + postValue = parentNode.values[i] } } } let pointer: BPTreeUnknownNode let guess: V|null + // 부모의 첫 자식 노드일 경우 if (prevNode === null) { pointer = nextNode! - guess = postK + guess = postValue } + // 부모의 마지막 자식 노드일 경우 else if (nextNode === null) { isPredecessor = true pointer = prevNode - guess = prevK + guess = prevValue } + // 부모의 중간 자식 노드일 경우 else { if (node.values.length + nextNode.values.length < this.order) { pointer = nextNode - guess = postK + guess = postValue } else { isPredecessor = true pointer = prevNode - guess = prevK + guess = prevValue } } if (node.values.length + pointer!.values.length < this.order) { @@ -784,6 +652,20 @@ export class BPTree { } else { pointer.next = node.next + pointer.prev = node.id + if (pointer.next) { + const n = this.getNode(node.next) + n.prev = pointer.id + this._setUpdates(n) + } + if (pointer.prev) { + const n = this.getNode(node.id) + n.next = pointer.id + this._setUpdates(n) + } + if (isPredecessor) { + pointer.prev = 0 + } } pointer.values.push(...node.values) diff --git a/test/unit.test.ts b/test/unit.test.ts index 293002f..0fbdc52 100644 --- a/test/unit.test.ts +++ b/test/unit.test.ts @@ -77,6 +77,30 @@ describe('unit-test', () => { { key: 'ㅋ', value: 8 }, { key: 'i', value: 9 }, ]) + expect(tree.where({ gte: 5, lte: 10 })).toEqual([ + { key: 'e', value: 5 }, + { key: 'f', value: 6 }, + { key: 'ㅌ', value: 6 }, + { key: 'g', value: 7 }, + { key: 'h', value: 8 }, + { key: 'ㅋ', value: 8 }, + { key: 'i', value: 9 }, + { key: 'j', value: 10 }, + { key: 'ㅊ', value: 10 }, + ]) + expect(tree.where({ gte: 5, lt: 10 })).toEqual([ + { key: 'e', value: 5 }, + { key: 'f', value: 6 }, + { key: 'ㅌ', value: 6 }, + { key: 'g', value: 7 }, + { key: 'h', value: 8 }, + { key: 'ㅋ', value: 8 }, + { key: 'i', value: 9 }, + ]) + expect(tree.where({ gte: 5, lte: 10, equal: 6 })).toEqual([ + { key: 'f', value: 6 }, + { key: 'ㅌ', value: 6 }, + ]) console.log(tree.keys({ gt: 0, lt: 10 })) }) @@ -117,6 +141,15 @@ describe('unit-test', () => { { key: 'f', value: 'the' }, { key: 'g', value: 'things' }, ]) + expect(tree.where({ like: '%h%' })).toEqual([ + { key: 'f', value: 'the' }, + { key: 'g', value: 'things' }, + { key: 'a', value: 'why' }, + ]) + expect(tree.where({ like: '%_s' })).toEqual([ + { key: 'c', value: 'cats' }, + { key: 'g', value: 'things' }, + ]) }) test('insert:notEqual', () => { @@ -250,29 +283,34 @@ describe('strategy-test', () => { new FileIOStrategy(6, storageDirectory), new NumericComparator() ) - tree.insert('a', 1) - tree.insert('b', 2) - tree.insert('c', 3) - tree.insert('d', 4) - tree.insert('e', 5) - tree.insert('f', 6) - tree.insert('g', 7) - tree.insert('h', 8) - tree.insert('i', 9) - tree.insert('j', 10) - tree.delete('d', 4) - tree.delete('g', 7) - tree.delete('h', 8) + const max = 50 + for (let i = 1; i < max; i++) { + tree.insert(i.toString(), i) + } + for (let i = 1; i < max; i++) { + if (i%3 === 0) { + tree.delete(i.toString(), i) + } + } tree.setHeadData({ ...tree.getHeadData(), count: (tree.getHeadData().count as number ?? 0)+1, at: Date.now() }) + for (let i = 1; i < max; i++) { + const r = tree.where({ equal: i }) + if (i%3 === 0) { + expect(r).toEqual([]) + } + else { + expect(r).toEqual([{ key: i.toString(), value: i }]) + } + } - expect(tree.where({ equal: 4 })).toEqual([]) - expect(tree.where({ equal: 7 })).toEqual([]) - expect(tree.where({ equal: 8 })).toEqual([]) + // expect(tree.where({ equal: 4 })).toEqual([]) + // expect(tree.where({ equal: 7 })).toEqual([]) + // expect(tree.where({ equal: 8 })).toEqual([]) }) }) \ No newline at end of file