Skip to content

Commit

Permalink
Merge pull request #107 from ravann/master
Browse files Browse the repository at this point in the history
weightFunction to calculate weight of path ...
  • Loading branch information
curran authored Feb 21, 2025
2 parents e1b0e1e + 3113074 commit 423d284
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 9 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ console.log(result.nodes); // Prints the array of nodes in the shortest path
console.log(result.weight); // Prints the total weight of the path
```

<a name="shortest-path" href="#shortest-path">#</a> <b>shortestPath</b>(<i>graph</i>, <i>sourceNode</i>, <i>destinationNode</i>, <i>nextWeightFn</i>)

Calculates the weight based on the custom function.

```javascript
import type { NextWeightFnParams } from '../../types.js';
function multiplyWeightFunction(wp: NextWeightFnParams): number {
if (wp.currentPathWeight === undefined) {
return wp.edgeWeight;
}
return wp.edgeWeight * wp.currentPathWeight;
}
var result = shortestPath(graph, 'a', 'c', multiplyWeightFunction);
console.log(result.nodes); // Prints the array of nodes in the shortest path
console.log(result.weight); // Prints the total weight of the path
```

<p align="center">
<a href="https://datavis.tech/">
<img src="https://cloud.githubusercontent.com/assets/68416/15298394/a7a0a66a-1bbc-11e6-9636-367bed9165fc.png">
Expand Down
25 changes: 22 additions & 3 deletions src/algorithms/shortestPath/getPath.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import type { EdgeWeight, NoInfer } from '../../types.js';
import type { TraversingTracks } from './types.js';
import type { NextWeightFnParams } from '../../types.js';

import { Graph } from '../../Graph.js';

/**
* Computes edge weight as the sum of all the edges in the path.
*/
export function addWeightFunction( wp: NextWeightFnParams): number {
if (wp.currentPathWeight === undefined) {
return wp.edgeWeight;
}
return wp.edgeWeight + wp.currentPathWeight;
}

/**
* Assembles the shortest path by traversing the
* predecessor subgraph from destination to source.
Expand All @@ -12,22 +23,30 @@ export function getPath<Node, LinkProps>(
tracks: TraversingTracks<NoInfer<Node>>,
source: NoInfer<Node>,
destination: NoInfer<Node>,
nextWeightFn: (params: NextWeightFnParams) => number = addWeightFunction
): {
nodes: [Node, Node, ...Node[]];
weight: number;
weight: number | undefined;
} {
const { p } = tracks;
const nodeList: Node[] & { weight?: EdgeWeight } = [];

let totalWeight = 0;
let totalWeight : EdgeWeight | undefined = undefined;
let node = destination;

let hop = 1;
while (p.has(node)) {
const currentNode = p.get(node)!;

nodeList.push(node);
totalWeight += graph.getEdgeWeight(currentNode, node);
const edgeWeight = graph.getEdgeWeight(currentNode, node)
totalWeight = nextWeightFn({
edgeWeight, currentPathWeight: totalWeight,
hop: hop, graph: graph, path: tracks,
previousNode: node, currentNode: currentNode
});
node = currentNode;
hop++;
}

if (node !== source) {
Expand Down
99 changes: 98 additions & 1 deletion src/algorithms/shortestPath/shortestPath.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { Graph } from '../../Graph.js';
import { serializeGraph } from '../../utils/serializeGraph.js';
import { shortestPath } from './shortestPath.js';
import { shortestPaths } from './shortestPaths.js';
import { addWeightFunction } from './getPath.js';
import { NextWeightFnParams } from '../../types.js';

describe("Dijkstra's Shortest Path Algorithm", function () {
it('Should compute shortest path on a single edge.', function () {
Expand Down Expand Up @@ -104,3 +106,98 @@ describe("Dijkstra's Shortest Path Algorithm", function () {
expect(postSerializedGraph.links).toContainEqual({ source: 'f', target: 'c' });
});
});

describe('addWeightFunction', () => {
it('should return edgeWeight if currentPathWeight is undefined', () => {
const graph = new Graph();
const params = {
edgeWeight: 5, currentPathWeight: undefined, hop: 1,
graph: graph, path: { d: new Map(), p: new Map(), q: new Set() },
previousNode: 'a', currentNode: 'b'
};
expect(addWeightFunction(params)).toBe(5);
});

it('should return the sum of edgeWeight and currentPathWeight', () => {
const graph = new Graph()
const params = { edgeWeight: 5, currentPathWeight: 10, hop: 1,
graph: graph, path: { d: new Map(), p: new Map(), q: new Set() },
previousNode: 'a', currentNode: 'b'
};
expect(addWeightFunction(params)).toBe(15);
});
});

describe('shortestPath with custom weight functions', () => {
it('should compute shortest path with default weight function (sum of weights)', () => {
const graph = new Graph().addEdge('a', 'b', 1).addEdge('b', 'c', 2);
expect(shortestPath(graph, 'a', 'c')).toEqual({
nodes: ['a', 'b', 'c'],
weight: 3,
});
});

it('should compute shortest path with a custom weight function', () => {
const customWeightFn = ({ edgeWeight, currentPathWeight, hop }: NextWeightFnParams) => {
if (currentPathWeight === undefined) {
return edgeWeight;
}
return currentPathWeight + edgeWeight ** hop;
};

const graph = new Graph().addEdge('a', 'b', 2).addEdge('b', 'c', 3);
expect(shortestPath(graph, 'a', 'c', customWeightFn)).toEqual({
nodes: ['a', 'b', 'c'],
weight: 7,
});
});

it('should pass correct parameters to custom weight function for a path with 3 nodes', () => {
const customWeightFn = vi.fn(({ edgeWeight, currentPathWeight, hop }: NextWeightFnParams) => {
if (currentPathWeight === undefined) {
return edgeWeight;
}
return currentPathWeight + edgeWeight ** hop;
});

const graph = new Graph().addEdge('a', 'b', 1).addEdge('b', 'c', 2);
shortestPath(graph, 'a', 'c', customWeightFn);

expect(customWeightFn).toHaveBeenCalledWith({ edgeWeight: 2, currentPathWeight: undefined, hop: 1,
graph: graph, currentNode: 'b', previousNode: 'c',
path: {
d: new Map([['a', 0], ['b', 1], ['c', 3]]),
p: new Map([['b', 'a'], ['c', 'b']]),
q: new Set(),
},
});
expect(customWeightFn).toHaveBeenCalledWith({ edgeWeight: 1, currentPathWeight: 2, hop: 2,
graph: graph, currentNode: 'a', previousNode: 'b',
path: {
d: new Map([['a', 0], ['b', 1], ['c', 3]]),
p: new Map([['b', 'a'], ['c', 'b']]),
q: new Set(),
}
});
});

it('should compute shortest path with a custom weight function in a graph with multiple paths', () => {
const customWeightFn = ({ edgeWeight, currentPathWeight }: NextWeightFnParams) => {
if (currentPathWeight === undefined) {
return edgeWeight;
}
return edgeWeight + currentPathWeight;
};

const graph = new Graph()
.addEdge('a', 'b', 1)
.addEdge('b', 'c', 2)
.addEdge('a', 'd', 1)
.addEdge('d', 'c', 1);

expect(shortestPath(graph, 'a', 'c', customWeightFn)).toEqual({
nodes: ['a', 'd', 'c'],
weight: 2,
});
});
});
8 changes: 5 additions & 3 deletions src/algorithms/shortestPath/shortestPath.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Graph } from '../../Graph.js';
import { NoInfer } from '../../types.js';
import { dijkstra } from './dijkstra.js';
import { getPath } from './getPath.js';
import { getPath, addWeightFunction } from './getPath.js';
import { TraversingTracks } from './types.js';
import type { NextWeightFnParams } from '../../types.js';

/**
* Dijkstra's Shortest Path Algorithm.
Expand All @@ -13,9 +14,10 @@ export function shortestPath<Node, LinkProps>(
graph: Graph<Node, LinkProps>,
source: NoInfer<Node>,
destination: NoInfer<Node>,
nextWeightFn: (params: NextWeightFnParams) => number = addWeightFunction
): {
nodes: [Node, Node, ...Node[]];
weight: number;
weight: number | undefined;
} {
const tracks: TraversingTracks<Node> = {
d: new Map(),
Expand All @@ -25,5 +27,5 @@ export function shortestPath<Node, LinkProps>(

dijkstra(graph, tracks, source, destination);

return getPath(graph, tracks, source, destination);
return getPath(graph, tracks, source, destination, nextWeightFn);
}
2 changes: 1 addition & 1 deletion src/algorithms/shortestPath/shortestPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function shortestPaths<Node, LinkProps>(

try {
path = shortestPath(graph, source, destination);
if (!path.weight || pathWeight < path.weight) break;
if (!path.weight || !pathWeight || pathWeight < path.weight) break;
paths.push(path);
} catch (e) {
break;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { Edge, Serialized, SerializedInput, EdgeWeight } from './types.js';
export type { Edge, Serialized, SerializedInput, EdgeWeight, NextWeightFnParams } from './types.js';

export { Graph } from './Graph.js';
export { CycleError } from './CycleError.js';
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { TraversingTracks } from './algorithms/shortestPath/types.js';
import { Graph } from './Graph.js';

export type EdgeWeight = number;

export type Edge<NodeIdentity = unknown, Props = unknown> = {
Expand All @@ -18,3 +21,13 @@ export type SerializedInput<Node = unknown, LinkProps = unknown> = {
};

export type NoInfer<T> = [T][T extends any ? 0 : never];

export type NextWeightFnParams<Node = unknown, LinkProps = unknown> = {
edgeWeight: EdgeWeight;
currentPathWeight: EdgeWeight | undefined;
hop: number;
graph: Graph<Node, LinkProps>;
path: TraversingTracks<NoInfer<Node>>;
previousNode: NoInfer<Node>;
currentNode: NoInfer<Node>;
};

0 comments on commit 423d284

Please sign in to comment.