I would like to list all paths of object that lead to leafs
Example:
var obj = {
a:"1",
b:{
foo:"2",
bar:3
},
c:[0,1]
}
Result:
"a","b.foo","b.bar", "c[0]","c[1]"
I would like to find simple and readable solution, best using lodash.
Here is a solution that uses lodash in as many ways as I can think of:
function paths(obj, parentKey) {
var result;
if (_.isArray(obj)) {
var idx = 0;
result = _.flatMap(obj, function (obj) {
return paths(obj, (parentKey || '') + '[' + idx++ + ']');
});
}
else if (_.isPlainObject(obj)) {
result = _.flatMap(_.keys(obj), function (key) {
return _.map(paths(obj[key], key), function (subkey) {
return (parentKey ? parentKey + '.' : '') + subkey;
});
});
}
else {
result = [];
}
return _.concat(result, parentKey || []);
}
Edit: If you truly want just the leaves, just return result in the last line.
Doesn't use lodash, but here it is with recursion:
var getLeaves = function(tree) {
var leaves = [];
var walk = function(obj,path){
path = path || "";
for(var n in obj){
if (obj.hasOwnProperty(n)) {
if(typeof obj[n] === "object" || obj[n] instanceof Array) {
walk(obj[n],path + "." + n);
} else {
leaves.push(path + "." + n);
}
}
}
}
walk(tree,"tree");
return leaves;
}
Based on Nick answer, here is a TS / ES6 imports version of the same code
import {isArray,flatMap,map,keys,isPlainObject,concat} from "lodash";
// See https://stackoverflow.com/a/36490174/82609
export function paths(obj: any, parentKey?: string): string[] {
var result: string[];
if (isArray(obj)) {
var idx = 0;
result = flatMap(obj, function(obj: any) {
return paths(obj, (parentKey || '') + '[' + idx++ + ']');
});
} else if (isPlainObject(obj)) {
result = flatMap(keys(obj), function(key) {
return map(paths(obj[key], key), function(subkey) {
return (parentKey ? parentKey + '.' : '') + subkey;
});
});
} else {
result = [];
}
return concat(result, parentKey || []);
}
Feeding that object through this function should do it I think.
recursePaths: function(obj){
var result = [];
//get keys for both arrays and objects
var keys = _.map(obj, function(value, index, collection){
return index;
});
//Iterate over keys
for (var key in keys) {
//Get paths for sub objects
if (typeof obj[key] === 'object'){
var paths = allPaths(obj[key]);
for (var path in paths){
result.push(key + "." + path);
}
} else {
result.push(key);
}
}
return result;
}
Here is my function. It generates all possible paths with dot notation, assuming there are no property names containing spaces
function getAllPathes(dataObj) {
const reducer = (aggregator, val, key) => {
let paths = [key];
if(_.isObject(val)) {
paths = _.reduce(val, reducer, []);
paths = _.map(paths, path => key + '.' + path);
}
aggregator.push(...paths);
return aggregator;
};
const arrayIndexRegEx = /\.(\d+)/gi;
let paths = _.reduce(dataObj, reducer, []);
paths = _.map(paths, path => path.replace(arrayIndexRegEx, '[$1]'));
return paths;
}
Here's my solution. I only did it because I felt the other solutions used too much logic. Mine does not use lodash since I don't think it would add any value. It also doesn't make array keys look like [0].
const getAllPaths = (() => {
function iterate(path,current,[key,value]){
const currentPath = [...path,key];
if(typeof value === 'object' && value != null){
return [
...current,
...iterateObject(value,currentPath)
];
}
else {
return [
...current,
currentPath.join('.')
];
}
}
function iterateObject(obj,path = []){
return Object.entries(obj).reduce(
iterate.bind(null,path),
[]
);
}
return iterateObject;
})();
If you need one where the keys are indexed using [] then use this:
const getAllPaths = (() => {
function iterate(path,isArray,current,[key,value]){
const currentPath = [...path];
if(isArray){
currentPath.push(`${currentPath.pop()}[${key}]`);
}
else {
currentPath.push(key);
}
if(typeof value === 'object' && value != null){
return [
...current,
...iterateObject(value,currentPath)
];
}
else {
return [
...current,
currentPath.join('.')
];
}
}
function iterateObject(obj,path = []){
return Object.entries(obj).reduce(
iterate.bind(null,path,Array.isArray(obj)),
[]
);
}
return iterateObject;
})();
const allEntries = (o, prefix = '', out = []) => {
if (_.isObject(o) || _.isArray(o)) Object.entries(o).forEach(([k, v]) => allEntries(v, prefix === '' ? k : `${prefix}.${k}`, out));
else out.push([prefix, o]);
return out;
};
Array are returned as .0 or .1 that are compatible with _.get of lodash
const getAllPaths = (obj: object) => {
function rKeys(o: object, path?: string) {
if (typeof o !== "object") return path;
return Object.keys(o).map((key) =>
rKeys(o[key], path ? [path, key].join(".") : key)
);
}
return rKeys(obj).toString().split(",").filter(Boolean) as string[];
};
const getAllPaths = (obj) => {
function rKeys(o, path) {
if (typeof o !== "object") return path;
return Object.keys(o).map((key) =>
rKeys(o[key], path ? [path, key].join(".") : key)
);
}
return rKeys(obj).toString().split(",").filter(Boolean);
};
const test = {
a: {
b: {
c: 1
},
d: 2
},
e: 1
}
console.log(getAllPaths(test))
Related
I have problem with my trampoline based function that stringify lisp list. Here is the code:
function Pair(car, cdr) {
this.car = car;
this.cdr = cdr;
}
const nil = new function Nil() {};
// ----------------------------------------------------------------------
Pair.fromArray = function(array) {
var result = nil;
var i = array.length;
while (i--) {
let car = array[i];
if (car instanceof Array) {
car = Pair.fromArray(car);
}
result = new Pair(car, result);
}
return result;
};
// ----------------------------------------------------------------------
function Thunk(fn, cont = () => {}) {
this.fn = fn;
this.cont = cont;
}
// ----------------------------------------------------------------------
Thunk.prototype.toString = function() {
return '#<Thunk>';
};
// ----------------------------------------------------------------------
function trampoline(fn) {
return function(...args) {
return unwind(fn.apply(this, args));
};
}
// ----------------------------------------------------------------------
function unwind(result) {
while (result instanceof Thunk) {
const thunk = result;
result = result.fn();
if (!(result instanceof Thunk)) {
thunk.cont();
}
}
return result;
}
// ----------------------------------------------------------------------
// original function have different data types here
// with simplified version this is fine
function toString(x) {
return x.toString();
}
// ----------------------------------------------------------------------
const pair_to_string = (function() {
const prefix = (pair, rest) => {
var result = [];
if (pair.ref) {
result.push(pair.ref + '(');
} else if (!rest) {
result.push('(');
}
return result;
};
const postfix = (pair, rest) => {
if (!rest || pair.ref) {
return [')'];
}
return [];
};
return trampoline(function pairToString(pair, quote, extra = {}) {
const {
nested,
result = [],
cont = () => {
result.push(...postfix(pair, nested));
}
} = extra;
result.push(...prefix(pair, nested));
let car;
if (pair.cycles && pair.cycles.car) {
car = pair.cycles.car;
} else {
car = toString(pair.car, quote, true, { nested: false, result, cont });
}
if (car !== undefined) {
result.push(car);
}
return new Thunk(() => {
if (pair.cdr instanceof Pair) {
if (pair.cycles && pair.cycles.cdr) {
result.push(' . ');
result.push(pair.cycles.cdr);
} else {
if (pair.cdr.ref) {
result.push(' . ');
} else {
result.push(' ');
}
return pairToString(pair.cdr, quote, {
nested: true,
result,
cont
});
}
} else if (pair.cdr !== nil) {
result.push(' . ');
result.push(toString(pair.cdr, quote));
}
}, cont);
});
})();
// ----------------------------------------------------------------------
Pair.prototype.toString = function(quote) {
var result = [];
pair_to_string(this, quote, {result});
return result.join('');
};
// ----------------------------------------------------------------------
function range(n) {
return new Array(n).fill(0).map((_, i) => i);
}
// ----------------------------------------------------------------------------
function markCycles(pair) {
var seen_pairs = [];
var cycles = [];
var refs = [];
function visit(pair) {
if (!seen_pairs.includes(pair)) {
seen_pairs.push(pair);
}
}
function set(node, type, child, parents) {
if (child instanceof Pair) {
if (parents.includes(child)) {
if (!refs.includes(child)) {
refs.push(child);
}
if (!node.cycles) {
node.cycles = {};
}
node.cycles[type] = child;
if (!cycles.includes(node)) {
cycles.push(node);
}
return true;
}
}
}
const detect = trampoline(function detect_thunk(pair, parents) {
if (pair instanceof Pair) {
delete pair.ref;
delete pair.cycles;
visit(pair);
parents.push(pair);
var car = set(pair, 'car', pair.car, parents);
var cdr = set(pair, 'cdr', pair.cdr, parents);
var thunks = [];
if (!car) {
detect(pair.car, parents.slice());
}
if (!cdr) {
const cdr_args = [pair.cdr, parents.slice()];
return new Thunk(() => {
return detect_thunk(...cdr_args);
});
}
}
});
function mark_node(node, type) {
if (node.cycles[type] instanceof Pair) {
const count = ref_nodes.indexOf(node.cycles[type]);
node.cycles[type] = `#${count}#`;
}
}
detect(pair, []);
var ref_nodes = seen_pairs.filter(node => refs.includes(node));
ref_nodes.forEach((node, i) => {
node.ref = `#${i}=`;
});
cycles.forEach(node => {
mark_node(node, 'car');
mark_node(node, 'cdr');
});
}
// ----------------------------------------------------------------------
// this works fine
//console.log(Pair.fromArray([[[range(8000), range(10)]]]).toString());
var data = new Pair(1, new Pair(new Pair(2, nil), new Pair(3, nil)));
data.cdr.car.cdr = data.cdr;
data.cdr.cdr.cdr = data;
markCycles(data)
console.log(data.toString());
console.log("#0=(1 . #1=((2 . #1#) 3 . #0#)) - valid");
The problem is the last parenthesis is missing, I'm not sure how I should use continuation in trampoline to fix the issue.
Here is the code that was working without trampoline:
// ----------------------------------------------------------------------
Pair.prototype.toString = function(quote, rest) {
var arr = [];
if (this.ref) {
arr.push(this.ref + '(');
} else if (!rest) {
arr.push('(');
}
var value;
if (this.cycles && this.cycles.car) {
value = this.cycles.car;
} else {
value = toString(this.car, quote, true);
}
if (value !== undefined) {
arr.push(value);
}
if (this.cdr instanceof Pair) {
if (this.cycles && this.cycles.cdr) {
arr.push(' . ');
arr.push(this.cycles.cdr);
} else {
if (this.cdr.ref) {
arr.push(' . ');
} else {
arr.push(' ');
}
const cdr = this.cdr.toString(quote, true);
arr.push(cdr);
}
} else if (this.cdr !== nil) {
arr = arr.concat([' . ', toString(this.cdr, quote, true)]);
}
if (!rest || this.ref) {
arr.push(')');
}
return arr.join('');
};
I have two cases that should work first big list (8000 elements) and small cycle. With the code in stack snippet it work with long list but not with cycles without the trampoline it overflow the stack on big list. Also it's lisp so it need to work any any tree not only linked list.
EDIT: if you try to answer please at least don't change the data structures. It need to be Pair class with car and cdr and cycles need to be calculated before they are converted to string. So it work with multiple functions that check if data in memory is cycle.
This is what I would do.
const pure = value => ({ constructor: pure, value });
const bind = (monad, arrow) => ({ constructor: bind, monad, arrow });
const thunk = eval => ({ constructor: thunk, eval });
function evaluate(expression) {
let expr = expression;
let stack = null;
while (true) {
switch (expr.constructor) {
case pure:
if (stack === null) return expr.value;
expr = stack.arrow(expr.value);
stack = stack.stack;
break;
case bind:
stack = { arrow: expr.arrow, stack };
expr = expr.monad;
break;
case thunk:
expr = expr.eval();
}
}
}
const monadic = func => thunk(() => {
const gen = func();
function next(data) {
const { value, done } = gen.next(data);
return done ? value : bind(value, next);
}
return next(undefined);
});
class Pair {
constructor(car, cdr) {
this.car = car;
this.cdr = cdr;
}
static fromArray(array) {
const loop = (array, index) => monadic(function* () {
if (index === array.length) return pure(null);
const item = array[index];
const car = Array.isArray(item) ? yield loop(item, 0) : item;
const cdr = yield loop(array, index + 1);
return pure(new Pair(car, cdr));
});
return evaluate(loop(array, 0));
}
duplicates() {
const visited = new WeakSet();
const result = new WeakSet();
const loop = pair => monadic(function* () {
if (visited.has(pair)) {
result.add(pair);
} else {
visited.add(pair);
const { car, cdr } = pair;
if (car instanceof Pair) yield loop(car);
if (cdr instanceof Pair) yield loop(cdr);
}
return pure(result);
});
return evaluate(loop(this));
}
toString() {
let result = "";
const duplicates = this.duplicates();
const visited = [];
const loop = (pair, end) => monadic(function* () {
const index = visited.indexOf(pair);
if (index < 0) {
const duplicate = duplicates.has(pair);
if (duplicate) {
const last = visited.push(pair) - 1;
result += end ? ` . #${last}=(` : `#${last}=(`;
} else result += end ? " " : "(";
const { car, cdr } = pair;
if (car instanceof Pair) yield loop(car, false);
else result += JSON.stringify(car);
if (cdr instanceof Pair) yield loop(cdr, true);
else if (cdr === null) result += ")";
else result += ` . ${JSON.stringify(cdr)})`;
if (duplicate && end) result += ")";
} else {
result += end ? ` . #${index}#)` : `#${index}#`;
}
return pure(result);
});
return evaluate(loop(this, false));
}
}
const data = new Pair(1, new Pair(new Pair(2, null), new Pair(3, null)));
data.cdr.car.cdr = data.cdr;
data.cdr.cdr.cdr = data;
console.log(data.toString());
const range = length => Array.from({ length }, (x, i) => i);
console.log(Pair.fromArray([[[range(8000), range(10)]]]).toString());
Hope that helps.
I have a trouble reducing the cognitive complexity here for this function.I tried to separate the contents inside the forEach as another function and by calling it in getCars function but failed. Could anyone please help?
const getCars = (cars, config, types) => {
const {
carName
} = types;
const carObject = {};
const carsRange = () => {}
let carRange = carsRange(cars);
Object.entries(cars).forEach(([key, value]) => {
if (key === 'sedan' && value) {
const carRangeVal = value.split(' ');
const year = carRangeVal[1];
const model = carRangeVal[0].substring(1, 2);
carRange = generateCarRange(year, model);
}
if (key === 'suv' && value) {
const carRangeVal = value.split(' ');
const year = carRangeVal[1];
const model = carRangeVal[0];
carObject['model'] = true;
carRange = checkYear(year, model);
}
if (value) {
carObject[key] = value;
}
});
if (
config.header === 'TEST A' ||
config.header === 'TEST B'
) {
carObject['carName'] = carName[0].id;
}
carObject['configName'] = config.header;
carObject['contractStartDate'] = carsRange[0];
carObject['contractEndDate'] = carsRange[1];
return carObject;
};
console.log(getCars({}, {}, {}));
You could shorten the code in the .forEach function:
[year,model] = value.split(' '); // destructuring assignment
if (key === 'sedan' && value) {
model = model.substring(1, 2);
carRange = generateCarRange(year, model);
}
if (key === 'suv' && value) {
carObject['model'] = true;
carRange = checkYear(year, model);
}
It would be helpful to know the input data.
I have an inkling that you may be better off using .map() instead of .forEach().
Why do you have two functions (generateCarRange() and checkYear()) to get carRange?
(I find it helpful to use Hungarian Notation to always know the type of my variables.)
I need to convert "flat object" like this (input data):
{
'prop1': 'value.1',
'prop2-subprop1': 'value.2.1',
'prop2-subprop2': 'value.2.2',
}
to immersion object like this (output data):
{
'prop1': 'value.1',
'prop2': {
'subprop1': 'value.2.1',
'subprop2': 'value.2.2'
}
}
Of course solution have to be prepare for no-limit deep level.
My solution does not work:
var inputData = {
'prop1': 'value.1',
'prop2-subprop1': 'value.2.1',
'prop2-subprop2': 'value.2.2',
};
function getImmersionObj(input, value) {
var output = {};
if ($.type(input) === 'object') { // first start
$.each(input, function (prop, val) {
output = getImmersionObj(prop.split('-'), val);
});
} else if ($.type(input) === 'array') { // recursion start
$.each(input, function (idx, prop) {
output[prop] = output[prop] || {};
output = output[prop];
});
}
return output;
}
console.log(getImmersionObj(inputData)); // return empty object
Can you help me find the problem in my code or maybe you know another one, better algorithm for conversion like my?
You could use a function for spliting the path to the value and generate new objects for it.
function setValue(object, path, value) {
var way = path.split('-'),
last = way.pop();
way.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = value;
}
var object = { 'prop1': 'value.1', 'prop2-subprop1': 'value.2.1', 'prop2-subprop2': 'value.2.2' };
Object.keys(object).forEach(function (key) {
if (key.indexOf('-') !== -1) {
setValue(object, key, object[key]);
delete object[key];
}
});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
var input = {
"property1": "value1",
"property2.property3": "value2",
"property2.property7": "value4",
"property4.property5.property6.property8": "value3"
}
function addProp(obj, path, pathValue) {
var pathArray = path.split('.');
pathArray.reduce(function (acc, value, index) {
if (index === pathArray.length - 1) {
acc[value] = pathValue;
return acc;
} else if (acc[value]) {
if (typeof acc[value] === "object" && index !== pathArray.length - 1) {
return acc[value];
} else {
var child = {};
acc[value] = child;
return child;
}
} else {
var child = {};
acc[value] = child;
return child;
}
}, obj);
}
var keys = Object.keys(input);
var output = {};
keys.forEach(function (k) {
addProp(output, k, input[k]);
});
console.log(output);
I'm trying to create a JavaScript function that creates an object using strings for structure and fills it from DOM data.
For example, the following strings could look like this:
some.example.here = "hello"
some.example.there = "hi"
other.example = "heyo"
Which should create this object:
{
some: {
example: {
here: "hello",
there: "hi"
},
other: {
example: "heyo
}
}
The data as said comes from DOM and is being load at the code segment labeled "read data into object". The data loads fine and the object structure is being setup fine as well, but the data is not being put into the data field.
Here's the code for the function:
function getDataFromElement(element) {
obj = {};
$(element)
.find("[data-value]")
.each(function() {
// create object node
valueObj = {};
currentValueObj = valueObj;
$.each($(this).attr("data-value").split("."), function(i, objpath) {
currentValueObj[objpath] = {};
currentValueObj = currentValueObj[objpath];
});
// read data into object
if($(this).is("[data-putvalue]") && $(this).attr("data-putvalue") != "html") {
currentValueObj = $(this).attr($(this).attr("data-putvalue"));
} else {
currentValueObj = $(this).html();
}
console.log(currentValueObj);
// combine with previous gathered data
obj = $.extend(true, {}, obj, valueObj);
});
return obj;
}
Does anyone know what to do?
I would do it like this:
var createObject = function(model, name, value) {
var nameParts = name.split("."),
currentObject = model;
for (var i in nameParts) {
var part = nameParts[i];
if (i == nameParts.length-1) {
currentObject[part] = value;
break;
}
if (typeof currentObject[part] == "undefined") {
currentObject[part] = {};
}
currentObject = currentObject[part];
}
};
And then use it like that:
var model = {};
createObject(model, "some.example.here", "hello");
createObject(model, "some.example.there", "hi");
createObject(model, "other.example", "heyo");
Probably this can suit you (adapted from another project of mine, adapt and use as needed):
NOTE the element's name is taken as key and value as the value
function fields2model( $elements, dataModel )
{
$elements.each(function( ){
var $el = $(this),
name = $el.attr('name'),
key, k, i, o, val
;
key = name;
val = $el.val() || '';
k = key.split('.'); o = dataModel;
while ( k.length )
{
i = k.shift( );
if ( k.length )
{
if ( !o.hasOwnProperty( i ) ) o[ i ] = /^\d+$/.test( k[0] ) ? [ ] : { };
o = o[ i ];
}
else
{
o[ i ] = val;
}
}
});
}
Example use:
<input name="some.example.here" value="hello" />
<input name="some.example.there" value="hi" />
var model = {};
fields2model($('input,textarea,select'), model);
The example elements above will give the below model:
model = {
some: {
example: {
here: "hello",
there: "hi"
}
};
Some functional implementation:
const value = 'hello';
'some.example.here'.split('.').reverse().reduce((reduction, segment, index) => {
const result = {};
if (index === 0) {
result[segment] = value;
} else {
result[segment] = reduction;
}
return result;
}, {})
#theFreedomBanana +1
Works for me
const magicFunction = (string, value) =>
string
.split('.')
.reverse()
.reduce((acc, cur, index) => ({ [cur]: index === 0 ? value : acc }), {});
I have a json object which looks like this:
var testJ = {"ROOT":{
dir : 'app',
files : [
'index.html',
{
dir : 'php',
files: [
'a.php',
{
dir : 'extras',
files : [
'a.js',
'b.js'
]
}
]
}
]
}};
I need to extract all the files and append into an array (index.html,a.php,a.js..etc)
For this I wrote a javascript code as follows:
var arr=[];
function scan(obj,append)
{
var k;
if (obj instanceof Object) {
for (k in obj){
if (obj.hasOwnProperty(k)){
if(k=='files')
{
scan( obj[k],1 );
}
}
}
} else {
body += 'found value : ' + obj + '<br/>';
if(append == 1)
arr.push(obj);
alert("Arr"+ arr);
};
};
scan(testJ,0);
I am not able to figure out where am I going wrong. Could some give me pointers?
var res = [];
function gather(j) {
for (var k in j) {
if (k === 'files') {
addFiles(j[k]);
} else if (typeof j[k] === 'object') {
gather(j[k]);
}
}
}
function addFiles(f) {
for (var i = 0; i < f.length; i++) {
if (typeof f[i] === "string") {
body += 'found value : ' + obj + '<br/>';
res.push(f[i]);
} else {
gather(f[i]);
}
}
}
gather(testJ);
Free tips:
instanceof is some cancerous stuff. Why does instanceof return false for some literals?
Always, always use === for comparison, not ==
I also wouldn't blindly use hasOwnProperty unless you're afraid the thing you're operating on might have a modified prototype, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in just for simplicity.
How about a map reduce approach?:
function mapJ(subject) {
return subject.files.map(function(item) {
if (typeof item === "string") {
return item;
} else {
return parseJ(item);
}
});
}
function reduceJ(subject) {
return subject.reduce(function(prev, cur) {
return prev.concat(cur);
}, []);
}
function parseJ(subject) {
return reduceJ(mapJ(subject));
}
var result = parseJ(testJ));