diff --git a/README.md b/README.md index a6481a5..9726703 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ const any1LexoRank = LexoRank.min(); const any2LexoRank = any1LexoRank.genNext().genNext(); // calculate between const betweenLexoRank = any1LexoRank.between(any2LexoRank); +// calculate between lexoRanks +const betweenLexoRanks = any1LexoRank.multipleBetween(any2LexoRank, 5); ``` ## Related projects diff --git a/__tests__/lexoRank.spec.ts b/__tests__/lexoRank.spec.ts index 8b82ec0..fa6f3e6 100644 --- a/__tests__/lexoRank.spec.ts +++ b/__tests__/lexoRank.spec.ts @@ -64,4 +64,111 @@ describe('LexoRank', () => { const between = prevRank.between(nextRank); expect(between.toString()).toEqual(expected); }); + + it('find the nearest power', () => { + expect(LexoRank.binaryDepthToInsertRanks(1)).toEqual(1); + expect(LexoRank.binaryDepthToInsertRanks(2)).toEqual(2); + expect(LexoRank.binaryDepthToInsertRanks(3)).toEqual(2); + expect(LexoRank.binaryDepthToInsertRanks(4)).toEqual(3); + expect(LexoRank.binaryDepthToInsertRanks(5)).toEqual(3); + expect(LexoRank.binaryDepthToInsertRanks(6)).toEqual(3); + expect(LexoRank.binaryDepthToInsertRanks(7)).toEqual(3); + expect(LexoRank.binaryDepthToInsertRanks(8)).toEqual(4); + expect(LexoRank.binaryDepthToInsertRanks(15)).toEqual(4); + expect(LexoRank.binaryDepthToInsertRanks(31)).toEqual(5); + expect(LexoRank.binaryDepthToInsertRanks(63)).toEqual(6); + expect(LexoRank.binaryDepthToInsertRanks(124)).toEqual(7); + expect(LexoRank.binaryDepthToInsertRanks(127)).toEqual(7); + }); + + it('find the multipleBetween for 1 lexoRanks to generate', () => { + const minRank = LexoRank.min(); + const maxRank = LexoRank.max(); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 1); + expect(lexoRanks.length).toEqual(1); + expect(lexoRanks[0].toString()).toEqual('0|hzzzzz:'); + }); + + it('find the multipleBetween for 2 lexoRanks to generate', () => { + const minRank = LexoRank.min(); + const maxRank = LexoRank.max(); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 2); + expect(lexoRanks.length).toEqual(2); + expect(lexoRanks[0].toString()).toEqual('0|8zzzzz:'); + expect(lexoRanks[1].toString()).toEqual('0|hzzzzz:'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + it('find the multipleBetween for 2 lexoRanks when one lexoRank is long in length', () => { + const maxRank = LexoRank.parse('0|i00006:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzr'); + const minRank = LexoRank.parse('0|i00006:zzr'); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 2); + expect(lexoRanks.length).toEqual(2); + expect(lexoRanks[0].toString()).toEqual('0|i00006:zzt'); + expect(lexoRanks[1].toString()).toEqual('0|i00006:zzv'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + it('find the multipleBetween for 3 lexoRanks to generate', () => { + const minRank = LexoRank.min(); + const maxRank = LexoRank.max(); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 3); + expect(lexoRanks.length).toEqual(3); + expect(lexoRanks[0].toString()).toEqual('0|8zzzzz:'); + expect(lexoRanks[1].toString()).toEqual('0|hzzzzz:'); + expect(lexoRanks[2].toString()).toEqual('0|qzzzzz:'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + it('find the multipleBetween for 4 lexoRanks to generate', () => { + const minRank = LexoRank.min(); + const maxRank = LexoRank.max(); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 4); + expect(lexoRanks.length).toEqual(4); + expect(lexoRanks[0].toString()).toEqual('0|4hzzzz:'); + expect(lexoRanks[1].toString()).toEqual('0|8zzzzz:'); + expect(lexoRanks[2].toString()).toEqual('0|dhzzzz:'); + expect(lexoRanks[3].toString()).toEqual('0|hzzzzz:'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + it('find the multipleBetween for 7 lexoRanks to generate', () => { + const minRank = LexoRank.min(); + const maxRank = LexoRank.max(); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 7); + expect(lexoRanks.length).toEqual(7); + expect(lexoRanks[0].toString()).toEqual('0|4hzzzz:'); + expect(lexoRanks[1].toString()).toEqual('0|8zzzzz:'); + expect(lexoRanks[2].toString()).toEqual('0|dhzzzz:'); + expect(lexoRanks[3].toString()).toEqual('0|hzzzzz:'); + expect(lexoRanks[4].toString()).toEqual('0|mhzzzz:'); + expect(lexoRanks[5].toString()).toEqual('0|qzzzzz:'); + expect(lexoRanks[6].toString()).toEqual('0|vhzzzz:'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + it('find the multipleBetween for 8 lexoRanks when one lexoRank is long in length', () => { + const maxRank = LexoRank.parse('0|i00006:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzr'); + const minRank = LexoRank.parse('0|i00006:zzr'); + const lexoRanks: LexoRank[] = minRank.multipleBetween(maxRank, 8); + expect(lexoRanks.length).toEqual(8); + expect(lexoRanks[0].toString()).toEqual('0|i00006:zzri'); + expect(lexoRanks[1].toString()).toEqual('0|i00006:zzs'); + expect(lexoRanks[2].toString()).toEqual('0|i00006:zzsi'); + expect(lexoRanks[3].toString()).toEqual('0|i00006:zzt'); + expect(lexoRanks[4].toString()).toEqual('0|i00006:zzti'); + expect(lexoRanks[5].toString()).toEqual('0|i00006:zzu'); + expect(lexoRanks[6].toString()).toEqual('0|i00006:zzui'); + expect(lexoRanks[7].toString()).toEqual('0|i00006:zzv'); + expect(checkSortedInAscendingOrder(lexoRanks)).toEqual(true); + }); + + function checkSortedInAscendingOrder(arr) { + for (let i = 0; i < arr.length - 1; i++) { + if (arr[i] > arr[i + 1]) { + return false; + } + } + return true; + } }); diff --git a/src/lexoRank/lexoRank.ts b/src/lexoRank/lexoRank.ts index 42bc0fb..e1efcb5 100644 --- a/src/lexoRank/lexoRank.ts +++ b/src/lexoRank/lexoRank.ts @@ -311,6 +311,69 @@ export class LexoRank { return new LexoRank(this.bucket, LexoRank.between(this.decimal, other.decimal)); } + public multipleBetween(other: LexoRank, ranksToGenerate: number): LexoRank[] { + + if (!this.bucket.equals(other.bucket)) { + throw new Error('Between works only within the same bucket'); + } + + if (ranksToGenerate <= 0) { + throw new Error(`'ranksToGenerate' should be greater than 0`); + } + + if (ranksToGenerate == 1) { + return [this.between(other)]; + } + + let left: LexoRank = this; + let right: LexoRank = other; + + const cmp = this.decimal.compareTo(other.decimal); + if (cmp > 0) { + left = other; + right = this; + } + + if (cmp === 0) { + throw new Error( + 'Try to rank between issues with same rank this=' + + this + + ' other=' + + other + + ' this.decimal=' + + this.decimal + + ' other.decimal=' + + other.decimal + ); + } + + const binaryDepth: number = LexoRank.binaryDepthToInsertRanks(ranksToGenerate); + + const lexoRanks: LexoRank[] = []; + + LexoRank.prepareLexoRanks(left, right, 1, binaryDepth, lexoRanks); + + return lexoRanks.slice(0, ranksToGenerate); + } + + private static prepareLexoRanks(left: LexoRank, right: LexoRank, currentDepth: number, maxDepth: number, lexoRanks: LexoRank[]) { + // find the midpoint for this operation + const rankAtThisLevel: LexoRank = left.between(right); + + if (currentDepth < maxDepth) { + // recursive into the left subtree + LexoRank.prepareLexoRanks(left, rankAtThisLevel, currentDepth + 1, maxDepth, lexoRanks); + } + + // insert the LexoRank at this level + lexoRanks.push(rankAtThisLevel); + + if (currentDepth < maxDepth) { + // recursive into the right subtree + LexoRank.prepareLexoRanks(rankAtThisLevel, right, currentDepth + 1, maxDepth, lexoRanks); + } + } + public getBucket(): LexoRankBucket { return this.bucket; } @@ -366,4 +429,17 @@ export class LexoRank { return this.value.localeCompare(other.value); } + + // Visible for unit test + public static binaryDepthToInsertRanks(noOfRanks: number): number { + if (noOfRanks < 1) return 0; + + let power = 1; + while (Math.pow(2, power) - 1 < noOfRanks) { + power++; + } + + return power; + } + }