Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> = {
Copy link
Contributor

@JesusTheHun JesusTheHun Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also add the "path" as parameter, that would allow to get the total number of hops for the cases when the calculation is based on the distance from the source.

Edit: actually, I think we should also add the previous node and the current node, so the properties of the link can be retrieved from the graph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done.

edgeWeight: EdgeWeight;
currentPathWeight: EdgeWeight | undefined;
hop: number;
graph: Graph<Node, LinkProps>;
path: TraversingTracks<NoInfer<Node>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"NoInfer" only matters in a function signature.

previousNode: NoInfer<Node>;
currentNode: NoInfer<Node>;
};