Transform dot notation to a tree data form - javascript

I have object oriented data in the form:
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
...
]
which I would like to transform into a tree structure, to be able to serve them with a tree component (https://github.com/vinz3872/vuejs-tree in particular). The require form is the following:
var ok = [
{
text: "foo",
state: { expanded: false },
nodes: [
{
id: 1,
path: "foo.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
id: 2,
path: "foo.lol2",
text: "lol2",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
text: "barbar",
state: { expanded: false },
nodes: [
{
id: 4,
path: "bar.barbar.kk",
text: "kk",
checkable: true,
state: { checked: false },
},
]
},
]
}
]
I am aware that I should use recursion and I have tried all relevan posts in stackoverflow, i.e. How to build a JSON tree structure using object dot notation.
My main problem is that I have to somehow preserve the information of the full path to the leaves of the tree. As a newbie in js I lost myself in counters and callback for days without any luck.
I would appreciate your help.
Thank you in advance

Basically you could use forEach then split each string into array and then use reduce on that. Then you build nested object where the keys are current paths and also ad to result array.
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
]
const result = []
const levels = {
result
}
let prev = ''
let id = 1
alist.forEach(str => {
str.split('.').reduce((r, text, i, arr) => {
const path = prev += (prev.length ? '.' : '') + text
if (!r[path]) {
r[path] = {result: []}
const obj = {
id: id++,
text,
}
if (i === 0) {
obj.state = {expanded: false}
} else {
obj.state = {checked: false}
obj.checkable = true
obj.path = path
}
obj.nodes = r[path].result
r.result.push(obj)
}
if (i === arr.length - 1) {
prev = ''
}
return r[path]
}, levels)
})
console.log(result)

I found that it was easiest to do this transformation in two steps. The first converts your input into this format:
{
foo: {
lol1: {},
lol2: {}
},
bar: {
barbar: {
kk: {}
},
lol1: {}
},
}
The second uses just this format to create your desired structure. This has two advantages. First, I have tools lying around that make it easy to create this structure from your input. Second, this structure embeds enough information to create your output, with only one branching construct: whether the value at a path is an empty object or has properties. This makes the generation code relatively simple:
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const reformat = (o, path = [], nextId = ((id) => () => String (++ id)) (0)) =>
Object .entries (o) .map (([k, v]) => Object .entries (v) .length > 0
? {text: k, state: {exapanded: false}, nodes: reformat (v, [...path, k], nextId)}
: {id: nextId (), path: [...path, k] .join('.'), text: k, checkable: false, state: {checked: false}}
)
const transform = (pathTokens) =>
reformat (pathTokens
.map (s => s .split ('.'))
.reduce ((a, path) => setPath (path) ({}) (a), {})
)
const alist = ['foo', 'foo.lol1', 'foo.lol2', 'bar.lol1', 'bar.barbar.kk']
console .log (transform (alist))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with setPath, which takes a path, in a format such as ['bar', 'barbar', 'kk'], the value to set at that path, and an object to shallow clone with this new property along that path. Thus setPath (['foo', 'bar', 'baz']) (42) ({foo: {qux: 7}, corge: 6}) yields {foo: {qux: 7, bar: {baz: 42}}, corge: 6}. (There's a little more in this reusable function to also handle array indices instead of string object paths, but we can't reach that from this input format.)
Then we have reformat, which does the format conversion. It simply builds a different input object based upon whether the input value is an empty object.
Finally, transform maps a splitting function over your input array to get the path structure needed for setPath, folds the results into an initially empty object by setting every path value to an empty object, yielding our intermediate format, which we then pas to reformat.
There is one thing I really don't like here, and that is the nextId function, which is a stateful function. We could just have easily used a generator function, but whatever we do here, we're using state to build this output and that bothers me. If someone has a cleaner suggestion for this, I'd love to hear it.

Related

Get value of Objet, according the "path"

