Build a tree from file paths - javascript

I'm trying to create a tree view from file paths, which can be added and removed dinamically, for instance:
A/B/C/D/file1.txt
A/B/D/E/file2.txt
A/B/D/G/file3.txt
A/B/D/G/file4.txt
My tree, however, has a requirement that paths with no child items (files) should be collapsed in one node. For the paths above it would yield:
A/B
|---C/D
file1.txt
|---D
|---E
| file2.txt
|---G
file3.txt
file4.txt
Any thoughts? Creating the tree is easy, but I can't get past that extra condition... I assume I'd have to use some kind of recursion as I go adding items and breaking the paths as we find that a certain path has more children (and then doing the same recursively?). Should I use some kind of trie? Would it work when the same path can have multiple files?... Thanks!

Let's begin by a simple solution to print the tree as it actually is:
function browseTree(node)
{
// ...print node...
// Visit recursively the children nodes:
for (var child: node.children)
{
browseTree(child);
}
}
Now, let's modify it to "shorten" the single-folder paths:
function browseTree(node)
{
// Before printing, accumulate as many straight folders as possible:
var nodeName=node.name
while (hasJustOneFolder(node))
{
// This loop steps deeper in the tree:
node=node.children[0]
nodeName+="/"+node.name;
}
// ...print node...
// Last, visit recursively the non-unique children nodes:
for (var child: node.children)
{
browseTree(child);
}
}
function hasJustOneFolder(node)
{
return node.children.length==1 && node.children[0].isFolder();
}

Given your requirements, it seems like adding a new file to, say, C, wouldn't imply recursive operations.
If you add file5.txt to folder C, you have to transform C/D in a node C having 2 children: file5.txt and a new node called D. D will have the same children as the old node C/D. You can then erase node C/D.
However, this will not affect node A/B, because folder A will still have only one folder (B) as a child. Thus you could solve the problem making only local changes.

I create a sample code with custom tree node format with Map and the print function is a generator function to get line by line the path of the tree.
// Node
class NodePath {
constructor(e) {
this.isFolder = e.isFolder;
this.name = e.name;
this.childs = new Map();
}
}
// Make path tree
function makePathsTree(paths) {
const treeRoot = new NodePath({isFolder: true, name: "*"});
for (const path of paths) {
// Set current post as root
let curPos = treeRoot;
// For each part
const parts = path.split("/");
while (parts.length) {
// Get cur
const curPart = parts.shift();
// Get child node, create if not exists
let childNode = curPos.childs.get(curPart);
if (!childNode) {
childNode = new NodePath({
isFolder: !!parts.length,
name: curPart,
});
curPos.childs.set(curPart, childNode)
}
// Update cur post to child node
curPos = childNode;
}
}
// Return tree
return treeRoot;
}
// Generator function prevent huge large file system strings
function *printPathsTree(node, offset = 0, prev = "") {
// Offset str
const offsetStr = " ".repeat(offset);
// Is folder
if (!node.isFolder) {
yield `${offsetStr}${prev}${node.name}`;
return;
}
// If one child and is folder, merge paths
if (node.childs.size === 1) {
const child = node.childs.values().next().value;
if (child.isFolder === true) {
for (const childData of printPathsTree(child, offset, `${prev}${node.name}/`)) {
yield childData;
}
return;
}
}
// Print node name
yield `${offsetStr}${prev}${node.name}`;
// For each child, print data inside
for (const child of node.childs.values()) {
for (const childData of printPathsTree(child, offset + prev.length, "|---")) {
yield childData;
}
}
}
// == CODE ==
console.log("WITH ROOT:");
const tree = makePathsTree([
"A/B/C/D/file1.txt",
"A/B/C/D/file2.txt",
"A/B/D/E/file2.txt",
"A/B/D/G/file3.txt",
"A/B/D/G/file4.txt",
]);
// Print tree step by step
for(const nodePath of printPathsTree(tree)) {
console.log(nodePath);
}
// Print with A as root
console.log("\nA AS ROOT:");
for(const nodePath of printPathsTree(tree.childs.values().next().value)) {
// for(const nodePath of printPathsTree(tree.childs.get("A"))) { // same
console.log(nodePath);
}
Output:
WITH ROOT:
*/A/B
|---C/D
|---file1.txt
|---file2.txt
|---D
|---E
|---file2.txt
|---G
|---file3.txt
|---file4.txt
A AS ROOT:
A/B
|---C/D
|---file1.txt
|---file2.txt
|---D
|---E
|---file2.txt
|---G
|---file3.txt
|---file4.txt

