Javascript: Determine Unknown Array Length And Map Dynamically
Solution 1:
As mentioned in the comments, there is no strict definition of the input format, it is hard to do it with perfect error handling and handle all corner cases.
Here is my lengthy implementation that works on your sample, but might fail for some other cases:
functionmerge_objects(a, b) {
var c = {}, attr;
for (attr in a) { c[attr] = a[attr]; }
for (attr in b) { c[attr] = b[attr]; }
return c;
}
var id = {
inner: null,
name: "id",
repr: "id",
type: "map",
exec: function (input) { return input; }
};
// set output fieldfunctionf(outp, mapper) {
mapper = typeof mapper !== "undefined" ? mapper : id;
var repr = "f("+outp+","+mapper.repr+")";
var name = "f("+outp;
return {
inner: mapper,
name: name,
repr: repr,
type: "map",
clone: function(mapper) { returnf(outp, mapper); },
exec:
function (input) {
var out = {};
out[outp] = mapper.exec(input);
return out;
}
};
}
// set input fieldfunctionp(inp, mapper) {
var repr = "p("+inp+","+mapper.repr+")";
var name = "p("+inp;
return {
inner: mapper,
name: name,
repr: repr,
type: mapper.type,
clone: function(mapper) { returnp(inp, mapper); },
exec: function (input) {
return mapper.exec(input[inp]);
}
};
}
// process arrayfunctionarr(mapper) {
var repr = "arr("+mapper.repr+")";
return {
inner: mapper,
name: "arr",
repr: repr,
type: mapper.type,
clone: function(mapper) { returnarr(mapper); },
exec: function (input) {
var out = [];
for (var i=0; i<input.length; i++) {
out.push(mapper.exec(input[i]));
}
return out;
}
};
}
functioncombine(m1, m2) {
var type = (m1.type == "flatmap" || m2.type == "flatmap") ? "flatmap" : "map";
var repr = "combine("+m1.repr+","+m2.repr+")";
return {
inner: null,
repr: repr,
type: type,
name: "combine",
exec:
function (input) {
var out1 = m1.exec(input);
var out2 = m2.exec(input);
var out, i, j;
if (m1.type == "flatmap" && m2.type == "flatmap") {
out = [];
for (i=0; i<out1.length; i++) {
for (j=0; j<out2.length; j++) {
out.push(merge_objects(out1[i], out2[j]));
}
}
return out;
}
if (m1.type == "flatmap" && m2.type != "flatmap") {
out = [];
for (i=0; i<out1.length; i++) {
out.push(merge_objects(out1[i], out2));
}
return out;
}
if (m1.type != "flatmap" && m2.type == "flatmap") {
out = [];
for (i=0; i<out2.length; i++) {
out.push(merge_objects(out2[i], out1));
}
return out;
}
returnmerge_objects(out1, out2);
}
};
}
functionflatmap(mapper) {
var repr = "flatmap("+mapper.repr+")";
return {
inner: mapper,
repr: repr,
type: "flatmap",
name: "flatmap",
clone: function(mapper) { returnflatmap(mapper); },
exec:
function (input) {
var out = [];
for (var i=0; i<input.length; i++) {
out.push(mapper.exec(input[i]));
}
return out;
}
};
}
functionsplit(s, t) {
var i = s.indexOf(t);
if (i == -1) returnnull;
else {
return [s.slice(0, i), s.slice(i+2, s.length)];
}
}
functioncompile_one(inr, outr) {
inr = (inr.charAt(0) == ".") ? inr.slice(1, inr.length) : inr;
outr = (outr.charAt(0) == ".") ? outr.slice(1, outr.length) : outr;
var box = split(inr, "[]");
var box2 = split(outr, "[]");
var m, ps, fs, i, j;
if (box == null && box2 == null) { // no array!
m = id;
ps = inr.split(".");
fs = outr.split(".");
for (i=0; i<fs.length; i++) { m = f(fs[i], m); }
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
if (box != null && box2 != null) { // array on both sides
m = arr(compile_one(box[1], box2[1]));
ps = box[0].split(".");
fs = box[0].split(".");
for (i=0; i<fs.length; i++) { m = f(fs[i], m); }
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
if (box != null && box2 == null) { // flatmap
m = flatmap(compile_one(box[1], outr));
ps = box[0].split(".");
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
returnnull;
}
functionmerge_rules(m1, m2) {
if (m1 == null) return m2;
if (m2 == null) return m1;
if (m1.name == m2.name && m1.inner != null) {
return m1.clone(merge_rules(m1.inner, m2.inner));
} else {
returncombine(m1, m2);
}
}
var input = {
store: "myStore",
items: [
{name: "Hammer", skus:[{num:"12345qwert"}]},
{name: "Bike", skus:[{num:"asdfghhj"}, {num:"zxcvbn"}]},
{name: "Fork", skus:[{num:"0987dfgh"}]}
]
};
var m1 = compile_one("items[].name", "items[].name");
var m2 = compile_one("items[].skus[].num", "items[].sku");
var m3 = compile_one("store", "storeName");
var m4 = merge_rules(m3,merge_rules(m1, m2));
var out = m4.exec(input);
alert(JSON.stringify(out));
Solution 2:
I have borrowed earlier answer and made improvements so as to solve both your examples and this should be generic. Though if you plan to run this sequencially with 2 sets of inputs, then the behavior will be as I have outlined in my comments to your original question.
var apiObj = {
items: [{
name: "Hammer",
skus: [{
num: "12345qwert"
}]
}, {
name: "Bike",
skus: [{
num: "asdfghhj"
}, {
num: "zxcvbn"
}]
}, {
name: "Fork",
skus: [{
num: "0987dfgh"
}]
}]
};
var myObj = { //Previously has valuesstoreName: "",
items: [{
uniqueName: ""
}],
outputModel: {
items: [{
name: "Hammer"
}]
}
};
/** Also works with this **
var myPath = "outputModel.items[].uniqueName";
var apiPath = "items[].name";
*/var myPath = "outputModel.items[].sku";
var apiPath = "items[].skus[].num";
functionmake_accessor(program) {
returnfunction (obj, callback) {
(functiondo_segment(obj, segments) {
var start = segments.shift() // Get first segmentvar pieces = start.match(/(\w+)(\[\])?/); // Get name and [] piecesvar property = pieces[1];
var isArray = pieces[2]; // [] on end
obj = obj[property]; // drill downif (!segments.length) { // last segment; callbackif (isArray) {
return obj.forEach(callback);
} else {
returncallback(obj);
}
} else { // more segments; recurseif (isArray) { // array--loop over elts
obj.forEach(function (elt) {
do_segment(elt, segments.slice());
});
} else {
do_segment(obj, segments.slice()); // scalar--continue
}
}
})(obj, program.split('.'));
};
}
functionmake_inserter(program) {
returnfunction (obj, value) {
(functiondo_segment(obj, segments) {
var start = segments.shift() // Get first segmentvar pieces = start.match(/(\w+)(\[\])?/); // Get name and [] piecesvar property = pieces[1];
var isArray = pieces[2]; // [] on endif (segments.length) { // more segmentsif (!obj[property]) {
obj[property] = isArray ? [] : {};
}
do_segment(obj[property], segments.slice());
} else { // last segmentif (Array.isArray(obj)) {
var addedInFor = false;
for (var i = 0; i < obj.length; i++) {
if (!(property in obj[i])) {
obj[i][property] = value;
addedInFor = true;
break;
}
}
if (!addedInFor) {
var entry = {};
entry[property] = value;
obj.push(entry);
}
} else obj[property] = value;
}
})(obj, program.split('.'));
};
}
access = make_accessor(apiPath);
insert = make_inserter(myPath);
access(apiObj, function (val) {
insert(myObj, val);
});
console.log(myObj);
Solution 3:
(old solution:https://jsfiddle.net/d7by0ywy/):
Here is my new generalized solution when you know the two objects to process in advance (called inp
and out
here). If you don't know them in advance you can use the trick in the old solution to assign the objects on both sides of =
to inp
and out
(https://jsfiddle.net/uxdney3L/3/).
Restrictions: There has to be the same amount of arrays on both sides and an array has to contain objects. Othewise it would be ambiguous, you would have to come up with a better grammar to express rules (or why don't you have functions instead of rules?) if you want it to be more sophisticated.
Example of ambiguity:out.items[].sku=inp[].skus[].num
Do you assign an array of the values of num
to sku
or do you assign an array of objects with the num
property?
Data:
rules = [
'out.items[].name=inp[].name',
'out.items[].sku[].num=inp[].skus[].num'
];inp = [{
'name': 'Hammer',
'skus':[{'num':'12345qwert','test':'ignore'}]
},{
'name': 'Bike',
'skus':[{'num':'asdfghhj'},{'num':'zxcvbn'}]
},{
'name': 'Fork',
'skus':[{'num':'0987dfgh'}]
}];
Program:
function process() {
if (typeofout == 'undefined') {
out = {};
}
var j, r;
for (j = 0; j < rules.length; j++) {
r = rules[j].split('=');
if (r.length != 2) {
console.log('invalid rule: symbol "=" is expected exactly once');
} elseif (r[0].substr(0, 3) != 'out' || r[1].substr(0, 3) != 'inp') {
console.log('invalid rule: expected "inp...=out..."');
} else {
processRule(r[0].substr(3).split('[]'), r[1].substr(3).split('[]'), 0, inp, out);
}
}
}
function processRule(l, r, n, i, o) { // left, right, index, in, outvar t = r[n].split('.');
for (var j = 0; j < t.length; j++) {
if (t[j] != '') {
i = i[t[j]];
}
}
t = l[n].split('.');
if (n < l.length - 1) {
for (j = 0; j < t.length - 1; j++) {
if (t[j] != '') {
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = {};
}
o = o[t[j]];
}
}
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = [];
}
o = o[t[j]];
for (j = 0; j < i.length; j++) {
if (typeof o[j] == 'undefined') {
o[j] = {};
}
processRule(l, r, n + 1, i[j], o[j]);
}
} else {
for (j = 0; j < t.length - 1; j++) {
if (t[j] != '') {
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = {};
}
o = o[t[j]];
}
}
o[t[j]] = i;
}
}
process();
console.log(out);
Solution 4:
Well, an interesting problem. Programmatically constructing nested objects from a property accessor string (or the reverse) isn't much of a problem, even doing so with multiple descriptors in parallel. Where it does get complicated are arrays, which require iteration; and that isn't as funny any more when it gets to different levels on setter and getter sides and multiple descriptor strings in parallel.
So first we need to distinguish the array levels of each accessor description in the script, and parse the text:
functionparse(script) {
return script.split(/\s*[;\r\n]+\s*/g).map(function(line) {
var assignment = line.split(/\s*=\s*/);
return assignment.length == 2 ? assignment : null; // console.warn ???
}).filter(Boolean).map(function(as) {
as = as.map(function(accessor) {
var parts = accessor.split("[]").map(function(part) {
return part.split(".");
});
for (var i=1; i<parts.length; i++) {
// assert(parts[i][0] == "")var prev = parts[i-1][parts[i-1].length-1];
parts[i][0] = prev.replace(/s$/, ""); // singular :-)
}
return parts;
});
if (as[0].length == 1 && as[1].length > 1) // getter contains array but setter does notas[0].unshift(["output"]); // implicitly return array (but better throw an error)return {setter:as[0], getter:as[1]};
});
}
With that, the textual input can be made into a usable data structure, and now looks like this:
[{"setter":[["outputModel","items"],["item","name"]],
"getter":[["items"],["item","name"]]},
{"setter":[["outputModel","items"],["item","sku"]],
"getter":[["items"],["item","skus"],["sku","num"]]}]
The getters already transform nicely into nested loops like
for (item of items)
for (sku of item.skus)
… sku.num …;
and that's exactly where we are going to. Each of those rules is relatively easy to process, copying properties on objects and iterating array for array, but here comes our most crucial issue: We have multiple rules. The basic solution when we deal with iterating multiple arrays is to create their cartesian product and this is indeed what we will need. However, we want to restrict this a lot - instead of creating every combination of all name
s and all num
s in the input, we want to group them by the item
that they come from.
To do so, we'll build some kind of prefix tree for our output structure that'll contain generators of objects, each of those recursivley being a tree for the respective output substructure again.
functionmultiGroupBy(arr, by) {
return arr.reduce(function(res, x) {
var p = by(x);
(res[p] || (res[p] = [])).push(x);
return res;
}, {});
}
functiongroup(rules) {
var paths = multiGroupBy(rules, function(rule) {
return rule.setter[0].slice(1).join(".");
});
var res = [];
for (var path in paths) {
var pathrules = paths[path],
array = [];
for (var i=0; i<pathrules.length; i++) {
var rule = pathrules[i];
var comb = 1 + rule.getter.length - rule.setter.length;
if (rule.setter.length > 1) // its an arrayarray.push({
generator: rule.getter.slice(0, comb),
next: {
setter: rule.setter.slice(1),
getter: rule.getter.slice(comb)
}
})
elseif (rule.getter.length == 1 && i==0)
res.push({
set: rule.setter[0],
get: rule.getter[0]
});
else
console.error("invalid:", rule);
}
if (array.length)
res.push({
set: pathrules[0].setter[0],
cross: product(array)
});
}
return res;
}
functionproduct(pathsetters) {
var groups = multiGroupBy(pathsetters, function(pathsetter) {
return pathsetter.generator[0].slice(1).join(".");
});
var res = [];
for (var genstart in groups) {
var creators = groups[genstart],
nexts = [],
nests = [];
for (var i=0; i<creators.length; i++) {
if (creators[i].generator.length == 1)
nexts.push(creators[i].next);
else
nests.push({path:creators[i].path, generator: creators[i].generator.slice(1), next:creators[i].next});
}
res.push({
get: creators[0].generator[0],
cross: group(nexts).concat(product(nests))
});
}
return res;
}
Now, our ruleset group(parse(script))
looks like this:
[{"set":["outputModel","items"],"cross":[{"get":["items"],"cross":[{"set":["item","name"],"get":["item","name"]},{"get":["item","skus"],"cross":[{"set":["item","sku"],"get":["sku","num"]}]}]}]}]
and that is a structure we can actually work with, as it now clearly conveys the intention on how to match together all those nested arrays and the objects within them. Let's dynamically interpret this, building an output for a given input:
function transform(structure, input, output) {
for (var i=0; i<structure.length; i++) {
output = assign(output, structure[i].set.slice(1), getValue(structure[i], input));
}
return output;
}
function retrieve(val, props) {
return props.reduce(function(o, p) { return o[p]; }, val);
}
function assign(obj, props, val) {
if (!obj)
if (!props.length) returnval;
else obj = {};
for (var j=0, o=obj; j<props.length-1 && o!=null && o[props[j]]; o=o[props[j++]]);
obj[props[j]] = props.slice(j+1).reduceRight(function(val, p) {
var o = {};
o[p] = val;
return o;
}, val);
return obj;
}
function getValue(descriptor, input) {
if (descriptor.get) // && !crossreturn retrieve(input, descriptor.get.slice(1));
var arr = [];
descriptor.cross.reduce(function horror(next, d) {
if (descriptor.set)
return function (inp, cb) {
next(inp, function(res){
cb(assign(res, d.set.slice(1), getValue(d, inp)));
});
};
else// its a crosserreturn function(inp, cb) {
var g = retrieve(inp, d.get.slice(1)),
e = d.cross.reduce(horror, next)
for (var i=0; i<g.length; i++)
e(g[i], cb);
};
}, function innermost(inp, cb) {
cb(); // start to create an item
})(input, function(res) {
arr.push(res); // store the item
});
return arr;
}
And this does indeed work with
var result = transform(group(parse(script)), items); // your expected result
But we can do better, and much more performant:
function compile(structure) {
function make(descriptor) {
if (descriptor.get)
return {inputName: descriptor.get[0], output: descriptor.get.join(".") };
var outputName = descriptor.set[descriptor.set.length-1];
var loops = descriptor.cross.reduce(function horror(next, descriptor) {
if (descriptor.set)
return function(it, cb) {
return next(it, function(res){
res.push(descriptor)
return cb(res);
});
};
else// its a crosserreturn function(it, cb) {
var arrName = descriptor.get[descriptor.get.length-1],
itName = String.fromCharCode(it);
varinner = descriptor.cross.reduce(horror, next)(it+1, cb);
return {
inputName: descriptor.get[0],
statement: (descriptor.get.length>1 ? "var "+arrName+" = "+descriptor.get.join(".")+";\n" : "")+
"for (var "+itName+" = 0; "+itName+" < "+arrName+".length; "+itName+"++) {\n"+
"var "+inner.inputName+" = "+arrName+"["+itName+"];\n"+
inner.statement+
"}\n"
};
};
}, function(_, cb) {
return cb([]);
})(105, function(res) {
var item = joinSetters(res);
return {
inputName: item.inputName,
statement: (item.statement||"")+outputName+".push("+item.output+");\n"
};
});
return {
statement: "var "+outputName+" = [];\n"+loops.statement,
output: outputName,
inputName: loops.inputName
};
}
function joinSetters(descriptors) {
if (descriptors.length == 1 && descriptors[0].set.length == 1)
return make(descriptors[0]);
var paths = multiGroupBy(descriptors, function(d){ return d.set[1] || console.error("multiple assignments on "+d.set[0], d); });
var statements = [],
inputName;
var props = Object.keys(paths).map(function(p) {
var d = joinSetters(paths[p].map(function(d) {
var names = d.set.slice(1);
names[0] = d.set[0]+"_"+names[0];
return {set:names, get:d.get, cross:d.cross};
}));
inputName = d.inputName;
if (d.statement)
statements.push(d.statement)
return JSON.stringify(p) + ": " + d.output;
});
return {
inputName: inputName,
statement: statements.join(""),
output: "{"+props.join(",")+"}"
};
}
var code = joinSetters(structure);
return new Function(code.inputName, code.statement+"return "+code.output+";");
}
So here is what you will get in the end:
> varexample= compile(group(parse("outputModel.items[].name = items[].name;outputModel.items[].sku = items[].skus[].num;")))
function(items) {
varoutputModel_items= [];
for (vari=0; i < items.length; i++) {
varitem= items[i];
varskus= item.skus;
for (varj=0; j < skus.length; j++) {
varsku= skus[j];
outputModel_items.push({"name": item.name,"sku": sku.num});
}
}
return {"items": outputModel_items};
}
> varflatten= compile(group(parse("as[]=bss[][]")))
function(bss) {
varas= [];
for (vari=0; i < bss.length; i++) {
varbs= bss[i];
for (varj=0; j < bs.length; j++) {
varb= bs[j];
as.push(b);
}
}
return as;
}
> varparallelRecords= compile(group(parse("x.as[]=y[].a; x.bs[]=y[].b")))
function(y) {
varx_as= [];
for (vari=0; i < y.length; i++) {
vary= y[i];
x_as.push(y.a);
}
varx_bs= [];
for (vari=0; i < y.length; i++) {
vary= y[i];
x_bs.push(y.b);
}
return {"as": x_as,"bs": x_bs};
}
And now you can easily pass your input data to that dynamically created function and it will be transformed quite fast :-)
Post a Comment for "Javascript: Determine Unknown Array Length And Map Dynamically"