Good morning,
I have an object with this structure :
{
instituts: {
default: 999,
users: { default: 888, email: 777 },
docs: undefined,
exams: { price: 3 },
empowermentTests: 2,
sessionsHist: 3
},
exams: { default: 0 },
countries: 1,
levels: 1,
roles: { default: 1 },
sessions: 1,
tests: 1,
users: { default: 3 },
options: undefined,
skills: { default: undefined }
}
And i have an array giving the path like ["instituts", "users"].
Thank to this array of paths, i want to parse my object and return the value of :
instituts.users.default ( 'default', because after the path "users", there are not other entries and users type is an object)
An other example : path : ['instituts', "users", "email"]
i want to have the value of : instituts.users.email (not 'default', cause in the object structure, email, is an integer, not an objet ).
I hope i am clear enought.
I tryed many things but i am lost with ES6 function : "filter / reduce / map ..."
Thank you for the help you can give.
I think this does what you're looking for:
const getPath = ([p, ...ps]) => (o) =>
p == undefined ? o : getPath (ps) (o && o[p])
const getPowerNeed = (path, obj, node = getPath (path) (obj)) =>
Object (node) === node && 'default' in node ? node .default : node
const input = {instituts: {default: 999, users: {default: 888, email: 777}, docs: undefined, exams: {price: 3}, empowermentTests: 2, sessionsHist: 3}, exams: {default: 0}, countries: 1, levels: 1, roles: {default: 1}, sessions: 1, tests: 1, users: {default: 3}, options: undefined, skills: {default: undefined}}
console .log (getPowerNeed (['instituts', 'users'], input)) //=> 888
console .log (getPowerNeed (['instituts', 'users', 'email'], input)) //=> 777
console .log (getPowerNeed (['instituts'], input)) //=> 999
console .log (getPowerNeed (['instituts', 'exams', 'price'], input)) //=> 3
I'm making the assumption that if the default value is not to be found, then you want to return the actual value if it exists. Another reasonable reading of the question is that you want to return the literal string 'instituts.users.email'. That wouldn't be much more difficult to do if necessary.
The utility function getPath takes a path and returns a function from an object to the value at that path in the object, returning undefined if there are any missing nodes along that path. The main function is getPowerNeed, which wraps some default-checking around a call the getPath.
My code :
i receive URL : /api/instituts/1/users/email for example
My express middleware is named : isAuthorized.
My object with the power ( for the giving roles of users ) is named : powerNeedByHttpMethod
The "paths" are giving by splitting and filterring URL :
const paths = req.url.split('/').filter(e => e !== 'api' && !parseInt(e) && e !== '');
I tryed to made this by following an example found on this board :
getPowerNeed(obj, paths) {
if (!obj) return null;
const partial = paths.shift(),
filteredKeys = Object.keys(obj).filter(k => k.toLowerCase().includes(partial));
if (!filteredKeys.length) return null; // no keys with the path found
return filteredKeys.reduce((acc, key) => {
if(!paths.length) return { ...acc, [key]: obj[key] }
const nest = module.exports.getPowerNeed(obj[key], [...paths]) // filter another level
return nest ? { ...acc, [key]: nest } : acc
}, null)
}
i found an other solutions :
const routes = ['options', 'arbo1', 'arbo2'];
const objPower = {
instituts: {
default: 999,
users: { default: 888, email: 777 },
docs: undefined,
exams: { price: 3 },
empowermentTests: 2,
sessionsHist: 3
},
exams: { default: 0 },
countries: 1,
levels: 1,
roles: { default: 1 },
sessions: 1,
tests: 1,
users: { default: 3 },
options: {
default: 19822,
arbo1 : {
arbo2 : {
arbo3: 3
}}},
skills: { default: undefined }
}
function getPower(objPower, entries, defaultPowerNeeded) {
// On extrait le premier élément de la route et on le retire.
const firstEntry = entries.shift();
// Object.keys to list all properties in raw (the original data), then
// Array.prototype.filter to select keys that are present in the allowed list, using
// Array.prototype.includes to make sure they are present
// Array.prototype.reduce to build a new object with only the allowed properties.
const filtered = Object.keys(objPower)
.filter(key => key === firstEntry)
.reduce((obj, key) => {
obj[key] = objPower[key];
if(objPower[key].default) {
defaultPowerNeeded = objPower[key].default;
}
return obj;
}, {})[firstEntry];
if(!entries.length) {
if(typeof filtered === 'object') {
if(filtered.default) {
return filtered.default;
}
else {
return defaultPowerNeeded;
}
}
else return filtered;
}
return getPower(filtered,entries, defaultPowerNeeded);
}
const defaultPowerNeeded = 12;
const result = getPower(objPower, routes, defaultPowerNeeded);
console.log(result);

Create a tree from a list of strings containing paths