You can build the tree by recursively grouping on the leading path name, and then merging parent-child names if a parent only has one child:
var paths = ["A/B/C/D/file1.txt", "A/B/C/D/file2.txt", "A/B/D/E/file2.txt", "A/B/D/G/file3.txt", "A/B/D/G/file4.txt"]
function merge_paths(paths){
var d = {};
var new_d = {}
for (var [a, ...b] of paths){
d[a] = (a in d) ? [...d[a], b] : [b]
}
for (var i of Object.keys(d)){
if (d[i].every(x => x.length === 1)){
new_d[i] = d[i].map(x => x[0]);
}
else{
var k = merge_paths(d[i])
if (Object.keys(k).length > 1){
new_d[i] = k
}
else{
new_d[`${i}/${Object.keys(k)[0]}`] = k[Object.keys(k)[0]]
}
}
}
return new_d;
}
var result = merge_paths(paths.map(x => x.split('/')))
console.log(result)

Related

How to print file path with directory Indentation Nodejs

Printing file path with directory indentation.
Input will be
[
"/root/html/file/a.html",
"/root/html/image/a.jpg",
"/root/html/file/b.html",
"/tmp/c.log"
]
Output needs to be like following,
- root
- html
- file
- a.html
- b.html
- image
- a.jpg
- tmp
- c.log
I couldn't find any solution. I guess it will be a recursive call with a tree structure. Any help would be appreciated.
You could map each file path of your input data into a simple tree-map structure and then recursively print this structure. Something like this (not tested and might still need to handle edge cases):
function processData(data) {
const separator = "/";
const tree = {};
data.forEach(element => mapPathToTree(tree, element.split(separator).filter(part => part)));
printTree(tree, 0);
}
function mapPathToTree(tree, pathParts) {
if (pathParts && pathParts.length <= 1) {
tree[pathParts[0]] = {};
return;
}
const currPart = pathParts[0];
let subTree = tree[currPart];
if (!subTree) {
subTree = {};
tree[currPart] = subTree;
}
mapPathToTree(subTree, pathParts.slice(1));
}
function printTree(subTree, level) {
for (const key in subTree) {
console.log(" ".repeat(level).concat("- ").concat(key));
printTree(subTree[key], level + 1);
}
}
processData([
"/root/html/file/a.html",
"/root/html/image/a.jpg",
"/root/html/file/b.html",
"/tmp/c.log"
]);

Stop this recursive call once all the items in the items(array) is visited (JavaScript)

I am trying to make a recursion call of this.isParentEmpty() inside this method and I need to stop this recursion once all the items are visited . Maybe using a counter.. Here for example I have items.length as 3 then the recursion should only happen till the time the length is met and break after that in order to avoid infinite loop or circular dependencies.. What could be the stopping condition for this??
isParentEmpty(item, items) {
const parentSystemRecordId = R.path(['parent', 'id'], item);
if(!parentSystemRecordId || !item.isDependentList) {
return false;
}
const parentItem =
items.find(({ _id }) => Number(_id) === parentSystemRecordId);
if(this.isParentEmpty(parentItem, items)) {
return true;
}
return parentItem && !R.path(['value', 'id'], parentItem);
}
You can pass a Set along in your function that marks which items you have visited.
To check whether you should process an item, you can check your set with visited.has(item).
Not sure about your specific example, but here's a more generic proof of concept:
const nodeA = { id: "A", links: [] };
const nodeB = { id: "B", links: [] };
const nodeC = { id: "C", links: [] };
const nodeD = { id: "D", links: [] };
// Create a circular structure
nodeA.links.push(nodeB);
nodeB.links.push(nodeC);
nodeC.links.push(nodeC);
nodeB.links.push(nodeD);
nodeD.links.push(nodeA);
// Write a recursive function that will only visit each node once
const visitOnce = (node, f, visited = new Set(), distance = 0) => {
// Mark the node as visited
visited.add(node);
// Execute some logic / side effect
f(node, distance);
node.links
// Prevent recursion for nodes that have been visited
.filter(n => !visited.has(n))
// Recurse, pass the set along
.forEach(n => visitOnce(n, f, visited, distance + 1));
}
visitOnce(nodeA, (n, d) => console.log(`${n.id} at distance ${d}`));

