UNPKG

ahp

Version:

Analytic Hierarchy Process(AHP) Library

1,245 lines (1,161 loc) 32.9 kB
"use strict"; const math = require("mathjs"); const numeric = require("numericjs"); const util = require("util"); /* Random Consistency Index */ const RI_MAP = { 2: 0, 3: 0.58, 4: 0.9, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49, }; /* AHP Rank Scale Table */ const AHP_RANK_SCALE_TABLE = [ { scale: 1, definition: "Equal importance", explaination: "Two factors contribute equally to the objective.", }, /*{ scale: 2, definition: 'Between scale 1 and 3', explaination: 'Between scale 1 and 3' },*/ { scale: 3, definition: "Somewhat more importance", explaination: "Experience and judgement slightly favor one over the other.", }, /* { scale: 4, definition: 'Between scale 3 and 5', explaination: 'Between scale 3 and 5' }, */ { scale: 5, definition: "Much more important", explaination: "Experience and judgment strongly favor one over the other.", }, /* { scale: 6, definition: 'Between scale 5 and 7', explaination: 'Between scale 5 and 7' }, */ { scale: 7, definition: "Very much more important", explaination: "Experience and judgment very strongly favor one over the other. Its importance is demonstrated in practice.", }, /* { scale: 8, definition: 'Between scale 7 and 9', explaination: 'Between scale 7 and 9' }, */ { scale: 9, definition: "Absolutely more important", explaination: "The evidence favoring one over the other is of the highest possible validity.", }, ]; /** * Class AHP * * @class AHP */ class AHP { /** * Creates an instance of AHP. * * @memberOf AHP */ constructor(context) { this.items = []; this.criteria = []; this.criteriaItemRank = {}; //for each criteria, compare between items this.criteriaRank = []; if (context) { this.import(context); } } /** * Import context * * @param {object} context * @returns {AHP} * * @memberOf AHP */ import(context) { let self = this; if (context.items && context.items.length > 0) { self.addItems(context.items); } if (context.criteria && context.criteria.length > 0) { self.addCriteria(context.criteria); } if (context.criteriaItemRank) { for (const criterion in context.criteriaItemRank) { self.criteriaItemRank[criterion] = self.criteriaItemRank[criterion] || []; const criterionItemRankContext = context.criteriaItemRank[criterion]; //1D array => input with ranking score if ( Array.isArray(criterionItemRankContext) && criterionItemRankContext[0] && !Array.isArray(criterionItemRankContext[0]) ) { const scoreVector = criterionItemRankContext; for (let i = 0; i < scoreVector.length; i++) { for (let j = 0; j < scoreVector.length; j++) { self.criteriaItemRank[criterion][i][j] = scoreVector[i] / scoreVector[j]; if (isNaN(self.criteriaItemRank[criterion][i][j])) { self.criteriaItemRank[criterion][i][j] = null; } } } } else if ( Array.isArray(criterionItemRankContext) && criterionItemRankContext[0] && Array.isArray(criterionItemRankContext[0]) ) { for (let i = 0; i < criterionItemRankContext.length; i++) { self.criteriaItemRank[criterion][i] = criterionItemRankContext[i] || []; for (let j = 0; j < criterionItemRankContext[i].length; j++) { self.criteriaItemRank[criterion][i][j] = criterionItemRankContext[i][j]; if (isNaN(self.criteriaItemRank[criterion][i][j])) { self.criteriaItemRank[criterion][i][j] = null; } } } } } } if (context.criteriaRank) { let criteriaRankContext = context.criteriaRank; //1D array => input with ranking score if ( Array.isArray(criteriaRankContext) && criteriaRankContext[0] && !Array.isArray(criteriaRankContext[0]) ) { let scoreVector = criteriaRankContext; for (let i = 0; i < scoreVector.length; i++) { for (let j = 0; j < scoreVector.length; j++) { self.criteriaRank[i][j] = scoreVector[i] / scoreVector[j]; if (isNaN(self.criteriaRank[i][j])) { self.criteriaRank[i][j] = null; } } } } else if ( Array.isArray(criteriaRankContext) && criteriaRankContext[0] && Array.isArray(criteriaRankContext[0]) ) { for (var i = 0; i < criteriaRankContext.length; i++) { self.criteriaRank[i] = self.criteriaRank[i] || []; for (var j = 0; j < criteriaRankContext[i].length; j++) { self.criteriaRank[i][j] = criteriaRankContext[i][j]; if (isNaN(self.criteriaRank[i][j])) { self.criteriaRank[i][j] = null; } } } } } return self; } /** * Export context as Json * * @returns {AHP} * * @memberOf AHP */ export() { let self = this; let json = { items: [], criteria: [], criteriaItemRank: {}, criteriaRank: [], }; json.items.push.apply(json.items, self.items); json.criteria.push.apply(json.criteria, self.criteria); json.criteriaItemRank = JSON.parse(JSON.stringify(self.criteriaItemRank)); json.criteriaRank = JSON.parse(JSON.stringify(self.criteriaRank)); return json; } /** * Add Items * * @param {string[]} items * @returns {AHP} * * @memberOf AHP */ addItems(items) { let self = this; items = items.filter((item) => self.items.indexOf(item) === -1); let originalLength = self.items.length; self.items.push.apply(self.items, items); for (let criterion of self.criteria) { for (let row of self.criteriaItemRank[criterion]) { row.push.apply(row, Array(items.length).fill(0)); } items.forEach((item, i) => { let newRow = Array(self.items.length).fill(0); newRow[originalLength + i] = 1; self.criteriaItemRank[criterion].push(newRow); }); } return self; } /** * Add Item * * @param {string} item * @returns {AHP} * * @memberOf AHP */ addItem(item) { this.addItems([item]); return this; } /** * Remove Item * * @param {string} item * @returns {AHP} * * @memberOf AHP */ removeItem(item) { let self = this; let index = self.items.indexOf(item); if (index >= 0) { self.items.splice(index, 1); for (let criterion of self.criteria) { self.criteriaItemRank[criterion].splice(index, 1); for (let row of self.criteriaItemRank[criterion]) { row.splice(index, 1); } } } return self; } /** * Remove Items * * @param {string[]} items * @returns {AHP} * * @memberOf AHP */ removeItems(items) { let self = this; items.forEach((item) => self.removeItem(item)); return self; } /** * Add Criteria * * @param {string[]} criteria * @returns {AHP} * * @memberOf AHP */ addCriteria(criteria) { var self = this; criteria = criteria.filter( (criterion) => self.criteria.indexOf(criterion) === -1 ); let originalLength = self.criteria.length; self.criteria.push.apply(self.criteria, criteria); for (let criterion of self.criteria) { if (!self.criteriaItemRank[criterion]) { self.criteriaItemRank[criterion] = numeric.identity(self.items.length); } } for (let row of self.criteriaRank) { row.push.apply(row, Array(criteria.length).fill(0)); } criteria.forEach((criterion, i) => { let newRow = Array(self.criteria.length).fill(0); newRow[originalLength + i] = 1; self.criteriaRank.push(newRow); }); return self; } /** * Add Criterion * * @param {string} criterion * @returns {AHP} * * @memberOf AHP */ addCriterion(criterion) { return this.addCriteria([criterion]); } /** * Remove Criterion * * @param {string} criterion * @returns {AHP} * * @memberOf AHP */ removeCriterion(criterion) { let self = this; let index = self.criteria.indexOf(criterion); if (index >= 0) { self.criteria.splice(index, 1); delete self.criteriaItemRank[criterion]; self.criteriaRank.splice(index, 1); for (let row of self.criteriaRank) { row.splice(index, 1); } } return self; } /** * Remove Criteria * * @param {string[]} criteria * @returns {AHP} * * @memberOf AHP */ removeCriteria(criteria) { let self = this; criteria.forEach((criterion) => self.removeCriterion(criterion)); return self; } /** * Reset Criteria perspective Item Rank Matrix * * @param {string[]} criteria * @returns {AHP} * * @memberOf AHP */ resetCriteriaItemRank(criteria) { let self = this; criteria.forEach(function (criterion) { this.criteriaItemRank[criterion] = numeric.identity(self.items.length); }, this); return self; } /** * Reset Criteria Rank Matrix * * @returns {AHP} * * @memberOf AHP */ resetCriteriaRank() { let self = this; this.criteriaRank = numeric.identity(self.criteria.length); return self; } /** * Set Criteria perspective Item Rank By Given Scores * * @param {string} criterion * @param {number[]} scoreVector * @returns {AHP} * * @memberOf AHP */ setCriteriaItemRankByGivenScores(criterion, scoreVector) { let self = this; for (let i = 0; i < scoreVector.length; i++) { for (let j = 0; j < scoreVector.length; j++) { self.criteriaItemRank[criterion][i][j] = scoreVector[i] / scoreVector[j]; } } return self; } /** * Rank Criteria perspective Item * * @param {string} criterion * @param {{preferredItem:string, comparingItem:string, scale:number}[]|(string|number)[]} preferences - Array of Preference. The 'preferredItem' of preference object is the preferred item while 'comparingItem' is the comparing item. The 'scale' is the preferred rank scale. You can pass the 3 objects as an array of 3 objects as well. * @returns {AHP} * * @memberOf AHP */ rankCriteriaItem(criterion, preferences) { let self = this; if (self.criteria.indexOf(criterion) === -1) { return self; } for (let prefer of preferences) { let preferredItem, comparingItem, scale; if (Array.isArray(prefer)) { [preferredItem, comparingItem, scale] = prefer; } else { preferredItem = prefer.preferredItem; comparingItem = prefer.comparingItem; scale = prefer.scale; } let itemAIndex = self.items.indexOf(preferredItem); let itemBIndex = self.items.indexOf(comparingItem); if (itemAIndex >= 0 && itemBIndex >= 0) { self.criteriaItemRank[criterion][itemAIndex][itemBIndex] = scale; self.criteriaItemRank[criterion][itemBIndex][itemAIndex] = 1 / scale; } } return self; } /** * Set Criteria Rank By Given Scores * * @param {string} criterion * @param {number[]} scoreVector * @returns {AHP} * * @memberOf AHP */ setCriteriaRankByGivenScores(scoreVector) { let self = this; for (let i = 0; i < scoreVector.length; i++) { for (let j = 0; j < scoreVector.length; j++) { self.criteriaRank[i][j] = scoreVector[i] / scoreVector[j]; } } return self; } /** * Rank Criteria * * @param {string} criterion * @param {{preferredCriterion:string, comparingCriterion:string, scale:number}[]|(string|number)[]} preferences - Array of Preference. The 'preferredCriterion' of preference object is the preferred criterion while 'comparingCriterion' is the comparing criterion. The 'scale' is the preferred rank scale. You can pass the 3 objects as an array of 3 objects as well. * @returns {AHP} * * @memberOf AHP */ rankCriteria(preferences) { let self = this; for (let prefer of preferences) { let preferredCriterion, comparingCriterion, scale; if (Array.isArray(prefer)) { [preferredCriterion, comparingCriterion, scale] = prefer; } else { preferredCriterion = prefer.preferredCriterion; comparingCriterion = prefer.comparingCriterion; scale = prefer.scale; } let criterionAIndex = self.criteria.indexOf(preferredCriterion); let criterionBIndex = self.criteria.indexOf(comparingCriterion); if (criterionAIndex >= 0 && criterionBIndex >= 0) { self.criteriaRank[criterionAIndex][criterionBIndex] = scale; self.criteriaRank[criterionBIndex][criterionAIndex] = 1 / scale; } } return self; } /** * Find next Problem, * * @returns {ContextError} * * @memberOf AHP */ findNextProblem() { let self = this; if (!self.items || self.items.length === 0) { return new NoItem(); } if (!self.criteria || self.criteria.length === 0) { return new NoCriteria(); } let [result, rowI, colI] = AHP.findRankMatrixIncompleteCell( self.criteriaRank ); if (result) { return new MissingCriteriaRank({ criterionA: self.criteria[rowI], criterionB: self.criteria[colI], }); } for (let crit in self.criteriaItemRank) { let [result, rowI, colI] = AHP.findRankMatrixIncompleteCell( self.criteriaItemRank[crit] ); if (result) { return new MissingCriteriaItemRank({ criterion: crit, itemA: self.items[rowI], itemB: self.items[colI], }); } } let { ci, ri, cr, weightedVector } = AHP.calculateMatrixConsistency( self.criteriaRank ); if (cr > 0.1) { return new CriteriaRankInsufficientConsistencyRatio({ cr: cr, }); } for (let j = 0; j < self.criteria.length; j++) { let crit = self.criteria[j]; let { ci, ri, cr, weightedVector } = AHP.calculateMatrixConsistency( self.criteriaItemRank[crit] ); if (cr > 0.1) { return new CriteriaItemRankInsufficientConsistencyRatio({ criterion: crit, cr: cr, }); } } return null; } /** * Debug Analytic Process * @returns {error: ContextError, rankingMatrix: number[][], criteriaRankMetaMap:{{ci:number,ri:number,cr:number}}, critWeightVector, rankedScoreMap:{item:number}, rankedScores: number[], log:string } * * @memberOf AHP */ debug() { let self = this; let log = ""; let debugLog = function () { for (let obj of arguments) { if (typeof obj === "object") { log += util.inspect(obj, false, null); } else { log += obj; } log += "\n"; } }; debugLog("=========================================="); debugLog("context:"); debugLog("items: ", self.items); debugLog("criteria: ", self.criteria); debugLog("criteriaItemRank: ", self.criteriaItemRank); debugLog("criteriaRank: ", self.criteriaRank); debugLog("__________________________________"); let problem = self.findNextProblem(); if (problem) { debugLog(problem); } if ( problem && (problem.type === AHP.contextErrorType.NoItem || problem.type === AHP.contextErrorType.NoCriteria) /* || problem.type === AHP.contextErrorType.MissingCriteriaItemRank || problem.type === AHP.contextErrorType.MissingCriteriaRank*/ ) { return { error: problem, rankingMatrix: null, itemRankMetaMap: null, criteriaRankMetaMap: null, rankedScoreMap: null, log: log, }; } let rankCompleteCounter = 0; for (let crit in self.criteriaItemRank) { let completed = AHP.checkRankMatrixCompleted(self.criteriaItemRank[crit]); debugLog(`criteriaItemRank['${crit}']`); if (!completed) { debugLog("(Incomplete)"); } debugLog( AHP.printMatrixAsStr(self.criteriaItemRank[crit], self.items, 10) ); if (completed) { let { ci, ri, cr } = AHP.calculateMatrixConsistency( self.criteriaItemRank[crit] ); debugLog(`Consistentcy index: ${ci}`); debugLog(`Random index: ${ri}`); debugLog(`Consistentcy ratio: ${cr}`); if (cr <= 0.1) { debugLog(`CR<=0.1 => sufficient consistency`); rankCompleteCounter++; } else { debugLog(`CR>0.1 => insufficient consistency`); } } debugLog("__________________________________"); } let maxCriteionLength = self.criteria .map((a) => a.length) .reduce((a, b) => Math.max(a.length, b.length), 5); let critRankCompleted = self.criteriaRank.length > 0 && AHP.checkRankMatrixCompleted(self.criteriaRank); debugLog(`criteriaRank:`); if (!critRankCompleted) { debugLog("(Incomplete)"); } debugLog(AHP.printMatrixAsStr(self.criteriaRank, self.criteria, 5)); if (critRankCompleted) { let { ci, ri, cr, weightedVector } = AHP.calculateMatrixConsistency( self.criteriaRank ); debugLog(`Consistentcy index: ${ci}`); debugLog(`Random index: ${ri}`); debugLog(`Consistentcy ratio: ${cr}`); if (cr > 0.1) { critRankCompleted = false; debugLog(`CR>0.1 => insufficient consistency`); } else { debugLog(`CR<=0.1 => sufficient consistency`); } debugLog(`Criteria Weight Vector: ${weightedVector}`); debugLog("__________________________________\n"); } let rankingMatrix = Array(self.items.length) .fill(null) .map(() => Array(self.criteria.length).fill(0)); let itemRankMetaMap = {}; for (let j = 0; j < self.criteria.length; j++) { let crit = self.criteria[j]; let { ci, ri, cr, weightedVector } = AHP.calculateMatrixConsistency( self.criteriaItemRank[crit] ); for (let i = 0; i < weightedVector.length; i++) { rankingMatrix[i][j] = weightedVector[i]; } itemRankMetaMap[crit] = { ci, ri, cr, }; } debugLog("rankingMatrix: (Higher score is better)"); debugLog( AHP.print2DMatrixAsStr( numeric.transpose(rankingMatrix), self.items, self.criteria, 5 ) ); debugLog("__________________________________"); let criteriaRankMetaMap = AHP.calculateMatrixConsistency(self.criteriaRank); let critWeightVector = criteriaRankMetaMap.weightedVector; let rankedScores = numeric.dotMV(rankingMatrix, critWeightVector); let rankedScoreMap = {}; self.items.forEach((item, i) => { rankedScoreMap[item] = rankedScores[i]; }); debugLog("ranked item scores: (Higher score is better)"); debugLog( AHP.print2DMatrixAsStr( numeric.transpose([rankedScores]), ["Score"], self.items, 5 ) ); debugLog("=========================================="); return { error: problem, rankingMatrix, itemRankMetaMap, criteriaRankMetaMap, rankedScoreMap, rankedScores, log, }; } /** * Debug Analytic Process * @returns {error: ContextError, rankingMatrix: number[][], criteriaRankMetaMap:{{ci:number,ri:number,cr:number}}, critWeightVector, rankedScoreMap:{item:number}, rankedScores: number[] } * * @memberOf AHP */ run() { let { error, rankingMatrix, itemRankMetaMap, criteriaRankMetaMap, rankedScoreMap, rankedScores, } = this.debug(); return { error, rankingMatrix, itemRankMetaMap, criteriaRankMetaMap, rankedScoreMap, rankedScores, }; } /** * Find Rank Matrix Incomplete Cell which is not defined or NaN. * * @static * @param {number[][]} matrix * @returns {(boolean|number)[]} - Array object. The inner array contains `find result` as the 1st object (boolean), and the row index and column index of incomplete cell as 2nd and 3rd object. * * @memberOf AHP */ static findRankMatrixIncompleteCell(matrix) { for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix.length; j++) { if (isNaN(matrix[i][j]) || matrix[i][j] <= 0) { return [true, i, j]; } } } return [false, -1, -1]; } /** * Check Rank Matrix Complated. * @static * @param {number[][]} matrix * @returns {boolean} * * @memberOf AHP */ static checkRankMatrixCompleted(matrix) { for (let row of matrix) { for (let cell of row) { if (isNaN(cell) || cell <= 0) { return false; } } } return true; } /** * static util function for printing matrix * * @static * @param {number[][]} matrix * @param {string[]} matrixHeaders * @param {number} minCellLength * @returns {string} * * @memberOf AHP */ static printMatrixAsStr(matrix, matrixHeaders, minCellLength) { if (!matrix || !matrixHeaders || matrix.length !== matrixHeaders.length) { return null; } let maxItemLength = matrixHeaders .map((a) => a.length) .reduce((a, b) => Math.max(a, b), minCellLength); let str = ""; str += "-".repeat( maxItemLength + matrixHeaders.length * (maxItemLength + 1) + 2 ) + "\n"; str += "|" + " ".repeat(maxItemLength) + "|" + matrixHeaders .map((item) => " ".repeat(maxItemLength - item.length) + item) .join("|") + "|\n"; str += "|" + "-".repeat(maxItemLength) + "|" + matrixHeaders.map((item) => "-".repeat(maxItemLength)).join("|") + "|\n"; for (let i = 0; i < matrix.length; i++) { let rowItem = matrixHeaders[i]; let row = matrix[i]; str += "|" + " ".repeat(maxItemLength - rowItem.length) + rowItem + "|" + row .map((datum) => { let rounded = Number(Math.round(datum + "e3") + "e-3").toFixed(3); let str = rounded.toString(); if (str.length < maxItemLength) { str = " ".repeat(maxItemLength - str.length) + str; } return str; }) .join("|") + "|\n"; } str += "-".repeat( maxItemLength + matrixHeaders.length * (maxItemLength + 1) + 2 ) + "\n"; return str; } static print2DMatrixAsStr( matrix, matrixColHeaders, matrixRowItems, minCellLength ) { if ( !matrix || !matrixColHeaders || !matrixRowItems || matrix.length !== matrixRowItems.length ) { return null; } let maxItemLength = matrixColHeaders .map((a) => a.length) .reduce((a, b) => Math.max(a, b), minCellLength); let maxRowItemLength = matrixRowItems .map((a) => a.length) .reduce((a, b) => Math.max(a, b), minCellLength); let str = ""; str += "-".repeat( maxRowItemLength + matrixColHeaders.length * (maxItemLength + 1) + 2 ) + "\n"; str += "|" + " ".repeat(maxRowItemLength) + "|" + matrixColHeaders .map((item) => " ".repeat(maxItemLength - item.length) + item) .join("|") + "|\n"; str += "|" + "-".repeat(maxRowItemLength) + "|" + matrixColHeaders.map((item) => "-".repeat(maxItemLength)).join("|") + "|\n"; for (let i = 0; i < matrix.length; i++) { let rowItem = matrixRowItems[i]; let row = matrix[i]; str += "|" + " ".repeat(maxRowItemLength - rowItem.length) + rowItem + "|" + row .map((datum) => { let rounded = Number(Math.round(datum + "e3") + "e-3").toFixed(3); let str = rounded.toString(); if (str.length < maxItemLength) { str = " ".repeat(maxItemLength - str.length) + str; } return str; }) .join("|") + "|\n"; } str += "-".repeat( maxRowItemLength + matrixColHeaders.length * (maxItemLength + 1) + 2 ) + "\n"; return str; } /** * static util function for calculating matrix consistency * * @static * @param {number[][]} matrix * @returns {ci: number, ri: number, cr: number, weightedVector: number[]} * * @memberOf AHP */ static calculateMatrixConsistency(matrix) { if (matrix.length === 0) { return { ci: null, ri: null, cr: null, weightedVector: null, }; } let weightedMatrix = []; for (let i = 0; i < matrix.length; i++) { let row = []; for (let j = 0; j < matrix.length; j++) { row.push(matrix[i][j]); } weightedMatrix.push(row); } for (let i = 0; i < matrix.length; i++) { let colWeightFactor = 1 / math.sum(matrix.map((row) => row[i])); for (let j = 0; j < matrix.length; j++) { weightedMatrix[j][i] = matrix[j][i] * colWeightFactor; } } let weightedVector = weightedMatrix.map((row) => math.mean(row)); let consistentcyVector = numeric.dotMV(matrix, weightedVector); let consistentcyMeasures = []; for (let i = 0; i < weightedVector.length; i++) { consistentcyMeasures[i] = consistentcyVector[i] / weightedVector[i]; } let avgConsistentcyMeasures = math.mean(consistentcyMeasures); let ci = (avgConsistentcyMeasures - matrix.length) / (matrix.length - 1); let ri = RI_MAP[matrix.length]; let cr = ri > 0 ? ci / ri : 0; return { ci, ri, cr, weightedVector, }; } } /** * Context Error * * @class ContextError */ class ContextError { /** * Creates an instance of ContextError. * * @memberOf ContextError */ constructor() { this.type = null; this.context = {}; } /** * get description * @returns {string} * @memberOf ContextError */ getDescription() {} } /** * No Item Error * * @class NoItem * @extends {ContextError} */ class NoItem extends ContextError { /** * Creates an instance of NoItem. * * @memberOf NoItem */ constructor() { super(); this.type = "NO_ITEM"; } /** * get description * @returns {string} * @memberOf NoItem */ getDescription() { return "Missing comparison options information."; } } /** * No Criteria Error * * @class NoCriteria * @extends {ContextError} */ class NoCriteria extends ContextError { /** * Creates an instance of NoCriteria. * * @memberOf NoCriteria */ constructor() { super(); this.type = "NO_CRITERIA"; } /** * get description * @returns {string} * @memberOf NoCriteria */ getDescription() { return "Missing comparison criteria information."; } } /** * Missing Criteria Iteam Rank Error * * @class MissingCriteriaItemRank * @extends {ContextError} */ class MissingCriteriaItemRank extends ContextError { /** * Creates an instance of MissingCriteriaItemRank. * @param {any} { * criterion, * itemA, * itemB * } * * @memberOf MissingCriteriaItemRank */ constructor({ criterion, itemA, itemB }) { super(); this.type = "MISSING_CRITERIA_ITEM_RANK"; this.context = { criterion, itemA, itemB, }; } /** * get description * @returns {string} * @memberOf MissingCriteriaItemRank */ getDescription() { return ( `In terms of criterion "${this.context.criterion}", which option do you prefer more and what is the scale level do you prefer?\n` + `Option (A): "${this.context.itemA}", Option (B): "${this.context.itemB}"\n\n` + `Scale(1-9):\n` + AHP_RANK_SCALE_TABLE.map( (scaleItem) => `${scaleItem.scale}: ${scaleItem.definition}` ).join("\n") + "\n" + "2,4,6,8: Intermediate values\n" ); } } /** * Missing Criteria Rank Error * * @class MissingCriteriaRank * @extends {ContextError} */ class MissingCriteriaRank extends ContextError { /** * Creates an instance of MissingCriteriaRank. * @param {any} { * criterionA, * criterionB * } * * @memberOf MissingCriteriaRank */ constructor({ criterionA, criterionB }) { super(); this.type = "MISSING_CRITERIA_RANK"; this.context = { criterionA, criterionB, }; } /** * get description * @returns {string} * @memberOf MissingCriteriaRank */ getDescription() { return ( `Which critetion do you prefer more and what is the scale level do you prefer?\n` + `Option (A): "${this.context.criterionA}", Option (B): "${this.context.criterionB}"\n\n` + `Scale(1-9):\n` + AHP_RANK_SCALE_TABLE.map( (scaleItem) => `${scaleItem.scale}: ${scaleItem.definition}` ).join("\n") + "\n" + "2,4,6,8: Intermediate values\n" ); } } /** * Criteria Item Rank Insufficient Consistendy Ratio Error * * @class CriteriaItemRankInsufficientConsistencyRatio * @extends {ContextError} */ class CriteriaItemRankInsufficientConsistencyRatio extends ContextError { /** * Creates an instance of CriteriaItemRankInsufficientConsistencyRatio. * @param {any} { * criterion, * cr * } * * @memberOf CriteriaItemRankInsufficientConsistencyRatio */ constructor({ criterion, cr }) { super(); this.type = "CRITERIA_ITEM_RANK_INSUFFICIENT_CONSISTENCY_RATIO"; this.context = { criterion, cr, }; } /** * get description * @returns {string} * @memberOf CriteriaItemRankInsufficientConsistencyRatio */ getDescription() { return `In terms of criterion "${this.context.criterion}", criteria item rank matrix consistency ratio > 0.1`; } } /** * Criteria Tank Insufficiency Ratio * * @class CriteriaRankInsufficientConsistencyRatio * @extends {ContextError} */ class CriteriaRankInsufficientConsistencyRatio extends ContextError { /** * Creates an instance of CriteriaRankInsufficientConsistencyRatio. * @param {any} { * cr * } * * @memberOf CriteriaRankInsufficientConsistencyRatio */ constructor({ cr }) { super(); this.type = "CRITERIA_RANK_INSUFFICIENT_CONSISTENCY_RATIO"; this.context = { cr, }; } /** * get description * @returns {string} * @memberOf CriteriaRankInsufficientConsistencyRatio */ getDescription() { return `Criteria rank matrix consistency ratio > 0.1`; } } AHP.AHP_RANK_SCALE_TABLE = AHP_RANK_SCALE_TABLE; AHP.contextErrorType = { NoItem: "NO_ITEM", NoCriteria: "NO_CRITERIA", MissingCriteriaItemRank: "MISSING_CRITERIA_ITEM_RANK", MissingCriteriaRank: "MISSING_CRITERIA_RANK", CriteriaItemRankInsufficientConsistencyRatio: "CRITERIA_ITEM_RANK_INSUFFICIENT_CONSISTENCY_RATIO", CriteriaRankInsufficientConsistencyRatio: "CRITERIA_RANK_INSUFFICIENT_CONSISTENCY_RATIO", }; module.exports = AHP;