See edit below
I wanted to try and create a tree from a list of paths and found this code on stackoverflow from another question and it seems to work fine but i would like to remove the empty children arrays instead of having them showing with zero items.
I tried counting r[name].result length and only pushing it if it greater than zero but i just end up with no children on any of the nodes.
let paths = ["About.vue","Categories/Index.vue","Categories/Demo.vue","Categories/Flavors.vue","Categories/Types/Index.vue","Categories/Types/Other.vue"];
let result = [];
let level = {result};
paths.forEach(path => {
path.split('/').reduce((r, name, i, a) => {
if(!r[name]) {
r[name] = {result: []};
r.result.push({name, children: r[name].result})
}
return r[name];
}, level)
})
console.log(result)
EDIT
I didnt want to ask directly for the purpose i am using it for but if it helps i am trying to create an array like this: (this is a copy paste of the config needed from ng-zorro cascader)
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
isLeaf: true
}
]
},
{
value: 'ningbo',
label: 'Ningbo',
isLeaf: true
}
]
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
isLeaf: true
}
]
}
]
}
];
from an array of flat fields like this:
let paths = ["About.vue","Categories/Index.vue","Categories/Demo.vue","Categories/Flavors.vue","Categories/Types/Index.vue","Categories/Types/Other.vue"];
I suggest to use a different approach.
This approach takes an object and not an array for reaching deeper levels and assigns an array only if the nested level is required.
let paths = ["About.vue", "Categories/Index.vue", "Categories/Demo.vue", "Categories/Flavors.vue", "Categories/Types/Index.vue", "Categories/Types/Other.vue"],
result = paths
.reduce((parent, path) => {
path.split('/').reduce((r, name, i, { length }) => {
let temp = (r.children ??= []).find(q => q.name === name);
if (!temp) r.children.push(temp = { name, ...(i + 1 === length && { isLeaf: true }) });
return temp;
}, parent);
return parent;
}, { children: [] })
.children;
console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to create/merge object from splitted string array in TypeScript?

I have an array of objects like below;
const arr1 = [
{"name": "System.Level" },
{"name": "System.Status" },
{"name": "System.Status:*" },
{"name": "System.Status:Rejected" },
{"name": "System.Status:Updated" }
]
I am trying to split name property and create an object. At the end I would like to create an object like;
{
"System.Level": true,
"System.Status": {
"*": true,
"Rejected": true,
"Updated": true
}
}
What I have done so far;
transform(element){
const transformed = element.split(/:/).reduce((previousValue, currentValue) => {
previousValue[currentValue] = true;
}, {});
console.log(transofrmed);
}
const transofrmed = arr1.foreEach(element => this.transform(element));
The output is;
{System.Level: true}
{System.Status: true}
{System.Status: true, *: true}
{System.Status: true, Rejected: true}
{System.Status: true, Updated: true}
It is close what I want to do but I should merge and give a key. How can I give first value as key in reduce method? Is it possible to merge objects have same key?
You could reduce the splitted keys adn check if the last level is reached, then assign true, otherwise take an existent object or a new one.
const
array = [{ name: "System.Level" }, { name: "System.Status" }, { name: "System.Status:*" }, { name: "System.Status:Rejected" }, { name: "System.Status:Updated" }],
object = array.reduce((r, { name }) => {
var path = name.split(':');
last = path.pop();
path.reduce((o, k) => o[k] = typeof o[k] === 'object' ? o[k] : {}, r)[last] = true;
return r;
}, {});
console.log(object);
Use Array.reduce() on the list of properties. After splitting the path by :, check if there is second part. If there is a second part assign an object. Use object spread on the previous values, because undefined or true values would be ignored, while object properties would be added. If there isn't a second part, assign true as value:
const array = [{ name: "System.Level" }, { name: "System.Status" }, { name: "System.Status:*" }, { name: "System.Status:Rejected" }, { name: "System.Status:Updated" }];
const createObject = (arr) =>
arr.reduce((r, { name }) => {
const [first, second] = name.split(':');
r[first] = second ? { ...r[first], [second]: true } : true;
return r;
}, {});
console.log(createObject(array));

How to remove any objects that appear more than once in an array of objects?