recursively generate filepaths from object properties

I am using node.js and as a side project i am creating a module that reads a .json file ,parse it then create directory structure based on object properties & object values.
Object properties(keys) would be the path to itself/to files & object values would be the list of files for that path
i have tried to recurse downwards through the object but i dont know how i extract the path from the inner-most object of each object
Also object would be dynamic as would be created by the user.
var path = 'c:/templates/<angular-app>';
var template = {
//outline of 'angular-app'
src:{
jade:['main.jade'],
scripts:{
modules:{
render:['index.js'],
winodws:['index.js'],
header:['header.js' ,'controller.js'],
SCSS:['index.scss' ,'setup.scss'],
}
}
},
compiled:['angular.js','angular-material.js' ,'fallback.js'],
built:{
frontEnd:[],//if the array is empty then create the path anyways
backEnd:[],
assets:{
fontAwesome:['font-awesome.css'],
img:[],
svg:[]
}
}
}
//desired result...
let out = [
'c:/template name/src/jade/main.jade',
'c:/template name/src/scripts/index.js',
'c:/template name/src/scripts/modules/render/index.js',
'c:/template name/compiled/angular.js',
'c:/template name/compiled/angular-material.js',
'c:/template name/compiled/fallback.js',
'c:/template name/built/frontEnd/',
'c:/template name/built/backEnd/',
//...ect...
];
Here's an example on how you can write this recursively:
var path = 'c:/templates';
var template = {
//outline of 'angular-app'
src: {
jade: ['main.jade'],
scripts: {
modules: {
render: ['index.js'],
winodws: ['index.js'],
header: ['header.js', 'controller.js'],
SCSS: ['index.scss', 'setup.scss'],
}
}
},
compiled: ['angular.js', 'angular-material.js', 'fallback.js'],
built: {
frontEnd: [], //if the array is empty then create the path anyways
backEnd: [],
assets: {
fontAwesome: ['font-awesome.css'],
img: [],
svg: []
}
}
}
function recurse(item, path, result) {
//create default output if not passed-in
result = result || [];
//item is an object, iterate its properties
for (let key in item) {
let value = item[key];
let newPath = path + "/" + key;
if (typeof value === "string") {
//if the property is a string, just append to the result
result.push(newPath + "/" + value);
} else if (Array.isArray(value)) {
//if an array
if (value.length === 0) {
//just the directory name
result.push(newPath + "/");
} else {
//itearate all files
value.forEach(function(arrayItem) {
result.push(newPath + "/" + arrayItem);
});
}
} else {
//this is an object, recursively build results
recurse(value, newPath, result);
}
}
return result;
}
var output = recurse(template, path);
console.log(output);
My solution for this problem would be as follows;
function getPaths(o, root = "", result = []) {
var ok = Object.keys(o);
return ok.reduce((a,k) => { var p = root + k + "/";
typeof o[k] == "object" && o[k] !== null &&
Array.isArray(o[k]) ? o[k].length ? o[k].forEach(f => a.push(p+=f))
: a.push(p)
: getPaths(o[k],p,a);
return a;
},result);
}
var path = 'c:/templates/',
template = {
//outline of 'angular-app'
src:{
jade:['main.jade'],
scripts:{
modules:{
render:['index.js'],
winodws:['index.js'],
header:['header.js' ,'controller.js'],
SCSS:['index.scss' ,'setup.scss'],
}
}
},
compiled:['angular.js','angular-material.js' ,'fallback.js'],
built:{
frontEnd:[],//if the array is empty then create the path anyways
backEnd:[],
assets:{
fontAwesome:['font-awesome.css'],
img:[],
svg:[]
}
}
},
paths = getPaths(template,path);
console.log(paths);
It's just one simple function called getPaths Actually it has a pretty basic recursive run. If your object is well structured (do not include any properties other than objects and arrays and no null values) you may even drop the typeof o[k] == "object" && o[k] !== null && line too. Sorry for my unorthodox indenting style but this is how i find to deal with the code more easy when doing ternaries, logical shortcuts and array methods with ES6 arrow callbacks.

