Skip to content

Commit

Permalink
feat: add toposort extra (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
oljekechoro authored Jan 30, 2023
1 parent 84dec67 commit 613141e
Show file tree
Hide file tree
Showing 24 changed files with 7,137 additions and 285 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["eslint-config-qiwi"]
}
29 changes: 29 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/[email protected]
with:
fetch-depth: 0
- name: Setup Node
uses: actions/[email protected]
with:
node-version: 18
- name: Release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_USER: 'qiwibot'
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_AUTHOR_EMAIL: '[email protected]'
GIT_COMMITTER_EMAIL: '[email protected]'
GIT_AUTHOR_NAME: 'QIWI Bot'
GIT_COMMITTER_NAME: 'QIWI Bot'
YARN_ENABLE_IMMUTABLE_INSTALLS: false
run: npm_config_yes=true npx zx-semrel
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Linting and Unit Tests
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- uses: actions/[email protected]
with:
node-version: 18

- run: yarn --prefer-offline
- run: yarn verify
74 changes: 74 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Coverage
flow-coverage
lib-cov
coverage
coverage.*
.nyc_output
*.lcov

# Codeclimate
codeclimate.*
cc-reporter
cc-reporter.*

# Bundles
bundle/
build/
dist/
target/
typings/
flow-typed/
buildstamp.json

# Docs
docs
doc

# Deps
node_modules
jspm_packages
bower_components

#IDE
.idea
.idea/

# Yarn Integrity file
.yarn-integrity

.npm
.eslintcache
.node_repl_history
.env

# Typescript
*.tsbuildinfo
.tsbuildinfo
buildcache
.buildcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Creds
*.asc
*.key
*.pem
*.cert
140 changes: 139 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,141 @@
# @qiwi/toposort

Fork of [toposort](https://github.com/marcelklehr/toposort) with updated dependencies and some new features
Fork of [toposort](https://github.com/marcelklehr/toposort) with updated dependencies and some new features

## Why?

Toposort is wonderful, but we also need to know which parts of graph can be handled in parallel mode.

You can simultaneously handle unconnected parts (components in graph theory) of a graph.

Also, you can analyze dependencies and dependants of graph nodes to find independent nodes to parallelize their processing.

So, toposortExtra returns maps of dependencies and dependants and a list of graph components.

## Installation

```shell
yarn add @qiwi/toposort

npm i @qiwi/toposort
```

## Usage

### toposortExtra({ nodes, edges, throwOnCycle })

Returns an array of the graph components

![two-component-graph](./src/test/resources/graphs/two-component.svg)

The graph above is used in the code below.

```js
import { toposortExtra } from '@qiwi/toposort'

const res = toposortExtra({ edges: [[1, 3], [1, 2], [2, 4], [2, 5], [6, 7], [6, 8], [9, 8]] }) // see diagramm above

console.log(res)
/*
{
sources: [1, 6, 9], // nodes which do not have incoming edges, e.g. dependencies/parents
prev: new Map([ // map of dependencies
[1, []],
[3, [1]],
[2, [1]],
[4, [2]],
[5, [2]],
[6, []],
[7, [6]],
[8, [6, 9]],
[9, []],
]),
next: new Map([ // map of dependants
[1, [2, 3]],
[3, []],
[2, [4, 5]],
[4, []],
[5, []],
[6, [7, 8]],
[7, []],
[8, []],
[9, [8]],
]),
graphs: [ // list of graph components (unconnected parts)
{
nodes: [1, 2, 3, 4, 5], // list of component nodes
sources: [1] // list of component start nodes
},
{
nodes: [6, 7, 8, 9],
sources: [6, 9]
}
]
}
*/
```

The same result, but also checks edge nodes to be in the `nodes` list

```js
const res = toposortExtra({
nodes: [1, 2, 3, 4, 5, 6, 7, 8, 9],
edges: [[1, 3], [1, 2], [2, 4], [2, 5], [6, 7], [6, 8], [9, 8]]
})
console.log(res) // the same result
```

```js
const res = toposortExtra({
nodes: [1, 2, 3, 4, 6, 7, 8, 9],
edges: [[1, 3], [1, 2], [2, 4], [2, 5], [6, 7], [6, 8], [9, 8]]
}) // Uncaught Error: Unknown node. There is an unknown node in the supplied edges.
```

You can also check the graph to be acyclic

```js
toposortExtra({
edges: [[1, 2], [2, 3], [3, 1]],
throwOnCycle: true
}) // Uncaught Error: Cyclic dependency, node was:1
```


### toposort(edges)

Marcelklehr's original toposort

```js
import toposort from '@qiwi/toposort'

console.log(toposort([
[ '3', '2' ],
[ '2', '1' ],
[ '6', '5' ],
[ '5', '2' ],
[ '5', '4' ]
]
)) // [ '3', '6', '5', '2', '1', '4' ]
```

### array(nodes, edges)

Marcelklehr's original toposort.array.

Checks edge nodes for presence in the nodes array

```js
import { array } from '@qiwi/toposort'

console.log(array(
['1', '2', '3', '4', '5', '6'],
[
[ '3', '2' ],
[ '2', '1' ],
[ '6', '5' ],
[ '5', '2' ],
[ '5', '4' ]
]
)) // [ '3', '6', '5', '2', '1', '4' ]
```
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
"description": "Topological sort of directed ascyclic graphs (like dependecy lists)",
"main": "src/main/js/index.js",
"scripts": {
"test": "node src/test/js/index.js"
"style": "eslint src",
"style:fix": "yarn style --fix",
"verify": "yarn style && yarn test",
"test": "yarn test:unit",
"test:unit": "c8 -r html -r text -r lcov --exclude src/test uvu src/test/js"
},
"files": [
"src/main",
Expand All @@ -17,6 +21,10 @@
"url": "https://github.com/qiwi-forks/toposort.git"
},
"devDependencies": {
"c8": "^7.12.0",
"eslint": "^8.32.0",
"eslint-config-qiwi": "^2.0.8",
"typescript": "^4.9.4",
"uvu": "^0.5.6"
},
"keywords": [
Expand Down
39 changes: 39 additions & 0 deletions src/main/js/extra.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
uniqueNodes,
groupByComponents,
getStartNodes,
makeOutgoingEdges,
makeIncomingEdges,
} from './helpers.js'
import { validateEdges, validateArgs, validateDag } from './validators.js'

export function toposortExtra (opts) {
validateArgs(opts)

const nodes = opts.nodes || uniqueNodes(opts.edges)
const edges = opts.edges

validateEdges(nodes, edges)

const prev = new Map([...makeIncomingEdges(edges)].map(([node, neighborsSet]) => [node, [...neighborsSet]]))
const next = new Map([...makeOutgoingEdges(edges)].map(([node, neighborsSet]) => [node, [...neighborsSet]]))
const sources = getStartNodes(edges)

if (opts.throwOnCycle) {
validateDag({ edges, nodes, outgoing: next })
}

return {
sources,
prev,
next,
graphs: groupByComponents({ edges })
.map(graphNodesSet => {
return {
nodes: [...graphNodesSet],
sources: sources.filter(node => graphNodesSet.has(node))
}
})
}
}

Loading

0 comments on commit 613141e

Please sign in to comment.