If I have an array like:
[
{
id: 1,
title: 'foo'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
},
{
id: 4,
title: 'bantz'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
}
]
And I want to return an array that contains any objects that appear only once. So for this example, the desired output would be:
[
{
id: 1,
title: 'foo'
},
{
id: 4,
title: 'bantz'
}
]
I have tried a few different approaches that I have found to solve this using reduce() and indexOf(), like this solution, but they do not work with objects for some reason.
Any assistance would be greatly appreciated.
You could use a Map to avoid having to look through the array again and again, which would lead to inefficient O(n²) time-complexity. This is O(n):
function getUniquesOnly(data) {
return Array.from(
data.reduce( (acc, o) => acc.set(o.id, acc.has(o.id) ? 0 : o), new Map),
(([k,v]) => v)
).filter( x => x );
}
var data = [
{
id: 1,
title: 'foo'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
},
{
id: 4,
title: 'bantz'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
}
];
console.log(getUniquesOnly(data));
Do something like this:
const data = [
{
id: 1,
title: 'foo'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
},
{
id: 4,
title: 'bantz'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
}
];
const isEqual = (a, b) => a.id === b.id;
const unique = (arr) => arr.reduce((result, a, index) =>
result.concat(arr.some(b => a !== b && isEqual(a, b)) ? [] : a)
, []);
console.log(unique(data));
In this case, we loop through each element to reduce(), and before we add it, we see if another version of it exists in the array before adding it. We have to make sure that we are also not being equal without ourselves (otherwise we'd get an empty array).
isEqual() is a separate function to make it easy to customize what "equal" means.
As written, each element in data is unique, they're all separate objects. data[0] === data[4] is false, even though they have the same data. You must compare the data inside to determine if they're duplicates or not. As Paulpro mentioned earlier, {} === {} is also false, because they're two different objects, even though their values are the same.
console.log({} === {});
console.log({ a: 1 } === { a: 1 });
In the example version of isEqual(), I considered them equal if they had the same id.
Answer to previous version of the question
Do something like this:
const data = [
{
id: 1,
title: 'foo'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
},
{
id: 4,
title: 'bantz'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'bat'
}
];
const isEqual = (a, b) => a.id === b.id;
const unique = (arr) => arr.reduce((result, a) =>
result.concat(result.some(b => isEqual(a, b)) ? [] : a)
, []);
console.log(unique(data));
I split isEqual() to it's own function so you could easily define what "equal" means. As someone pointed out, technically all of those are unique, even if the data is different. In my example, I defined equal ids to mean equal.
I then use reduce to go through each and build an object. Before I add it to the array (via concat()), I loop through all of them with some() and go until either I find one that is equal (which I wouldn't include) or none are equal and I add it.
A straightforward implementation would look something like this:
Create an empty set (in this case an array) to contain unique values by whatever metric you define (I.E. deep comparison or comparing by a unique value like the "id")
Loop over the list of values
Whenever you find a value that is not contained within the set of unique values, add it
That is essentially how the solution you posted works, except that all of the values in your array are -- in JavaScript's eyes -- unique. Because of this you need to define your own way to compare values.
The .reduce method can be used like so:
function areEqual(a, b) { /* define how you want the objects compared here */ }
function contains(a, lst) {
return lst.reduce(function (acc, x) {
return acc || areEqual(a, x);
}, false);
}
function getUnique(lst) {
return lst.reduce(function (acc, x) {
if(!contains(x, acc))
{
acc.push(x);
}
return acc;
}, []);
}
You may want to look at how JavaScript object comparison works. For deep comparison specifically (which it sounds like you want) I would look at existing answers.

javascript: Using lodash/fp flow to return objects

I am converting a _.chain group of functions to use _fp.flow, but am having some difficulty dealing with the way flow curries complex objects. I am trying to
Reduce an array of objects with some grouped function (e.g. countBy/sumBy) into a object/dictionary (e.g. { group1:10, group2:15... } )
Map it into an array of key/value pairs (e.g. [{column: 'group1', value: '10'}, ...])
Sort by some variable into asc/desc order
but right now the resulting object ends up being flattened into a long array. A sample of the code is below. The reducer function in the code below is working correctly and grouping the values as I intended, but then I think the currying between the each step and orderBy is flattening the object somehow (the desired object is formed correctly after _.each in the console.log.
I've put a sample of the code in the attached JSFiddle.
const inData = [{
target: 123,
groupby: 'a'
},...
}];
const colData = _.flow(
_.reduce(reducer, {}),
_.toPairs,
_.each(([value, column]) => {
console.log(value);
console.log(column);
const outObj = {
value: value,
column: column
}
console.log(outObj)
return (outObj);
}),
_.orderBy(['value'], [sortDir]),
// Have tried result with or without fromPairs
_.fromPairs
)(inData);
PS: I am using ES6 syntax and React in my main project, if that makes a difference.
https://jsfiddle.net/moc0L5ac/
You need to use map instead of each and also fix the order of [value, column] to [column, value]
const colData = _.flow(
_.reduce(reducer, {}),
_.toPairs,
_.map(([column, value]) => {
const outObj = {
value: value,
column: column
}
return outObj;
}),
_.orderBy(['value'], [sortDir])
)(inData);
To the best my understanding, this is what you're looking to accomplish
const inData =
[ { target: 123, groupby: 'a' },
{ target: -123, groupby: 'b' },
{ target: 123, groupby: 'a' },
{ target: -123, groupby: 'b' } ]
const colData = _.flow(
_.reduce((map, {target:v, groupby:k}) =>
Object.assign(map, { [k]: map[k] === undefined ? v : map[k] + v }), {}),
_.toPairs,
_.map(([column, value]) => ({ column, value }))
)
console.log(colData(inData));
// => [ { column: 'a', value: 246 },
// { column: 'b', value: -246 } ]

Categories

Resources