Create a pipeline from Json to streams with transducers-js and most.js

I have this Amd module
define(function (require, exports, module) {
'use strict';
var t = require("transducers");
var most = require("most");
var groupby = function (prev, curr) {
var key = curr.type;
if (!prev[key]) {
prev[key] = [curr];
} else {
prev[key].push(curr);
}
return prev;
};
function category(kv) {
return {
type: kv[0],
label: kv[1][0].label,
counter: kv[1].length
}
}
function dom(cat) {
var el = document.createElement("li");
el.innerHTML = cat.label;
return el;
}
function append(prev, curr) {
prev.appendChild(curr);
return prev;
}
function createClick(prev, curr) {
return prev.merge(most.fromEvent("click", curr)
.map(function (e) {
return e.currentTarget.innerHTML;
})
)
}
var xf = t.comp(
t.map(category),
t.map(dom)
);
module.exports = {
main: function (data) {
var groups = t.reduce(groupby, {}, data);
var container = t.transduce(xf, append, document.querySelector("ul"), groups);
var streams = t.reduce(createClick, most.empty(), [].slice.call(container.querySelectorAll("li"), 0));
streams.forEach(function (e) {
console.log("click", e);
});
}
};
});
Main function takes a list of items, then groups them by 'type' property. After that it creates and appends < li > elements. Finally it creates a stream of clicks. I'm new in reactive programming and transducers.
But I was wondering if there would be a way to create a pipeline.
I trouble because groupby is a reduction and a can't compose it in transducer. I'm sure I'm missing something. Thanks
Try and separate your problem into things that can operate on the individual item vs on the collection and wait until last to reduce them. also check into the often missed "scan" operation which can save you from over aggressive reductions
In your example, you have 3 reducing possible operations listed:
- merge all click streams into one stream of events
- merge all dom into a single ul
- count
the can all be accomplished with scan, but the issue arrises in that you want to unique categories, but you also count the non unique ones. It's not clear from your example if that's actually a requirement though...
Since most already works similar to transducers, you don't really need them. For this example I'll stick with pure mostjs;
var most = require("most");
function gel(tag) {
return document.createElement(tag);
}
var cache = Object.create(null);
function main(dataArray) {
most.from(dataArray)
//only pass along unique items to represent categories
//optionally, also count...
.filter(function uniq(item) {
var category = item.type;
if (!(category in cache))
cache[category] = 0;
cache[category]++;
return cache[category] == 1;
})
//transform
.map(function makeLI(item) {
var li = gel("li");
li.innerHTML = item.label;
item.dom = li;
})
//attach click handler
.map(function (item) {
item.click = most
.fromEvent("click", item.dom)
.map(function(e) {
item.label;
});
return item;
})
// merge
.scan(function mergeIn(all, item) {
el.appendChild(item.dom);
clicks.merge(item.click);
}, { ul: gel("ul"), clicks: most.empty() })
//force stream init b creating a demand
.drain()
//most resolve stream into promises, but we could have just
//as easily used reduce here instead
.then(function onComplete(all) {
all.clicks.forEach(function(msg) {
console.log("click", e);
})
})
}
further variation are possible. for example if we wanted to add a sublist for each category item, we could attach a stream to the context object for each category and incrementally append to each child as we go

How to remove nested attributes?

