Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add toposort extra #2

Merged
merged 15 commits into from
Jan 30, 2023
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
oljekechoro marked this conversation as resolved.
Show resolved Hide resolved
.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