Skip to content

Commit

Permalink
[FEATURE] Added new method betweenLexoRanks which allows to generate …
Browse files Browse the repository at this point in the history
…multiple between lexoRanks based on user input (#1)

Closes issue reported on upstream repo:
kvandake#28.

A new method betweenLexoRanks has been introduced to efficiently
generate the intermediate lexoRank values in bulk. Previously, the
process involved identifying the exact middle rank each time, leading to
rapid growth of lexoRanks. The updated approach involves creating a
slice based on the desired number of lexoRanks, optimizing the
computation by using slices instead of repeatedly halving.
  • Loading branch information
mananrshah authored Oct 18, 2024
1 parent eaf14d5 commit c16ac45
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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
Expand Down
107 changes: 107 additions & 0 deletions __tests__/lexoRank.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
76 changes: 76 additions & 0 deletions src/lexoRank/lexoRank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

}

0 comments on commit c16ac45

Please sign in to comment.