I have this JSON file: http://danish-regional-data.googlecode.com/svn/trunk/danish_regional_data.json
How do I remove all the attribues within_5_km, within_10_km, within_25_km, within_50_km, within_100_km for all postcodes?
I have read this question: Remove a JSON attribute
$(document).ready(function() {
$.getJSON("post.json", function(data) {
var pc = data.postalcodes;
for (var id in pc) {
if(pc.hasOwnProperty(id)) {
for(var attr in pc[id]) {
if(pc[id].hasOwnProperty(attr) && attr.indexOf('within_') === 0) {
delete pc[id][attr];
}
}
}
}
$("#json").html(pc);
});
});
In ES2016 you can use destructing to pick the fields you want for the subset object.
//ES6 subset of an object by specific fields
var object_private = {name: "alex", age: 25, password: 123};
var {name,age} = object_private, object_public = {name,age}
//method 2 using literals
let object_public = (({name,age})=>({name,age}))(object_private);
//use map if array of objects
users_array.map(u=>u.id)
Go to the json url you provided and open the Firebug console. Then drop in the folloing code and execute it:
var p = document.getElementsByTagName('pre');
for(i=0; i < p.length; i++) {
var data = JSON.parse(p[i].innerHTML);
var pc = data.postalcodes;
// this is the code i gave you... the previous is jsut to pull it out of the page
// in Firebug - this works for me
for (var id in pc) {
if(pc.hasOwnProperty(id)) {
for(var attr in pc[id]) {
if(pc[id].hasOwnProperty(attr) && attr.indexOf('within_') === 0) {
console.log('Deleting postalcodes.'+id+'.'+attr);
delete pc[id][attr];
}
}
}
}
}
// assume data is the complete json
var pc = data.postalcodes;
for (var id in pc) {
if(pc.hasOwnProperty(id)) {
for(var attr in pc[id]) {
if(pc[id].hasOwnProperty(attr) && attr.indexOf('within_') === 0) {
delete pc[id][attr];
}
}
}
}
JSON truncated:
var data = {"postalcodes":
{"800":{"id":"800","name":"H\u00f8je Taastrup","region_ids":["1084"],"region_names":["Hovedstaden"],"commune_ids":["169"],"commune_names":["H\u00f8je-Taastrup"],"lat":"55.66713","lng":"12.27888", "within_5_km":["800","2620","2630","2633"],"within_10_km":["800","2600","2605","2620"]},
"900":{"id":"900","name":"K\u00f8benhavn C","region_ids":["1084"],"region_names":["Hovedstaden"],"commune_ids":["101"],"commune_names":["K\u00f8benhavns"],"lat":"55.68258093401054","lng":"12.603657245635986","within_5_km":["900","999"]},
"1417":{"commune_id":"390","region_id":"1085"}}};
var pc = data.postalcodes;
for (var id in pc) {
var entry = pc[id];
for(var attr in entry) {
if(attr.indexOf('within_') === 0) {
delete entry[attr];
}
}
}
console.dir(data); // your data object has been augmented at this point
you can also use regular expression
var data = {"postalcodes":
{"800":{"id":"800","name":"H\u00f8je Taastrup","region_ids":["1084"],"region_names":["Hovedstaden"],"commune_ids":["169"],"commune_names":["H\u00f8je-Taastrup"],"lat":"55.66713","lng":"12.27888", "within_5_km":["800","2620","2630","2633"],"within_10_km":["800","2600","2605","2620"]},
"900":{"id":"900","name":"K\u00f8benhavn C","region_ids":["1084"],"region_names":["Hovedstaden"],"commune_ids":["101"],"commune_names":["K\u00f8benhavns"],"lat":"55.68258093401054","lng":"12.603657245635986","within_5_km":["900","999"]},
"1417":{"commune_id":"390","region_id":"1085"}}};
var regexp = new RegExp("^within_", "i"); // case insensitive regex matching strings starting with within_
var pc = data.postalcodes;
for (var id in pc) {
var entry = pc[id];
for(var attr in entry) {
if(regexp.test(attr)) {
delete entry[attr];
}
}
}
console.dir(data);
I have written an npm module unset which exactly does this. You specify json paths similar to the json-path module until the leaf attribute that you want to remove.
let unset = require('unset');
let object = {a: { b: [ {x: 1}, {x: [{ e: 2} ]}]}};
let newObject = unset(object, ['/a/b[*]/x']);
Multiple paths are supported in the second argument
well you can do this:
var postalcodes = YOUR JSON;
for(var code in postalcodes)
{
delete postalcodes[code].within_5_km;
.
.
.
}
you will probably want to check if the code contains your properties...

Categories

Resources