├── .eslintignore ├── .gitignore ├── .eslintrc ├── CHANGELOG.md ├── libs ├── validateDeep.js ├── removeDeepFromMap.js ├── toDeepMap.js ├── PriorityQueue.js └── Graph.js ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── Gulpfile.js ├── LICENSE.md ├── test ├── removeDeepFromMap.test.js ├── validateDeep.test.js ├── toDeepMap.test.js ├── PriorityQueue.test.js └── Graph.test.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:import/recommended", "plugin:jsx-a11y/recommended", "prettier"], 3 | "plugins": ["import", "jsx-a11y", "prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.5.1] - 2025-09-17 9 | 10 | ### Changed 11 | - Updated dependencies to latest versions 12 | - No breaking changes 13 | 14 | ### Added 15 | - GitHub Actions workflow for automated NPM releases 16 | - CI/CD pipeline for quality-gated publishing 17 | 18 | ## [2.5.0] - 2017-05-03 19 | 20 | ### Added 21 | - `avoid` option in `Graph#path` - You can now pass an array of nodes to avoid when computing the path -------------------------------------------------------------------------------- /libs/validateDeep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validate a map to ensure all it's values are either a number or a map 3 | * 4 | * @param {Map} map - Map to valiadte 5 | */ 6 | function validateDeep(map) { 7 | if (!(map instanceof Map)) { 8 | throw new Error(`Invalid graph: Expected Map instead found ${typeof map}`); 9 | } 10 | 11 | map.forEach((value, key) => { 12 | if (typeof value === "object" && value instanceof Map) { 13 | validateDeep(value); 14 | return; 15 | } 16 | 17 | if (typeof value !== "number" || value <= 0) { 18 | throw new Error( 19 | `Values must be numbers greater than 0. Found value ${value} at ${key}` 20 | ); 21 | } 22 | }); 23 | } 24 | 25 | module.exports = validateDeep; 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '18.x' 19 | registry-url: 'https://registry.npmjs.org' 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Run tests 26 | run: npm test 27 | 28 | - name: Publish to NPM 29 | run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node-version: [16.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: "npm" 26 | 27 | - run: npm ci 28 | 29 | - run: npm test 30 | 31 | - uses: codecov/codecov-action@v3 32 | with: 33 | directory: ./coverage 34 | -------------------------------------------------------------------------------- /libs/removeDeepFromMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes a key and all of its references from a map. 3 | * This function has no side-effects as it returns 4 | * a brand new map. 5 | * 6 | * @param {Map} map - Map to remove the key from 7 | * @param {string} key - Key to remove from the map 8 | * @return {Map} New map without the passed key 9 | */ 10 | function removeDeepFromMap(map, key) { 11 | const newMap = new Map(); 12 | 13 | for (const [aKey, val] of map) { 14 | if (aKey !== key && val instanceof Map) { 15 | newMap.set(aKey, removeDeepFromMap(val, key)); 16 | } else if (aKey !== key) { 17 | newMap.set(aKey, val); 18 | } 19 | } 20 | 21 | return newMap; 22 | } 23 | 24 | module.exports = removeDeepFromMap; 25 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 2 | 3 | const babel = require("gulp-babel"); 4 | const browserify = require("browserify"); 5 | const gulp = require("gulp"); 6 | const rename = require("gulp-rename"); 7 | const source = require("vinyl-source-stream"); 8 | const uglify = require("gulp-uglify"); 9 | 10 | /** 11 | * Prepares the files for browser usage 12 | * 13 | * - Bundle with browserify 14 | * - Transpile with Babel 15 | * - Minify with uglify 16 | */ 17 | gulp.task("build", ["bundle"], () => { 18 | gulp 19 | .src("./dist/dijkstra.js") 20 | .pipe(babel()) 21 | .pipe(gulp.dest("./dist")) 22 | .pipe(uglify()) 23 | .pipe(rename({ suffix: ".min" })) 24 | .pipe(gulp.dest("./dist")); 25 | }); 26 | 27 | gulp.task("bundle", () => { 28 | const b = browserify({ entries: "./libs/Graph.js" }); 29 | 30 | return b.bundle().pipe(source("dijkstra.js")).pipe(gulp.dest("./dist")); 31 | }); 32 | -------------------------------------------------------------------------------- /libs/toDeepMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates a cost for a node 3 | * 4 | * @private 5 | * @param {number} val - Cost to validate 6 | * @return {bool} 7 | */ 8 | function isValidNode(val) { 9 | const cost = Number(val); 10 | 11 | if (isNaN(cost) || cost <= 0) { 12 | return false; 13 | } 14 | 15 | return true; 16 | } 17 | 18 | /** 19 | * Creates a deep `Map` from the passed object. 20 | * 21 | * @param {Object} source - Object to populate the map with 22 | * @return {Map} New map with the passed object data 23 | */ 24 | function toDeepMap(source) { 25 | const map = new Map(); 26 | const keys = Object.keys(source); 27 | 28 | keys.forEach((key) => { 29 | const val = source[key]; 30 | 31 | if (val !== null && typeof val === "object" && !Array.isArray(val)) { 32 | return map.set(key, toDeepMap(val)); 33 | } 34 | 35 | if (!isValidNode(val)) { 36 | throw new Error( 37 | `Could not add node at key "${key}", make sure it's a valid node`, 38 | val 39 | ); 40 | } 41 | 42 | return map.set(key, Number(val)); 43 | }); 44 | 45 | return map; 46 | } 47 | 48 | module.exports = toDeepMap; 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alberto Restifo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/removeDeepFromMap.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 3 | 4 | require("must"); 5 | 6 | const removeDeepFromMap = require("../libs/removeDeepFromMap"); 7 | 8 | describe("removeDeepFromMap", () => { 9 | it("returns a map without the passed key", () => { 10 | const map = new Map(); 11 | map.set("a", true); 12 | map.set("b", true); 13 | 14 | const newMap = removeDeepFromMap(map, "b"); 15 | 16 | newMap.has("b").must.be.false(); 17 | newMap.has("a").must.be.true(); 18 | }); 19 | 20 | it("removes a deep reference to the key", () => { 21 | const map = new Map(); 22 | const barMap = new Map(); 23 | 24 | barMap.set("bar", true); 25 | barMap.set("foo", true); 26 | 27 | map.set("foo", barMap); 28 | map.set("bar", true); 29 | 30 | const newMap = removeDeepFromMap(map, "bar"); 31 | 32 | newMap.has("foo").must.be.true(); 33 | newMap.get("foo").has("foo").must.be.true(); 34 | newMap.get("foo").has("bar").must.be.false(); 35 | newMap.has("bar").must.be.false(); 36 | }); 37 | 38 | it("produes no side-effects", () => { 39 | const map = new Map(); 40 | map.set("a", true); 41 | map.set("b", true); 42 | 43 | const newMap = removeDeepFromMap(map, "b"); 44 | 45 | newMap.has("b").must.be.false(); 46 | map.has("b").must.be.true(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-dijkstra", 3 | "version": "2.5.1", 4 | "description": "A NodeJS implementation of Dijkstra's algorithm", 5 | "author": "Alberto Restifo ", 6 | "main": "libs/Graph.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/albertorestifo/node-dijkstra" 10 | }, 11 | "scripts": { 12 | "test": "eslint . && nyc --reporter=html mocha -t 5000", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint . --fix", 15 | "compile": "gulp build" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/albertorestifo/node-dijkstra/issues" 19 | }, 20 | "keywords": [ 21 | "dijkstra", 22 | "shortest path" 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@babel/core": "^7.20.5", 27 | "@babel/eslint-parser": "^7.19.1", 28 | "browserify": "^17.0.0", 29 | "eslint": "^8.29.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-import": "^2.26.0", 32 | "eslint-plugin-jsx-a11y": "^6.6.1", 33 | "eslint-plugin-prettier": "^4.2.1", 34 | "gulp": "^5.0.1", 35 | "gulp-babel": "^8.0.0", 36 | "gulp-rename": "^2.0.0", 37 | "gulp-uglify": "^3.0.2", 38 | "mocha": "^10.2.0", 39 | "must": "^0.13.4", 40 | "nyc": "^15.1.0", 41 | "sinon": "^15.0.0", 42 | "vinyl-source-stream": "^2.0.0" 43 | }, 44 | "browserify": { 45 | "transform": [ 46 | "babelify" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/validateDeep.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 3 | 4 | require("must"); 5 | const demand = require("must"); 6 | 7 | const validateDeep = require("../libs/validateDeep"); 8 | 9 | describe("validateDeep()", () => { 10 | it("does nothing on a valid deep map", () => { 11 | const m = new Map(); 12 | const a = new Map(); 13 | a.set("a", 1); 14 | m.set("a", a); 15 | 16 | validateDeep(m); 17 | }); 18 | 19 | it("rejects non-number values", () => { 20 | const m = new Map(); 21 | const a = new Map(); 22 | a.set("a", "something"); 23 | m.set("a", a); 24 | 25 | demand(validateDeep.bind(this, m)).to.throw(Error, /must be numbers/); 26 | }); 27 | 28 | it("rejects negative values", () => { 29 | const m = new Map(); 30 | const a = new Map(); 31 | a.set("a", -3); 32 | m.set("a", a); 33 | 34 | demand(validateDeep.bind(this, m)).to.throw(Error, /must be numbers/); 35 | }); 36 | 37 | it("rejects 0", () => { 38 | const m = new Map(); 39 | const a = new Map(); 40 | a.set("a", 0); 41 | m.set("a", a); 42 | 43 | demand(validateDeep.bind(this, m)).to.throw(Error, /must be numbers/); 44 | }); 45 | 46 | it("rejects graphs not of type Map", () => { 47 | demand(validateDeep.bind(this, undefined)).to.throw( 48 | Error, 49 | /Expected Map instead/ 50 | ); 51 | }); 52 | 53 | it("accepts 0.02", () => { 54 | const m = new Map(); 55 | const a = new Map(); 56 | a.set("a", 0.02); 57 | m.set("a", a); 58 | 59 | validateDeep(m); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/toDeepMap.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 3 | 4 | require("must"); 5 | const demand = require("must"); 6 | 7 | const toDeepMap = require("../libs/toDeepMap"); 8 | 9 | describe("toDeepMap()", () => { 10 | it("transforms a one level object", () => { 11 | const obj = { example: 1 }; 12 | 13 | const map = toDeepMap(obj); 14 | 15 | map.size.must.equal(1); 16 | map.get("example").must.equal(1); 17 | }); 18 | 19 | it("transforms a two level object", () => { 20 | const obj = { 21 | a: { b: 1 }, 22 | }; 23 | 24 | const map = toDeepMap(obj); 25 | 26 | map.size.must.equal(1); 27 | map.get("a").must.be.instanceOf(Map); 28 | map.get("a").get("b").must.equal(1); 29 | }); 30 | 31 | it("transforms a three level object", () => { 32 | const obj = { 33 | a: { 34 | b: { c: 1 }, 35 | }, 36 | }; 37 | 38 | const map = toDeepMap(obj); 39 | 40 | map.size.must.equal(1); 41 | map.get("a").must.be.instanceOf(Map); 42 | map.get("a").get("b").must.be.instanceOf(Map); 43 | map.get("a").get("b").get("c").must.equal(1); 44 | }); 45 | 46 | it("transforms a four level object", () => { 47 | const obj = { 48 | a: { 49 | b: { 50 | c: { d: 1 }, 51 | }, 52 | }, 53 | }; 54 | 55 | const map = toDeepMap(obj); 56 | 57 | map.size.must.equal(1); 58 | map.get("a").must.be.instanceOf(Map); 59 | map.get("a").get("b").get("c").must.be.instanceOf(Map); 60 | map.get("a").get("b").get("c").get("d").must.equal(1); 61 | }); 62 | 63 | it("rejects non-number values", () => { 64 | const obj = { example: null }; 65 | 66 | demand(toDeepMap.bind(this, obj)).to.throw(Error, /valid node/); 67 | }); 68 | 69 | it("rejects negative values", () => { 70 | const obj = { example: -3 }; 71 | 72 | demand(toDeepMap.bind(this, obj)).to.throw(Error, /valid node/); 73 | }); 74 | 75 | it("rejects 0", () => { 76 | const obj = { example: 0 }; 77 | 78 | demand(toDeepMap.bind(this, obj)).to.throw(Error, /valid node/); 79 | }); 80 | 81 | it("accepts 0.02", () => { 82 | const obj = { example: 0.02 }; 83 | 84 | const map = toDeepMap(obj); 85 | 86 | map.size.must.equal(1); 87 | map.get("example").must.equal(0.02); 88 | }); 89 | 90 | it("accepts a string reppreseting a number", () => { 91 | const obj = { example: "4" }; 92 | 93 | const map = toDeepMap(obj); 94 | 95 | map.size.must.equal(1); 96 | map.get("example").must.equal(4); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /libs/PriorityQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This very basic implementation of a priority queue is used to select the 3 | * next node of the graph to walk to. 4 | * 5 | * The queue is always sorted to have the least expensive node on top. 6 | * Some helper methods are also implemented. 7 | * 8 | * You should **never** modify the queue directly, but only using the methods 9 | * provided by the class. 10 | */ 11 | class PriorityQueue { 12 | /** 13 | * Creates a new empty priority queue 14 | */ 15 | constructor() { 16 | // The `keys` set is used to greatly improve the speed at which we can 17 | // check the presence of a value in the queue 18 | this.keys = new Set(); 19 | this.queue = []; 20 | } 21 | 22 | /** 23 | * Sort the queue to have the least expensive node to visit on top 24 | * 25 | * @private 26 | */ 27 | sort() { 28 | this.queue.sort((a, b) => a.priority - b.priority); 29 | } 30 | 31 | /** 32 | * Sets a priority for a key in the queue. 33 | * Inserts it in the queue if it does not already exists. 34 | * 35 | * @param {any} key Key to update or insert 36 | * @param {number} value Priority of the key 37 | * @return {number} Size of the queue 38 | */ 39 | set(key, value) { 40 | const priority = Number(value); 41 | if (isNaN(priority)) throw new TypeError('"priority" must be a number'); 42 | 43 | if (!this.keys.has(key)) { 44 | // Insert a new entry if the key is not already in the queue 45 | this.keys.add(key); 46 | this.queue.push({ key, priority }); 47 | } else { 48 | // Update the priority of an existing key 49 | this.queue.map((element) => { 50 | if (element.key === key) { 51 | Object.assign(element, { priority }); 52 | } 53 | 54 | return element; 55 | }); 56 | } 57 | 58 | this.sort(); 59 | return this.queue.length; 60 | } 61 | 62 | /** 63 | * The next method is used to dequeue a key: 64 | * It removes the first element from the queue and returns it 65 | * 66 | * @return {object} First priority queue entry 67 | */ 68 | next() { 69 | const element = this.queue.shift(); 70 | 71 | // Remove the key from the `_keys` set 72 | this.keys.delete(element.key); 73 | 74 | return element; 75 | } 76 | 77 | /** 78 | * @return {boolean} `true` when the queue is empty 79 | */ 80 | isEmpty() { 81 | return Boolean(this.queue.length === 0); 82 | } 83 | 84 | /** 85 | * Check if the queue has a key in it 86 | * 87 | * @param {any} key Key to lookup 88 | * @return {boolean} 89 | */ 90 | has(key) { 91 | return this.keys.has(key); 92 | } 93 | 94 | /** 95 | * Get the element in the queue with the specified key 96 | * 97 | * @param {any} key Key to lookup 98 | * @return {object} 99 | */ 100 | get(key) { 101 | return this.queue.find((element) => element.key === key); 102 | } 103 | } 104 | 105 | module.exports = PriorityQueue; 106 | -------------------------------------------------------------------------------- /test/PriorityQueue.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 3 | 4 | require("must"); 5 | const demand = require("must"); 6 | const sinon = require("sinon"); 7 | 8 | const Queue = require("../libs/PriorityQueue"); 9 | 10 | describe("PriorityQueue", () => { 11 | describe("#constructor", () => { 12 | it("starts an empty queue and keys set", () => { 13 | const queue = new Queue(); 14 | 15 | queue.keys.must.be.instanceOf(Set); 16 | queue.queue.must.be.an.array(); 17 | }); 18 | }); 19 | 20 | describe("#sort()", () => { 21 | it("sorts by having the smallest first", () => { 22 | const queue = new Queue(); 23 | queue.queue = [{ priority: 10 }, { priority: 1 }]; 24 | 25 | queue.sort(); 26 | queue.queue[0].priority.must.equal(1); 27 | }); 28 | }); 29 | 30 | describe("#set()", () => { 31 | it("only accept numbers as priority values", () => { 32 | const queue = new Queue(); 33 | 34 | demand(queue.set.bind(queue, "key", {})).throw(TypeError, /number/); 35 | }); 36 | 37 | it("adds an unexisting key to the queue and reorders it", () => { 38 | const queue = new Queue(); 39 | sinon.spy(queue, "sort"); 40 | 41 | queue.set("ok", 1); 42 | 43 | sinon.assert.calledOnce(queue.sort); 44 | queue.keys.size.must.equal(1); 45 | queue.queue.must.have.length(1); 46 | queue.queue[0].key.must.equal("ok"); 47 | queue.queue[0].priority.must.equal(1); 48 | }); 49 | 50 | it("updates the value of an existing key", () => { 51 | const queue = new Queue(); 52 | sinon.spy(queue, "sort"); 53 | 54 | queue.set("ok", 1); 55 | queue.set("ok", 5); 56 | 57 | sinon.assert.calledTwice(queue.sort); 58 | queue.keys.size.must.equal(1); 59 | queue.queue.must.have.length(1); 60 | queue.queue[0].key.must.equal("ok"); 61 | queue.queue[0].priority.must.equal(5); 62 | }); 63 | }); 64 | 65 | describe("#next()", () => { 66 | it("removes the first element in the queue", () => { 67 | const queue = new Queue(); 68 | queue.set("ok", 10); 69 | queue.set("not-ok", 1); 70 | 71 | queue.next(); 72 | 73 | queue.queue.must.have.length(1); 74 | queue.keys.size.must.equal(1); 75 | }); 76 | 77 | it("return the first element in the queue", () => { 78 | const queue = new Queue(); 79 | queue.set("ok", 10); 80 | queue.set("not-ok", 1); 81 | 82 | const el = queue.next(); 83 | 84 | el.must.have.keys(["priority", "key"]); 85 | el.priority.must.equal(1); 86 | el.key.must.equal("not-ok"); 87 | }); 88 | }); 89 | 90 | describe("#isEmpty()", () => { 91 | it("returns false when there are elements in the queue", () => { 92 | const queue = new Queue(); 93 | queue.set("ok", 3); 94 | 95 | queue.isEmpty().must.be.false(); 96 | }); 97 | 98 | it("returns true when the queue is empty", () => { 99 | const queue = new Queue(); 100 | 101 | queue.isEmpty().must.be.true(); 102 | }); 103 | }); 104 | 105 | describe("#has()", () => { 106 | it("returns false when the key does not exist", () => { 107 | const queue = new Queue(); 108 | queue.set("not-ok", 3); 109 | 110 | queue.has("ok").must.be.false(); 111 | }); 112 | 113 | it("returns false when the key does not exist", () => { 114 | const queue = new Queue(); 115 | queue.set("not-ok", 3); 116 | 117 | queue.has("ok").must.be.false(); 118 | }); 119 | }); 120 | 121 | describe("#get()", () => { 122 | it("gets the entry with the provided key", () => { 123 | const queue = new Queue(); 124 | queue.set("ok", 3); 125 | 126 | const res = queue.get("ok"); 127 | res.must.have.keys(["key", "priority"]); 128 | res.key.must.equal("ok"); 129 | res.priority.must.equal(3); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-dijkstra 2 | 3 | [![Unit Tests](https://github.com/albertorestifo/node-dijkstra/actions/workflows/test.yml/badge.svg)](https://github.com/albertorestifo/node-dijkstra/actions/workflows/test.yml) [![codecov.io](http://codecov.io/github/albertorestifo/node-dijkstra/coverage.svg?branch=master)](http://codecov.io/github/albertorestifo/node-dijkstra?branch=master) 4 | 5 | > Fast JavaScript implementation of the Dijkstra's shortest path problem for NodeJS 6 | 7 | ## Installation 8 | 9 | Since version 2 this plugin uses some ES6 features. You can run the latest version on **NodeJS `v4.0.0` or newer** 10 | 11 | ```shell 12 | npm install node-dijkstra --save 13 | ``` 14 | 15 | ### NodeJS prior `v4.0.0` 16 | 17 | On versions of NodeJS prior `v4.0.0`, although less performant, it's safe to use the version `1.1.3` that you can install as follows: 18 | 19 | ```shell 20 | npm install node-dijkstra@1.1.3 --save 21 | ``` 22 | 23 | You can then refer to the [`v1.1.3` documentation](https://github.com/albertorestifo/node-dijkstra/blob/v1.1.3/README.md#api) 24 | 25 | ## Usage 26 | 27 | Basic example: 28 | 29 | ```js 30 | const Graph = require("node-dijkstra"); 31 | 32 | const route = new Graph(); 33 | 34 | route.addNode("A", { B: 1 }); 35 | route.addNode("B", { A: 1, C: 2, D: 4 }); 36 | route.addNode("C", { B: 2, D: 1 }); 37 | route.addNode("D", { C: 1, B: 4 }); 38 | 39 | route.path("A", "D"); // => [ 'A', 'B', 'C', 'D' ] 40 | ``` 41 | 42 | ## API 43 | 44 | ### `Graph([nodes])` 45 | 46 | #### Parameters 47 | 48 | - `Object|Map nodes` _optional_: Initial nodes graph. 49 | 50 | A nodes graph must follow this structure: 51 | 52 | ``` 53 | { 54 | node: { 55 | neighbor: cost Number 56 | } 57 | } 58 | ``` 59 | 60 | ```js 61 | { 62 | 'A': { 63 | 'B': 1 64 | }, 65 | 'B': { 66 | 'A': 1, 67 | 'C': 2, 68 | 'D': 4 69 | } 70 | } 71 | ``` 72 | 73 | #### Example 74 | 75 | ```js 76 | const route = new Graph(); 77 | 78 | // or with pre-populated graph 79 | const route = new Graph({ 80 | A: { B: 1 }, 81 | B: { A: 1, C: 2, D: 4 }, 82 | }); 83 | ``` 84 | 85 | It's possible to pass the constructor a deep `Map`. This allows using numbers as keys for the nodes. 86 | 87 | ```js 88 | const graph = new Map(); 89 | 90 | const a = new Map(); 91 | a.set("B", 1); 92 | 93 | const b = new Map(); 94 | b.set("A", 1); 95 | b.set("C", 2); 96 | b.set("D", 4); 97 | 98 | graph.set("A", a); 99 | graph.set("B", b); 100 | 101 | const route = new Graph(graph); 102 | ``` 103 | 104 | ### `Graph#addNode(name, edges)` 105 | 106 | Add a node to the nodes graph 107 | 108 | #### Parameters 109 | 110 | - `String name`: name of the node 111 | - `Object|Map edges`: object or `Map` containing the name of the neighboring nodes and their cost 112 | 113 | #### Returns 114 | 115 | Returns `this` allowing chained calls. 116 | 117 | ```js 118 | const route = new Graph(); 119 | 120 | route.addNode("A", { B: 1 }); 121 | 122 | // chaining is possible 123 | route.addNode("B", { A: 1 }).addNode("C", { A: 3 }); 124 | 125 | // passing a Map directly is possible 126 | const c = new Map(); 127 | c.set("A", 4); 128 | 129 | route.addNode("C", c); 130 | ``` 131 | 132 | ### `Graph#removeNode(name)` 133 | 134 | Removes a node and all its references from the graph 135 | 136 | #### Parameters 137 | 138 | - `String name`: name of the node to remove 139 | 140 | #### Returns 141 | 142 | Returns `this` allowing chained calls. 143 | 144 | ```js 145 | const route = new Graph({ 146 | a: { b: 3, c: 10 }, 147 | b: { a: 5, c: 2 }, 148 | c: { b: 1 }, 149 | }); 150 | 151 | route.removeNode("c"); 152 | // => The graph now is: 153 | // { 154 | // a: { b: 3 }, 155 | // b: { a: 5 }, 156 | // } 157 | ``` 158 | 159 | ### `Graph#path(start, goal [, options])` 160 | 161 | #### Parameters 162 | 163 | - `String start`: Name of the starting node 164 | - `String goal`: Name of out goal node 165 | - `Object options` _optional_: Addittional options: 166 | - `Boolean trim`, default `false`: If set to true, the result won't include the start and goal nodes 167 | - `Boolean reverse`, default `false`: If set to true, the result will be in reverse order, from goal to start 168 | - `Boolean cost`, default `false`: If set to true, an object will be returned with the following keys: 169 | - `Array path`: Computed path (subject to other options) 170 | - `Number cost`: Total cost for the found path 171 | - `Array avoid`, default `[]`: Nodes to be avoided 172 | 173 | #### Returns 174 | 175 | If `options.cost` is `false` (default behaviour) an `Array` will be returned, containing the name of the crossed nodes. By default it will be ordered from start to goal, and those nodes will also be included. This behaviour can be changes with `options.trim` and `options.reverse` (see above) 176 | 177 | If `options.cost` is `true`, an `Object` with keys `path` and `cost` will be returned. `path` follows the same rules as above and `cost` is the total cost of the found route between nodes. 178 | 179 | When to route can be found, the path will be set to `null`. 180 | 181 | ```js 182 | const Graph = require("node-dijkstra"); 183 | 184 | const route = new Graph(); 185 | 186 | route.addNode("A", { B: 1 }); 187 | route.addNode("B", { A: 1, C: 2, D: 4 }); 188 | route.addNode("C", { B: 2, D: 1 }); 189 | route.addNode("D", { C: 1, B: 4 }); 190 | 191 | route.path("A", "D"); // => ['A', 'B', 'C', 'D'] 192 | 193 | // trimmed 194 | route.path("A", "D", { trim: true }); // => [B', 'C'] 195 | 196 | // reversed 197 | route.path("A", "D", { reverse: true }); // => ['D', 'C', 'B', 'A'] 198 | 199 | // include the cost 200 | route.path("A", "D", { cost: true }); 201 | // => { 202 | // path: [ 'A', 'B', 'C', 'D' ], 203 | // cost: 4 204 | // } 205 | ``` 206 | 207 | ## Upgrading from `v1` 208 | 209 | - The `v2` release in not compatible with NodeJS prior to the version 4.0 210 | - The method `Graph#shortestPath` has been deprecated, use `Graph#path` instead 211 | - The method `Graph#addVertex` has been deprecated, use `Graph#addNode` instead 212 | 213 | ## Releases 214 | 215 | This package uses automated releases to NPM via GitHub Actions. When a new [GitHub release](https://github.com/albertorestifo/node-dijkstra/releases) is published, the package is automatically published to NPM after running the test suite. 216 | 217 | To create a new release: 218 | 1. Update the version in `package.json` 219 | 2. Create a new [GitHub release](https://github.com/albertorestifo/node-dijkstra/releases/new) with a tag matching the version 220 | 3. The GitHub Actions workflow will automatically publish to NPM 221 | 222 | ## Testing 223 | 224 | ```shell 225 | npm test 226 | ``` 227 | 228 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 229 | 230 | [1]: https://github.com/andrewhayward/dijkstra 231 | -------------------------------------------------------------------------------- /libs/Graph.js: -------------------------------------------------------------------------------- 1 | const Queue = require("./PriorityQueue"); 2 | const removeDeepFromMap = require("./removeDeepFromMap"); 3 | const toDeepMap = require("./toDeepMap"); 4 | const validateDeep = require("./validateDeep"); 5 | 6 | /** Creates and manages a graph */ 7 | class Graph { 8 | /** 9 | * Creates a new Graph, optionally initializing it a nodes graph representation. 10 | * 11 | * A graph representation is an object that has as keys the name of the point and as values 12 | * the points reacheable from that node, with the cost to get there: 13 | * 14 | * { 15 | * node (Number|String): { 16 | * neighbor (Number|String): cost (Number), 17 | * ..., 18 | * }, 19 | * } 20 | * 21 | * In alternative to an object, you can pass a `Map` of `Map`. This will 22 | * allow you to specify numbers as keys. 23 | * 24 | * @param {Objec|Map} [graph] - Initial graph definition 25 | * @example 26 | * 27 | * const route = new Graph(); 28 | * 29 | * // Pre-populated graph 30 | * const route = new Graph({ 31 | * A: { B: 1 }, 32 | * B: { A: 1, C: 2, D: 4 }, 33 | * }); 34 | * 35 | * // Passing a Map 36 | * const g = new Map() 37 | * 38 | * const a = new Map() 39 | * a.set('B', 1) 40 | * 41 | * const b = new Map() 42 | * b.set('A', 1) 43 | * b.set('C', 2) 44 | * b.set('D', 4) 45 | * 46 | * g.set('A', a) 47 | * g.set('B', b) 48 | * 49 | * const route = new Graph(g) 50 | */ 51 | constructor(graph) { 52 | if (graph instanceof Map) { 53 | validateDeep(graph); 54 | this.graph = graph; 55 | } else if (graph) { 56 | this.graph = toDeepMap(graph); 57 | } else { 58 | this.graph = new Map(); 59 | } 60 | } 61 | 62 | /** 63 | * Adds a node to the graph 64 | * 65 | * @param {string} name - Name of the node 66 | * @param {Object|Map} neighbors - Neighbouring nodes and cost to reach them 67 | * @return {this} 68 | * @example 69 | * 70 | * const route = new Graph(); 71 | * 72 | * route.addNode('A', { B: 1 }); 73 | * 74 | * // It's possible to chain the calls 75 | * route 76 | * .addNode('B', { A: 1 }) 77 | * .addNode('C', { A: 3 }); 78 | * 79 | * // The neighbors can be expressed in a Map 80 | * const d = new Map() 81 | * d.set('A', 2) 82 | * d.set('B', 8) 83 | * 84 | * route.addNode('D', d) 85 | */ 86 | addNode(name, neighbors) { 87 | let nodes; 88 | if (neighbors instanceof Map) { 89 | validateDeep(neighbors); 90 | nodes = neighbors; 91 | } else { 92 | nodes = toDeepMap(neighbors); 93 | } 94 | 95 | this.graph.set(name, nodes); 96 | 97 | return this; 98 | } 99 | 100 | /** 101 | * @deprecated since version 2.0, use `Graph#addNode` instead 102 | */ 103 | addVertex(name, neighbors) { 104 | return this.addNode(name, neighbors); 105 | } 106 | 107 | /** 108 | * Removes a node and all of its references from the graph 109 | * 110 | * @param {string|number} key - Key of the node to remove from the graph 111 | * @return {this} 112 | * @example 113 | * 114 | * const route = new Graph({ 115 | * A: { B: 1, C: 5 }, 116 | * B: { A: 3 }, 117 | * C: { B: 2, A: 2 }, 118 | * }); 119 | * 120 | * route.removeNode('C'); 121 | * // The graph now is: 122 | * // { A: { B: 1 }, B: { A: 3 } } 123 | */ 124 | removeNode(key) { 125 | this.graph = removeDeepFromMap(this.graph, key); 126 | 127 | return this; 128 | } 129 | 130 | /** 131 | * Compute the shortest path between the specified nodes 132 | * 133 | * @param {string} start - Starting node 134 | * @param {string} goal - Node we want to reach 135 | * @param {object} [options] - Options 136 | * 137 | * @param {boolean} [options.trim] - Exclude the origin and destination nodes from the result 138 | * @param {boolean} [options.reverse] - Return the path in reversed order 139 | * @param {boolean} [options.cost] - Also return the cost of the path when set to true 140 | * 141 | * @return {array|object} Computed path between the nodes. 142 | * 143 | * When `option.cost` is set to true, the returned value will be an object with shape: 144 | * - `path` *(Array)*: Computed path between the nodes 145 | * - `cost` *(Number)*: Cost of the path 146 | * 147 | * @example 148 | * 149 | * const route = new Graph() 150 | * 151 | * route.addNode('A', { B: 1 }) 152 | * route.addNode('B', { A: 1, C: 2, D: 4 }) 153 | * route.addNode('C', { B: 2, D: 1 }) 154 | * route.addNode('D', { C: 1, B: 4 }) 155 | * 156 | * route.path('A', 'D') // => ['A', 'B', 'C', 'D'] 157 | * 158 | * // trimmed 159 | * route.path('A', 'D', { trim: true }) // => [B', 'C'] 160 | * 161 | * // reversed 162 | * route.path('A', 'D', { reverse: true }) // => ['D', 'C', 'B', 'A'] 163 | * 164 | * // include the cost 165 | * route.path('A', 'D', { cost: true }) 166 | * // => { 167 | * // path: [ 'A', 'B', 'C', 'D' ], 168 | * // cost: 4 169 | * // } 170 | */ 171 | path(start, goal, options = {}) { 172 | // Don't run when we don't have nodes set 173 | if (!this.graph.size) { 174 | if (options.cost) return { path: null, cost: 0 }; 175 | 176 | return null; 177 | } 178 | 179 | const explored = new Set(); 180 | const frontier = new Queue(); 181 | const previous = new Map(); 182 | 183 | let path = []; 184 | let totalCost = 0; 185 | 186 | let avoid = []; 187 | if (options.avoid) avoid = [].concat(options.avoid); 188 | 189 | if (avoid.includes(start)) { 190 | throw new Error(`Starting node (${start}) cannot be avoided`); 191 | } else if (avoid.includes(goal)) { 192 | throw new Error(`Ending node (${goal}) cannot be avoided`); 193 | } 194 | 195 | // Add the starting point to the frontier, it will be the first node visited 196 | frontier.set(start, 0); 197 | 198 | // Run until we have visited every node in the frontier 199 | while (!frontier.isEmpty()) { 200 | // Get the node in the frontier with the lowest cost (`priority`) 201 | const node = frontier.next(); 202 | 203 | // When the node with the lowest cost in the frontier in our goal node, 204 | // we can compute the path and exit the loop 205 | if (node.key === goal) { 206 | // Set the total cost to the current value 207 | totalCost = node.priority; 208 | 209 | let nodeKey = node.key; 210 | while (previous.has(nodeKey)) { 211 | path.push(nodeKey); 212 | nodeKey = previous.get(nodeKey); 213 | } 214 | 215 | break; 216 | } 217 | 218 | // Add the current node to the explored set 219 | explored.add(node.key); 220 | 221 | // Loop all the neighboring nodes 222 | const neighbors = this.graph.get(node.key) || new Map(); 223 | neighbors.forEach((nCost, nNode) => { 224 | // If we already explored the node, or the node is to be avoided, skip it 225 | if (explored.has(nNode) || avoid.includes(nNode)) return null; 226 | 227 | // If the neighboring node is not yet in the frontier, we add it with 228 | // the correct cost 229 | if (!frontier.has(nNode)) { 230 | previous.set(nNode, node.key); 231 | return frontier.set(nNode, node.priority + nCost); 232 | } 233 | 234 | const frontierPriority = frontier.get(nNode).priority; 235 | const nodeCost = node.priority + nCost; 236 | 237 | // Otherwise we only update the cost of this node in the frontier when 238 | // it's below what's currently set 239 | if (nodeCost < frontierPriority) { 240 | previous.set(nNode, node.key); 241 | return frontier.set(nNode, nodeCost); 242 | } 243 | 244 | return null; 245 | }); 246 | } 247 | 248 | // Return null when no path can be found 249 | if (!path.length) { 250 | if (options.cost) return { path: null, cost: 0 }; 251 | 252 | return null; 253 | } 254 | 255 | // From now on, keep in mind that `path` is populated in reverse order, 256 | // from destination to origin 257 | 258 | // Remove the first value (the goal node) if we want a trimmed result 259 | if (options.trim) { 260 | path.shift(); 261 | } else { 262 | // Add the origin waypoint at the end of the array 263 | path = path.concat([start]); 264 | } 265 | 266 | // Reverse the path if we don't want it reversed, so the result will be 267 | // from `start` to `goal` 268 | if (!options.reverse) { 269 | path = path.reverse(); 270 | } 271 | 272 | // Return an object if we also want the cost 273 | if (options.cost) { 274 | return { 275 | path, 276 | cost: totalCost, 277 | }; 278 | } 279 | 280 | return path; 281 | } 282 | 283 | /** 284 | * @deprecated since version 2.0, use `Graph#path` instead 285 | */ 286 | shortestPath(...args) { 287 | return this.path(...args); 288 | } 289 | } 290 | 291 | module.exports = Graph; 292 | -------------------------------------------------------------------------------- /test/Graph.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | /* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */ 3 | 4 | require("must"); 5 | const demand = require("must"); 6 | const sinon = require("sinon"); 7 | 8 | const Graph = require("../libs/Graph"); 9 | 10 | describe("Graph", () => { 11 | describe("#constructor", () => { 12 | it("creates an instance of Graph", () => { 13 | const graph = new Graph(); 14 | 15 | demand(graph).be.instanceOf(Graph); 16 | graph.must.be.instanceOf(Graph); 17 | }); 18 | 19 | it("populates the vertices with the passed point", () => { 20 | const route = new Graph({ 21 | a: { b: 1, c: 2 }, 22 | }); 23 | 24 | demand(route.graph).be.instanceOf(Map); 25 | demand(route.graph.size).equal(1); 26 | 27 | const a = route.graph.get("a"); 28 | demand(a).be.instanceOf(Map); 29 | }); 30 | 31 | it("accepts a Map as graph", () => { 32 | const graph = new Map(); 33 | const a = new Map(); 34 | a.set("b", 1); 35 | a.set("c", 2); 36 | graph.set("a", a); 37 | 38 | const route = new Graph(graph); 39 | 40 | demand(route.graph.size).equal(1); 41 | }); 42 | 43 | it("accepts numbers as key when in a map", () => { 44 | const graph = new Map(); 45 | const a = new Map(); 46 | a.set(1, 1); 47 | a.set(2, 2); 48 | graph.set(1, a); 49 | 50 | const route = new Graph(graph); 51 | 52 | demand(route.graph.size).equal(1); 53 | }); 54 | }); 55 | 56 | describe("#addNode()", () => { 57 | it("adds a vertex", () => { 58 | const route = new Graph(); 59 | 60 | route.addNode("a", { b: 10, c: 20 }); 61 | 62 | const node = route.graph.get("a"); 63 | 64 | demand(node).be.instanceOf(Map); 65 | demand(node.get("b")).equal(10); 66 | demand(node.get("c")).equal(20); 67 | }); 68 | 69 | it("returns the Graph object", () => { 70 | const graph = new Graph(); 71 | 72 | demand(graph.addNode("a", { b: 10, c: 20 })).be.instanceOf(Graph); 73 | }); 74 | 75 | it("accepts a map", () => { 76 | const route = new Graph(); 77 | const a = new Map(); 78 | a.set("b", 10); 79 | a.set("c", 20); 80 | 81 | route.addNode("a", a); 82 | 83 | const node = route.graph.get("a"); 84 | 85 | demand(node).be.instanceOf(Map); 86 | demand(node.get("b")).equal(10); 87 | demand(node.get("c")).equal(20); 88 | }); 89 | }); 90 | 91 | describe("#addNode()", () => { 92 | it("is alias of Graph#addVertex()", () => { 93 | const route = new Graph(); 94 | const spy = sinon.spy(route, "addNode"); 95 | 96 | route.addVertex("a", { b: 1 }); 97 | 98 | sinon.assert.calledOnce(spy); 99 | sinon.assert.alwaysCalledOn(spy, route); 100 | }); 101 | }); 102 | 103 | describe("#path()", () => { 104 | const vertices = { 105 | a: { b: 20, c: 80 }, 106 | b: { a: 20, c: 20 }, 107 | c: { a: 80, b: 20 }, 108 | }; 109 | 110 | it("retuns the shortest path", () => { 111 | const route = new Graph(vertices); 112 | 113 | const path = route.path("a", "c"); 114 | 115 | path.must.eql(["a", "b", "c"]); 116 | }); 117 | 118 | it("retuns an object containing the cost", () => { 119 | const route = new Graph(vertices); 120 | 121 | const res = route.path("a", "c", { cost: true }); 122 | 123 | res.must.be.object(); 124 | res.path.must.eql(["a", "b", "c"]); 125 | res.cost.must.equal(40); 126 | }); 127 | 128 | it("retuns the inverted path", () => { 129 | const route = new Graph(vertices); 130 | 131 | const path = route.path("a", "c", { reverse: true }); 132 | 133 | path.must.eql(["c", "b", "a"]); 134 | }); 135 | 136 | it("retuns an object containing the cost and inverted path", () => { 137 | const route = new Graph(vertices); 138 | 139 | const res = route.path("a", "c", { cost: true, reverse: true }); 140 | 141 | res.must.be.object(); 142 | res.path.must.eql(["c", "b", "a"]); 143 | res.cost.must.equal(40); 144 | }); 145 | 146 | it("retuns the trimmed path", () => { 147 | const route = new Graph(vertices); 148 | 149 | const path = route.path("a", "c", { trim: true }); 150 | 151 | path.must.eql(["b"]); 152 | }); 153 | 154 | it("retuns an object containing the cost and trimmed path", () => { 155 | const route = new Graph(vertices); 156 | 157 | const res = route.path("a", "c", { cost: true, trim: true }); 158 | 159 | res.must.be.object(); 160 | res.path.must.eql(["b"]); 161 | res.cost.must.equal(40); 162 | }); 163 | 164 | it("retuns the reverse and trimmed path", () => { 165 | const route = new Graph(vertices); 166 | 167 | const path = route.path("a", "c", { trim: true }); 168 | 169 | path.must.eql(["b"]); 170 | }); 171 | 172 | it("retuns an object containing the cost and inverted and trimmed path", () => { 173 | const route = new Graph(vertices); 174 | 175 | const res = route.path("a", "c", { 176 | cost: true, 177 | reverse: true, 178 | trim: true, 179 | }); 180 | 181 | res.must.be.object(); 182 | res.path.must.eql(["b"]); 183 | res.cost.must.equal(40); 184 | }); 185 | 186 | it("returns null when no path is found", () => { 187 | const route = new Graph(vertices); 188 | 189 | const path = route.path("a", "d"); 190 | 191 | demand(path).be.null(); 192 | }); 193 | 194 | it("returns null as path and 0 as cost when no path exists and we want the cost", () => { 195 | const route = new Graph(vertices); 196 | 197 | const res = route.path("a", "d", { cost: true }); 198 | 199 | demand(res.path).be.null(); 200 | res.cost.must.equal(0); 201 | }); 202 | 203 | it("returns null when no vertices are defined", () => { 204 | const route = new Graph(); 205 | 206 | const path = route.path("a", "d"); 207 | 208 | demand(path).be.null(); 209 | }); 210 | 211 | it("returns null as path and 0 as cost when no vertices are defined and we want the cost", () => { 212 | const route = new Graph(); 213 | 214 | const res = route.path("a", "d", { cost: true }); 215 | 216 | demand(res.path).be.null(); 217 | res.cost.must.equal(0); 218 | }); 219 | 220 | it("returns the same path if a node which is not part of the shortest path is avoided", () => { 221 | const route = new Graph({ 222 | a: { b: 1 }, 223 | b: { a: 1, c: 1 }, 224 | c: { b: 1, d: 1 }, 225 | d: { c: 1 }, 226 | }); 227 | 228 | const path = route.path("a", "c", { cost: true }); 229 | const path2 = route.path("a", "c", { avoid: ["d"], cost: true }); 230 | 231 | path.must.eql(path2); 232 | }); 233 | 234 | it("returns a different path if a node which is part of the shortest path is avoided", () => { 235 | const route = new Graph({ 236 | a: { b: 1, c: 50 }, 237 | b: { a: 1, c: 1 }, 238 | c: { a: 50, b: 1, d: 1 }, 239 | d: { c: 1 }, 240 | }); 241 | 242 | const res = route.path("a", "d", { cost: true }); 243 | const res2 = route.path("a", "d", { avoid: ["b"], cost: true }); 244 | 245 | res.path.must.not.eql(res.path2); 246 | res.cost.must.be.at.most(res2.cost); 247 | }); 248 | 249 | it("throws an error if the start node is avoided", () => { 250 | const route = new Graph(vertices); 251 | 252 | demand(() => route.path("a", "c", { avoid: ["a"] })).throw(Error); 253 | }); 254 | 255 | it("throws an error if the end node is avoided", () => { 256 | const route = new Graph(vertices); 257 | 258 | demand(() => route.path("a", "c", { avoid: ["c"] })).throw(Error); 259 | }); 260 | 261 | it("returns the same path and cost if a node which is not part of the graph is avoided", () => { 262 | const route = new Graph(vertices); 263 | 264 | const res = route.path("a", "c", { cost: true }); 265 | const res2 = route.path("a", "c", { avoid: ["z"], cost: true }); 266 | 267 | res.path.must.eql(res2.path); 268 | res.cost.must.equal(res2.cost); 269 | }); 270 | 271 | it("works with a more complicated graph", () => { 272 | const route = new Graph({ 273 | a: { b: 7, c: 9, f: 14 }, 274 | b: { c: 10, d: 15 }, 275 | c: { d: 11, f: 2 }, 276 | d: { e: 6 }, 277 | e: { f: 9 }, 278 | }); 279 | 280 | const path = route.path("a", "e"); 281 | 282 | path.must.eql(["a", "c", "d", "e"]); 283 | }); 284 | }); 285 | 286 | describe("#shortestPath()", () => { 287 | it("is an alias of path", () => { 288 | const route = new Graph({ 289 | a: { b: 20, c: 80 }, 290 | b: { a: 20, c: 20 }, 291 | c: { a: 80, b: 20 }, 292 | }); 293 | 294 | sinon.spy(route, "path"); 295 | 296 | route.shortestPath("a", "c"); 297 | sinon.assert.calledOnce(route.path); 298 | }); 299 | }); 300 | 301 | describe("#removeNode()", () => { 302 | it("removes a previously set node from the graph", () => { 303 | const route = new Graph({ 304 | a: { b: 20, c: 80 }, 305 | b: { a: 20, c: 20 }, 306 | c: { a: 80, b: 20 }, 307 | }); 308 | 309 | route.removeNode("c"); 310 | 311 | route.graph.has("c").must.be.false(); 312 | route.graph.has("a").must.be.true(); 313 | route.graph.has("b").must.be.true(); 314 | }); 315 | 316 | it("removes all references to the removed node", () => { 317 | const route = new Graph({ 318 | a: { b: 20, c: 80 }, 319 | b: { a: 20, c: 20 }, 320 | c: { a: 80, b: 20 }, 321 | }); 322 | 323 | route.removeNode("c"); 324 | 325 | route.graph.has("c").must.be.false(); 326 | route.graph.get("b").has("c").must.be.false(); 327 | route.graph.get("a").has("c").must.be.false(); 328 | }); 329 | }); 330 | }); 331 | --------------------------------------------------------------------------------