UNPKG

@dalet-oss/lexorank

Version:

A reference implementation of a list ordering system like JIRA's Lexorank algorithm

331 lines (330 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LexoRank = void 0; const lexoDecimal_1 = require("./lexoDecimal"); const lexoRankBucket_1 = require("./lexoRankBucket"); const stringBuilder_1 = require("../utils/stringBuilder"); const numeralSystems_1 = require("../numeralSystems"); class LexoRank { constructor(bucket, decimal) { this.value = bucket.format() + '|' + LexoRank.formatDecimal(decimal); this.bucket = bucket; this.decimal = decimal; } static get NUMERAL_SYSTEM() { if (!this._NUMERAL_SYSTEM) { this._NUMERAL_SYSTEM = new numeralSystems_1.LexoNumeralSystem36(); } return this._NUMERAL_SYSTEM; } static get ZERO_DECIMAL() { if (!this._ZERO_DECIMAL) { this._ZERO_DECIMAL = lexoDecimal_1.LexoDecimal.parse('0', LexoRank.NUMERAL_SYSTEM); } return this._ZERO_DECIMAL; } static get ONE_DECIMAL() { if (!this._ONE_DECIMAL) { this._ONE_DECIMAL = lexoDecimal_1.LexoDecimal.parse('1', LexoRank.NUMERAL_SYSTEM); } return this._ONE_DECIMAL; } static get EIGHT_DECIMAL() { if (!this._EIGHT_DECIMAL) { this._EIGHT_DECIMAL = lexoDecimal_1.LexoDecimal.parse('8', LexoRank.NUMERAL_SYSTEM); } return this._EIGHT_DECIMAL; } static get MIN_DECIMAL() { if (!this._MIN_DECIMAL) { this._MIN_DECIMAL = LexoRank.ZERO_DECIMAL; } return this._MIN_DECIMAL; } static get MAX_DECIMAL() { if (!this._MAX_DECIMAL) { this._MAX_DECIMAL = lexoDecimal_1.LexoDecimal.parse('1000000', LexoRank.NUMERAL_SYSTEM).subtract(LexoRank.ONE_DECIMAL); } return this._MAX_DECIMAL; } static get MID_DECIMAL() { if (!this._MID_DECIMAL) { this._MID_DECIMAL = LexoRank.between(LexoRank.MIN_DECIMAL, LexoRank.MAX_DECIMAL); } return this._MID_DECIMAL; } static get INITIAL_MIN_DECIMAL() { if (!this._INITIAL_MIN_DECIMAL) { this._INITIAL_MIN_DECIMAL = lexoDecimal_1.LexoDecimal.parse('100000', LexoRank.NUMERAL_SYSTEM); } return this._INITIAL_MIN_DECIMAL; } static get INITIAL_MAX_DECIMAL() { if (!this._INITIAL_MAX_DECIMAL) { this._INITIAL_MAX_DECIMAL = lexoDecimal_1.LexoDecimal.parse(LexoRank.NUMERAL_SYSTEM.toChar(LexoRank.NUMERAL_SYSTEM.getBase() - 2) + '00000', LexoRank.NUMERAL_SYSTEM); } return this._INITIAL_MAX_DECIMAL; } static min() { return LexoRank.from(lexoRankBucket_1.default.BUCKET_0, LexoRank.MIN_DECIMAL); } static middle() { const minLexoRank = LexoRank.min(); return minLexoRank.between(LexoRank.max(minLexoRank.bucket)); } static max(bucket = lexoRankBucket_1.default.BUCKET_0) { return LexoRank.from(bucket, LexoRank.MAX_DECIMAL); } static initial(bucket) { return bucket === lexoRankBucket_1.default.BUCKET_0 ? LexoRank.from(bucket, LexoRank.INITIAL_MIN_DECIMAL) : LexoRank.from(bucket, LexoRank.INITIAL_MAX_DECIMAL); } static between(oLeft, oRight) { if (oLeft.getSystem().getBase() !== oRight.getSystem().getBase()) { throw new Error('Expected same system'); } let left = oLeft; let right = oRight; let nLeft; if (oLeft.getScale() < oRight.getScale()) { nLeft = oRight.setScale(oLeft.getScale(), false); if (oLeft.compareTo(nLeft) >= 0) { return LexoRank.mid(oLeft, oRight); } right = nLeft; } if (oLeft.getScale() > right.getScale()) { nLeft = oLeft.setScale(right.getScale(), true); if (nLeft.compareTo(right) >= 0) { return LexoRank.mid(oLeft, oRight); } left = nLeft; } let nRight; for (let scale = left.getScale(); scale > 0; right = nRight) { const nScale1 = scale - 1; const nLeft1 = left.setScale(nScale1, true); nRight = right.setScale(nScale1, false); const cmp = nLeft1.compareTo(nRight); if (cmp === 0) { return LexoRank.checkMid(oLeft, oRight, nLeft1); } if (nLeft1.compareTo(nRight) > 0) { break; } scale = nScale1; left = nLeft1; } let mid = LexoRank.middleInternal(oLeft, oRight, left, right); let nScale; for (let mScale = mid.getScale(); mScale > 0; mScale = nScale) { nScale = mScale - 1; const nMid = mid.setScale(nScale); if (oLeft.compareTo(nMid) >= 0 || nMid.compareTo(oRight) >= 0) { break; } mid = nMid; } return mid; } static parse(str) { const parts = str.split('|'); const bucket = lexoRankBucket_1.default.from(parts[0]); const decimal = lexoDecimal_1.LexoDecimal.parse(parts[1], LexoRank.NUMERAL_SYSTEM); return new LexoRank(bucket, decimal); } static from(bucket, decimal) { if (decimal.getSystem().getBase() !== LexoRank.NUMERAL_SYSTEM.getBase()) { throw new Error('Expected different system'); } return new LexoRank(bucket, decimal); } static middleInternal(lbound, rbound, left, right) { const mid = LexoRank.mid(left, right); return LexoRank.checkMid(lbound, rbound, mid); } static checkMid(lbound, rbound, mid) { if (lbound.compareTo(mid) >= 0) { return LexoRank.mid(lbound, rbound); } return mid.compareTo(rbound) >= 0 ? LexoRank.mid(lbound, rbound) : mid; } static mid(left, right) { const sum = left.add(right); const mid = sum.multiply(lexoDecimal_1.LexoDecimal.half(left.getSystem())); const scale = left.getScale() > right.getScale() ? left.getScale() : right.getScale(); if (mid.getScale() > scale) { const roundDown = mid.setScale(scale, false); if (roundDown.compareTo(left) > 0) { return roundDown; } const roundUp = mid.setScale(scale, true); if (roundUp.compareTo(right) < 0) { return roundUp; } } return mid; } static formatDecimal(decimal) { const formatVal = decimal.format(); const val = new stringBuilder_1.default(formatVal); let partialIndex = formatVal.indexOf(LexoRank.NUMERAL_SYSTEM.getRadixPointChar()); const zero = LexoRank.NUMERAL_SYSTEM.toChar(0); if (partialIndex < 0) { partialIndex = formatVal.length; val.append(LexoRank.NUMERAL_SYSTEM.getRadixPointChar()); } while (partialIndex < 6) { val.insert(0, zero); ++partialIndex; } while (val[val.length - 1] === zero) { val.length = val.length - 1; } return val.toString(); } genPrev() { if (this.isMax()) { return new LexoRank(this.bucket, LexoRank.INITIAL_MAX_DECIMAL); } const floorInteger = this.decimal.floor(); const floorDecimal = lexoDecimal_1.LexoDecimal.from(floorInteger); let nextDecimal = floorDecimal.subtract(LexoRank.EIGHT_DECIMAL); if (nextDecimal.compareTo(LexoRank.MIN_DECIMAL) <= 0) { nextDecimal = LexoRank.between(LexoRank.MIN_DECIMAL, this.decimal); } return new LexoRank(this.bucket, nextDecimal); } genNext() { if (this.isMin()) { return new LexoRank(this.bucket, LexoRank.INITIAL_MIN_DECIMAL); } const ceilInteger = this.decimal.ceil(); const ceilDecimal = lexoDecimal_1.LexoDecimal.from(ceilInteger); let nextDecimal = ceilDecimal.add(LexoRank.EIGHT_DECIMAL); if (nextDecimal.compareTo(LexoRank.MAX_DECIMAL) >= 0) { nextDecimal = LexoRank.between(this.decimal, LexoRank.MAX_DECIMAL); } return new LexoRank(this.bucket, nextDecimal); } between(other) { if (!this.bucket.equals(other.bucket)) { throw new Error('Between works only within the same bucket'); } const cmp = this.decimal.compareTo(other.decimal); if (cmp > 0) { return new LexoRank(this.bucket, LexoRank.between(other.decimal, this.decimal)); } 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); } return new LexoRank(this.bucket, LexoRank.between(this.decimal, other.decimal)); } multipleBetween(other, ranksToGenerate) { 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 = this; let right = 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 = LexoRank.binaryDepthToInsertRanks(ranksToGenerate); const lexoRanks = []; LexoRank.prepareLexoRanks(left, right, 1, binaryDepth, lexoRanks); return lexoRanks.slice(0, ranksToGenerate); } static prepareLexoRanks(left, right, currentDepth, maxDepth, lexoRanks) { // find the midpoint for this operation const rankAtThisLevel = 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); } } getBucket() { return this.bucket; } getDecimal() { return this.decimal; } inNextBucket() { return LexoRank.from(this.bucket.next(), this.decimal); } inPrevBucket() { return LexoRank.from(this.bucket.prev(), this.decimal); } isMin() { return this.decimal.equals(LexoRank.MIN_DECIMAL); } isMax() { return this.decimal.equals(LexoRank.MAX_DECIMAL); } format() { return this.value; } equals(other) { if (this === other) { return true; } if (!other) { return false; } return this.value === other.value; } toString() { return this.value; } compareTo(other) { if (this === other) { return 0; } if (!other) { return 1; } return this.value.localeCompare(other.value); } // Visible for unit test static binaryDepthToInsertRanks(noOfRanks) { if (noOfRanks < 1) return 0; let power = 1; while (Math.pow(2, power) - 1 < noOfRanks) { power++; } return power; } } exports.LexoRank = LexoRank;