├── test ├── mocha.opts └── test.js ├── types ├── tslint.json ├── tsconfig.json ├── index.test.ts └── index.d.ts ├── .eslintrc.yaml ├── .travis.yml ├── .gitignore ├── .jshintrc ├── LICENSE ├── package.json ├── README.md └── index.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --bail 3 | --check-leaks 4 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: eslint:recommended 4 | env: 5 | browser: true 6 | node: true 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - npm install -g npm 4 | node_js: 5 | - node 6 | - lts/* 7 | - 10 8 | - 12 9 | - 14 10 | - 15 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | dist/** 18 | 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "immed": true, 7 | "indent": 2, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "nonew": true, 13 | "undef": true, 14 | "unused": true, 15 | "strict": true, 16 | "trailing": true, 17 | "maxlen": 120, 18 | "browser": true, 19 | "devel": true, 20 | "node": true, 21 | "white": true, 22 | "onevar": true, 23 | "globals": { 24 | "require": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es6" 6 | ], 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noEmit": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "flat-to-nested": [ 16 | "." 17 | ] 18 | } 19 | }, 20 | "files": [ 21 | "index.d.ts", 22 | "index.test.ts" 23 | ] 24 | } 25 | 26 | -------------------------------------------------------------------------------- /types/index.test.ts: -------------------------------------------------------------------------------- 1 | // Minimum TypeScript Version: 3.5 2 | import FlatToNested = require("flat-to-nested"); 3 | 4 | const flatToNested = new FlatToNested({}); 5 | 6 | interface SampleFlatType { 7 | id: number; 8 | parent?: number; 9 | } 10 | 11 | const sampleFlat: SampleFlatType[] = [ 12 | { id: 111, parent: 11 }, 13 | { id: 11, parent: 1 }, 14 | { id: 12, parent: 1 }, 15 | { id: 1 }, 16 | ]; 17 | 18 | // $ExpectType Nested 19 | flatToNested.convert(sampleFlat); 20 | 21 | /** all roots */ 22 | const sampleAllRootsFlat: SampleFlatType[] = [{ id: 111 }, { id: 11 }]; 23 | 24 | // $ExpectType Nested 25 | flatToNested.convert(sampleAllRootsFlat); 26 | 27 | // $ExpectType never 28 | flatToNested.convert([]); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 João Nuno Silva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Project: 2 | // Definitions by: Peter Muriuki 3 | // typescript version: 3.8.3 4 | 5 | export = FlatToNested; 6 | 7 | /** 8 | * Create a new FlatToNested object. 9 | * 10 | * @param config The configuration object. 11 | */ 12 | declare class FlatToNested { 13 | constructor(config: FlatToNested.Config); 14 | 15 | private config: FlatToNested.Config; 16 | 17 | /** 18 | * Convert a hierarchy from flat to nested representation. 19 | * 20 | * @param flat The array with the hierarchy flat representation. 21 | */ 22 | convert(flat: T[]): FlatToNested.Nested; 23 | } 24 | 25 | declare namespace FlatToNested { 26 | interface Dictionary { 27 | [key: string]: T; 28 | } 29 | interface ConfigOptions { 30 | deleteParent: boolean; 31 | } 32 | 33 | interface Config { 34 | id?: string; 35 | parent?: string; 36 | children?: string; 37 | options?: ConfigOptions; 38 | } 39 | 40 | interface RecursiveObjectTree { 41 | [children: string]: RecursiveObjectTree & T; 42 | } 43 | 44 | type Nested = RecursiveObjectTree & T; 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flat-to-nested", 3 | "version": "1.1.1", 4 | "description": "Convert a hierarchy from flat to nested representation.", 5 | "keywords": [ 6 | "tree", 7 | "hierarchy", 8 | "flat", 9 | "nested", 10 | "transform", 11 | "parent", 12 | "child" 13 | ], 14 | "homepage": "https://github.com/joaonuno/flat-to-nested-js", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/joaonuno/flat-to-nested-js.git" 18 | }, 19 | "license": "MIT", 20 | "author": "João Nuno Silva (http://jnuno.com)", 21 | "main": "index.js", 22 | "scripts": { 23 | "test": "mocha", 24 | "lint": "jshint index.js test/test.js && eslint index.js test/test.js && npm run dtslint", 25 | "dist": "mkdir -p dist && browserify index.js -o dist/FlatToNested.js -s FlatToNested && uglifyjs dist/FlatToNested.js > dist/FlatToNested-min.js", 26 | "dtslint": "dtslint types" 27 | }, 28 | "devDependencies": { 29 | "browserify": "*", 30 | "chai": "*", 31 | "dtslint": "*", 32 | "eslint": "*", 33 | "jshint": "*", 34 | "mocha": "*", 35 | "typescript": "*", 36 | "uglify-js": "*" 37 | }, 38 | "types": "types" 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flat-to-nested 2 | ============== 3 | 4 | Convert a hierarchy from flat to nested representation. 5 | 6 | [![Build Status](https://travis-ci.org/joaonuno/flat-to-nested-js.svg)](https://travis-ci.org/joaonuno/flat-to-nested-js) 7 | 8 | ## Example 9 | 10 | ```js 11 | var FlatToNested, flatToNested, flat; 12 | 13 | FlatToNested = require('flat-to-nested'); 14 | flatToNested = new FlatToNested( /* can take a config object to use other property names */ ); 15 | 16 | flat = [ 17 | {id: 111, parent: 11}, 18 | {id: 11, parent: 1}, 19 | {id: 12, parent: 1}, 20 | {id: 1} 21 | ]; 22 | 23 | var nested = flatToNested.convert(flat); 24 | console.log(nested); 25 | 26 | // { 27 | // id: 1, 28 | // children: [ 29 | // { 30 | // id: 11, 31 | // children: [ 32 | // { 33 | // id: 111 34 | // } 35 | // ] 36 | // }, 37 | // { 38 | // id: 12 39 | // } 40 | // ] 41 | // } 42 | ``` 43 | 44 | ## Configuration 45 | 46 | The constructor accepts an optional object with some or all of these properties: 47 | 48 | ```js 49 | flatToNested = new FlatToNested({ 50 | // The name of the property with the node id in the flat representation 51 | id: 'id', 52 | // The name of the property with the parent node id in the flat representation 53 | parent: 'parent', 54 | // The name of the property that will hold the children nodes in the nested representation 55 | children: 'children' 56 | }}); 57 | 58 | ``` 59 | 60 | ## Contributing 61 | 62 | ### Setup 63 | 64 | Fork this repository and run `npm install` on the project root folder to make sure you have all project dependencies installed. 65 | 66 | ### Code Linting 67 | 68 | Run `npm run lint` 69 | 70 | This will check both source and tests for code correctness and style compliance. 71 | 72 | ### Running Tests 73 | 74 | Run `npm test` 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = (function () { 2 | 'use strict'; 3 | 4 | function initPush(arrayName, obj, toPush) { 5 | if (obj[arrayName] === undefined) { 6 | obj[arrayName] = []; 7 | } 8 | obj[arrayName].push(toPush); 9 | } 10 | 11 | function multiInitPush(arrayName, obj, toPushArray) { 12 | var len; 13 | len = toPushArray.length; 14 | if (obj[arrayName] === undefined) { 15 | obj[arrayName] = []; 16 | } 17 | while (len-- > 0) { 18 | obj[arrayName].push(toPushArray.shift()); 19 | } 20 | } 21 | 22 | /** 23 | * Create a new FlatToNested object. 24 | * 25 | * @constructor 26 | * @param {object} config The configuration object. 27 | */ 28 | function FlatToNested(config) { 29 | this.config = config = config || {}; 30 | this.config.id = config.id || 'id'; 31 | this.config.parent = config.parent || 'parent'; 32 | this.config.children = config.children || 'children'; 33 | this.config.options = config.options || { deleteParent: true }; 34 | } 35 | 36 | /** 37 | * Convert a hierarchy from flat to nested representation. 38 | * 39 | * @param {array} flat The array with the hierachy flat representation. 40 | */ 41 | FlatToNested.prototype.convert = function (flat) { 42 | var i, len, temp, roots, id, parent, nested, pendingChildOf, flatEl; 43 | i = 0; 44 | roots = []; 45 | temp = {}; 46 | pendingChildOf = {}; 47 | 48 | for (i, len = flat.length; i < len; i++) { 49 | flatEl = flat[i]; 50 | id = flatEl[this.config.id]; 51 | parent = flatEl[this.config.parent]; 52 | temp[id] = flatEl; 53 | if (parent === undefined || parent === null) { 54 | // Current object has no parent, so it's a root element. 55 | roots.push(flatEl); 56 | } else { 57 | if (temp[parent] !== undefined) { 58 | // Parent is already in temp, adding the current object to its children array. 59 | initPush(this.config.children, temp[parent], flatEl); 60 | } else { 61 | // Parent for this object is not yet in temp, adding it to pendingChildOf. 62 | initPush(parent, pendingChildOf, flatEl); 63 | } 64 | if (this.config.options.deleteParent) { 65 | delete flatEl[this.config.parent]; 66 | } 67 | } 68 | if (pendingChildOf[id] !== undefined) { 69 | // Current object has children pending for it. Adding these to the object. 70 | multiInitPush(this.config.children, flatEl, pendingChildOf[id]); 71 | } 72 | } 73 | 74 | if (roots.length === 1) { 75 | nested = roots[0]; 76 | } else if (roots.length > 1) { 77 | nested = {}; 78 | nested[this.config.children] = roots; 79 | } else { 80 | nested = {}; 81 | } 82 | return nested; 83 | }; 84 | 85 | return FlatToNested; 86 | })(); 87 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | var chai, assert, FlatToNested; 4 | chai = require('chai'); 5 | FlatToNested = require('..'); 6 | assert = chai.assert; 7 | chai.Assertion.includeStack = true; 8 | 9 | describe('flatToNested', function () { 10 | 'use strict'; 11 | 12 | describe('using default configuration', function () { 13 | var flatToNested; 14 | 15 | flatToNested = new FlatToNested(); 16 | 17 | it('should convert an empty array to an empty object', function () { 18 | assert.deepEqual(flatToNested.convert([]), {}); 19 | }); 20 | 21 | it('should convert a one element array to an object without children', function () { 22 | assert.deepEqual(flatToNested.convert([{id: 1, someKey: 'someValue'}]), {id: 1, someKey: 'someValue'}); 23 | }); 24 | 25 | it('should convert when the parents come before the children and there is a root', function () { 26 | var flat, expected, actual; 27 | 28 | flat = [{id: 1}, {id: 11, parent: 1}, {id: 12, parent: 1}, {id: 111, parent: 11}]; 29 | 30 | expected = {id: 1, children: [ 31 | {id: 11, children: [ 32 | {id: 111} 33 | ]}, 34 | {id: 12} 35 | ]}; 36 | 37 | actual = flatToNested.convert(flat); 38 | 39 | assert.deepEqual(actual, expected); 40 | }); 41 | 42 | it('should convert when the parents come after the children and there is a root', function () { 43 | var flat, expected; 44 | 45 | flat = [{id: 111, parent: 11}, {id: 11, parent: 1}, {id: 12, parent: 1}, {id: 1}]; 46 | 47 | expected = {id: 1, children: [ 48 | {id: 11, children: [ 49 | {id: 111} 50 | ]}, 51 | {id: 12} 52 | ]}; 53 | 54 | assert.deepEqual(flatToNested.convert(flat), expected); 55 | }); 56 | 57 | it('should convert when the parents come before the children and there is no root', function () { 58 | var flat, expected, actual; 59 | 60 | flat = [{id: 1}, {id: 11, parent: 1}, {id: 12, parent: 1}, {id: 111, parent: 11}, {id: 2}, {id: 21, parent: 2}]; 61 | 62 | expected = { 63 | children: [ 64 | { 65 | id: 1, 66 | children: [ 67 | {id: 11, children: [{id: 111}]}, 68 | {id: 12} 69 | ] 70 | }, 71 | { 72 | id: 2, 73 | children: [{id: 21}] 74 | } 75 | ] 76 | }; 77 | 78 | actual = flatToNested.convert(flat); 79 | 80 | assert.deepEqual(actual, expected); 81 | }); 82 | 83 | it('should convert when the parents come after the children and there is no root', function () { 84 | var flat, expected; 85 | 86 | flat = [{id: 111, parent: 11}, {id: 11, parent: 1}, {id: 12, parent: 1}, {id: 1}, {id: 21, parent: 2}, {id: 2}]; 87 | 88 | expected = { 89 | children: [ 90 | { 91 | id: 1, 92 | children: [ 93 | {id: 11, children: [{id: 111}]}, 94 | {id: 12} 95 | ] 96 | }, 97 | { 98 | id: 2, 99 | children: [{id: 21}] 100 | } 101 | ] 102 | }; 103 | 104 | assert.deepEqual(flatToNested.convert(flat), expected); 105 | }); 106 | }); 107 | 108 | describe('using custom configuration', function () { 109 | var flatToNested; 110 | 111 | flatToNested = new FlatToNested({ 112 | id: 'code', 113 | parent: 'from', 114 | children: 'to' 115 | }); 116 | 117 | it('should convert an empty array to an empty object', function () { 118 | assert.deepEqual(flatToNested.convert([]), {}); 119 | }); 120 | 121 | it('should convert a one element array to an object without children', function () { 122 | assert.deepEqual(flatToNested.convert([{code: 1, someKey: 'someValue'}]), {code: 1, someKey: 'someValue'}); 123 | }); 124 | 125 | it('should convert when the parents come before the children and there is a root', function () { 126 | var flat, expected, actual; 127 | 128 | flat = [{code: 1}, {code: 11, from: 1}, {code: 12, from: 1}, {code: 111, from: 11}]; 129 | 130 | expected = {code: 1, to: [ 131 | {code: 11, to: [ 132 | {code: 111} 133 | ]}, 134 | {code: 12} 135 | ]}; 136 | 137 | actual = flatToNested.convert(flat); 138 | 139 | assert.deepEqual(actual, expected); 140 | }); 141 | 142 | it('should convert when the parents come after the children and there is a root', function () { 143 | var flat, expected; 144 | 145 | flat = [{code: 111, from: 11}, {code: 11, from: 1}, {code: 12, from: 1}, {code: 1}]; 146 | 147 | expected = {code: 1, to: [ 148 | {code: 11, to: [ 149 | {code: 111} 150 | ]}, 151 | {code: 12} 152 | ]}; 153 | 154 | assert.deepEqual(flatToNested.convert(flat), expected); 155 | }); 156 | 157 | it('should convert when the parents come before the children and there is no root', function () { 158 | var flat, expected, actual; 159 | 160 | flat = [ 161 | {code: 1}, 162 | {code: 11, from: 1}, 163 | {code: 12, from: 1}, 164 | {code: 111, from: 11}, 165 | {code: 2}, 166 | {code: 21, from: 2} 167 | ]; 168 | 169 | expected = { 170 | to: [ 171 | { 172 | code: 1, 173 | to: [ 174 | {code: 11, to: [{code: 111}]}, 175 | {code: 12} 176 | ] 177 | }, 178 | { 179 | code: 2, 180 | to: [{code: 21}] 181 | } 182 | ] 183 | }; 184 | 185 | actual = flatToNested.convert(flat); 186 | 187 | assert.deepEqual(actual, expected); 188 | }); 189 | 190 | it('should convert when the parents come after the children and there is no root', function () { 191 | var flat, expected; 192 | 193 | flat = [ 194 | {code: 111, from: 11}, 195 | {code: 11, from: 1}, 196 | {code: 12, from: 1}, 197 | {code: 1}, 198 | {code: 21, from: 2}, 199 | {code: 2} 200 | ]; 201 | 202 | expected = { 203 | to: [ 204 | { 205 | code: 1, 206 | to: [ 207 | {code: 11, to: [{code: 111}]}, 208 | {code: 12} 209 | ] 210 | }, 211 | { 212 | code: 2, 213 | to: [{code: 21}] 214 | } 215 | ] 216 | }; 217 | 218 | assert.deepEqual(flatToNested.convert(flat), expected); 219 | }); 220 | }); 221 | 222 | describe('using options to not delete the parent', function () { 223 | var flatToNested; 224 | 225 | flatToNested = new FlatToNested({ 226 | options: { 227 | deleteParent: false 228 | } 229 | }); 230 | 231 | it('should have parent after convert', function () { 232 | var flat, expected, actual; 233 | 234 | flat = [{id: 1}, {id: 2, parent: 1}]; 235 | 236 | expected = {id: 1, children: [ 237 | {id: 2, parent: 1} 238 | ]}; 239 | 240 | actual = flatToNested.convert(flat); 241 | assert.deepEqual(actual, expected); 242 | }); 243 | }); 244 | }); 245 | --------------------------------------------------------------------------------