Javascript: déterminez la longueur et la taille du tableau inconnus de manière dynamique

Je vais faire de mon mieux pour expliquer ce que j'essaie de faire.

J'ai deux modèles, le mien et une réponse api que je reçois. Lorsque les éléments de la réponse d'api entrent, je dois le mapper sur mon modèle et insérer tous les éléments. C'est évidemment bien sûr. Le problème, je dois le faire sans vraiment savoir ce dont je suis confronté. Mon code sera transmis en deux chaînes, l'un de mes modèles de chemin d'accès et l'un des chemins de recherche de réponse d'api.

Voici les deux chemins

var myPath = "outputModel.items[].uniqueName" var apiPath = "items[].name" 

Fondamentalement POUR tous les items dans apiPath , appuyez sur les items dans myPath et définissez sur uniqueName

Ce se résume à ce que mon code n'a aucune idée lorsque deux éléments doivent être mappés, ou même s'ils contiennent un tableau ou un champ simple pour les chemins de terrain. Ils pourraient même contenir plusieurs tableaux, comme ceci:

******************** EXEMPLE *************************

 var items = [ { name: "Hammer", skus:[ {num:"12345qwert"} ] }, { name: "Bike", skus:[ {num:"asdfghhj"}, {num:"zxcvbn"} ] }, { name: "Fork", skus:[ {num:"0987dfgh"} ] } ] var outputModel = { storeName: "", items: [ { name: "", sku:"" } ] }; outputModel.items[].name = items[].name; outputModel.items[].sku = items[].skus[].num; 

************************ Voici le résultat attendu ci-dessus

 var result = { storeName: "", items: [ { name: "Hammer", sku:"12345qwert" }, { name: "Bike", sku:"asdfghhj" }, { name: "Bike", sku:"zxcvbn" }, { name: "Fork", sku:"0987dfgh" } ] }; 

On vous donnera un ensemble de chemins pour chaque valeur à mapper. Dans le cas ci-dessus, j'ai été remis deux ensembles de chemins car je mappage deux valeurs. Il faudrait traverser les deux ensembles de tableaux pour créer le tableau unique dans mon modèle.

Question – Comment puis-je détecter dynamiquement les tableaux et déplacer les données correctement, quel que soit l'aspect des deux modèles? Possible?

Comme mentionné dans les commentaires, il n'y a pas de définition stricte du format d'entrée, il est difficile de le faire avec une gestion parfaite des erreurs et gérer tous les cas d'angle.

Voici ma longue implémentation qui fonctionne sur votre échantillon, mais pourrait échouer pour d'autres cas:

 function merge_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 field function f(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) { return f(outp, mapper); }, exec: function (input) { var out = {}; out[outp] = mapper.exec(input); return out; } }; } // set input field function p(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) { return p(inp, mapper); }, exec: function (input) { return mapper.exec(input[inp]); } }; } // process array function arr(mapper) { var repr = "arr("+mapper.repr+")"; return { inner: mapper, name: "arr", repr: repr, type: mapper.type, clone: function(mapper) { return arr(mapper); }, exec: function (input) { var out = []; for (var i=0; i<input.length; i++) { out.push(mapper.exec(input[i])); } return out; } }; } function combine(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; } return merge_objects(out1, out2); } }; } function flatmap(mapper) { var repr = "flatmap("+mapper.repr+")"; return { inner: mapper, repr: repr, type: "flatmap", name: "flatmap", clone: function(mapper) { return flatmap(mapper); }, exec: function (input) { var out = []; for (var i=0; i<input.length; i++) { out.push(mapper.exec(input[i])); } return out; } }; } function split(s, t) { var i = s.indexOf(t); if (i == -1) return null; else { return [s.slice(0, i), s.slice(i+2, s.length)]; } } function compile_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; } return null; } function merge_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 { return combine(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)); 

Vous avez donc défini un petit langage pour définir certaines règles d'adressage et de manipulation de données. Pensons à une approche qui vous permettra de dire

 access(apiPath, function(value) { insert(myPath, value); } 

La fonction d' access trouve tous les éléments requis dans apiPath , puis rappelle l' insert , qui les insère dans myPath . Notre travail consiste à écrire des fonctions qui créent les fonctions d' access et d' insert ; Ou, vous pouvez dire, "compiler" votre petit langage dans des fonctions que nous pouvons exécuter.

Nous écrirons les "compilateurs" appelés make_accessor et make_inserter , comme suit:

 function make_accessor(program) { return function(obj, callback) { return function do_segment(obj, segments) { var start = segments.shift() // Get first segment var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces var property = pieces[1]; var isArray = pieces[2]; // [] on end obj = obj[property]; // drill down if (!segments.length) { // last segment; callback if (isArray) { return obj.forEach(callback); } else { return callback(obj); } } else { // more segments; recurse if (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('.')); }; } 

Nous pouvons maintenant faire un accessoire en appelant make_accessor('items[].name') .

Ensuite, écrivons l'insert:

 function make_inserter(program) { return function(obj, value) { return function do_segment(obj, segments) { var start = segments.shift() // Get first segment var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces var property = pieces[1]; var isArray = pieces[2]; // [] on end if (segments.length) { // more segments if (!obj[property]) { obj[property] = isArray ? [] : {}; } do_segment(obj, segments.slice()); } else { // last segment obj[property] = value; } }(obj, program.split('.')); }; } 

Maintenant, vous pouvez exprimer toute votre logique en tant que

 access = make_accessor('items[].name'); insert = make_inserter('outputModel.items[].uniqueName'); access(apiPath, function(val) { insert(myPath, val); }); 

J'ai emprunté une réponse antérieure et j'ai apporté des améliorations afin de résoudre vos deux exemples et cela devrait être générique. Bien que si vous prévoyez de l'exécuter séquentiellement avec 2 ensembles d'entrées, le comportement sera comme je l'ai décrit dans mes commentaires à votre question initiale.

  var apiObj = { items: [{ name: "Hammer", skus: [{ num: "12345qwert" }] }, { name: "Bike", skus: [{ num: "asdfghhj" }, { num: "zxcvbn" }] }, { name: "Fork", skus: [{ num: "0987dfgh" }] }] }; var myObj = { //Previously has values storeName: "", 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"; function make_accessor(program) { return function (obj, callback) { (function do_segment(obj, segments) { var start = segments.shift() // Get first segment var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces var property = pieces[1]; var isArray = pieces[2]; // [] on end obj = obj[property]; // drill down if (!segments.length) { // last segment; callback if (isArray) { return obj.forEach(callback); } else { return callback(obj); } } else { // more segments; recurse if (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('.')); }; } function make_inserter(program) { return function (obj, value) { (function do_segment(obj, segments) { var start = segments.shift() // Get first segment var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces var property = pieces[1]; var isArray = pieces[2]; // [] on end if (segments.length) { // more segments if (!obj[property]) { obj[property] = isArray ? [] : {}; } do_segment(obj[property], segments.slice()); } else { // last segment if (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); 

( Ancienne solution: https://jsfiddle.net/d7by0ywy/ ):

Voici ma nouvelle solution généralisée lorsque vous connaissez les deux objets à traiter en avance (appelé inp et ici). Si vous ne les connaissez pas à l'avance, vous pouvez utiliser l'astuce dans l'ancienne solution pour attribuer les objets des deux côtés de = to inp and out ( https://jsfiddle.net/uxdney3L/3/ ).

Restrictions: Il doit y avoir la même quantité de tableaux des deux côtés et un tableau doit contenir des objets. Sinon, cela serait ambigu, il faudrait trouver une meilleure grammaire pour exprimer des règles (ou pourquoi ne pas avoir des fonctions au lieu de règles?) Si vous voulez qu'il soit plus sophistiqué.

Exemple d'ambiguïté: out.items[].sku=inp[].skus[].num Vous attribuez un tableau des valeurs de num à sku ou attribuez-vous un ensemble d'objets à la propriété num ?

Les données:

 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'}] }]; 

Programme:

 function process() { if (typeof out == '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'); } else if (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, out var 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); 

Eh bien, un problème intéressant. La construction par programmation de objets imbriqués à partir d'une chaîne d'accès de propriété (ou l'inverse ) n'est pas un problème, même si plusieurs descripteurs sont en parallèle . Lorsqu'il se complique, les tableaux, qui nécessitent une itération; Et ce n'est plus aussi drôle quand il arrive à différents niveaux sur setter et getter sides et plusieurs chaînes de descripteurs en parallèle.

Donc, il faut d'abord distinguer les niveaux de tableau de chaque description d'accesseur dans le script et analyser le texte:

 function parse(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 not as[0].unshift(["output"]); // implicitly return array (but better throw an error) return {setter:as[0], getter:as[1]}; }); } 

Avec cela, l'entrée textuelle peut être transformée en une structure de données utilisable, et se présente comme suit:

 [{"setter":[["outputModel","items"],["item","name"]], "getter":[["items"],["item","name"]]}, {"setter":[["outputModel","items"],["item","sku"]], "getter":[["items"],["item","skus"],["sku","num"]]}] 

Les getters se transforment déjà bien en boucles imbriquées comme

 for (item of items) for (sku of item.skus) … sku.num …; 

Et c'est exactement là où nous allons. Chacune de ces règles est relativement facile à traiter, à copier des propriétés sur des objets et à itérer le tableau pour le tableau, mais voici notre problème le plus crucial: nous avons plusieurs règles. La solution de base lorsque nous traitons l'itération de multiples tableaux est de créer leur produit cartésien et c'est en effet ce dont nous aurons besoin. Cependant, nous voulons restreindre cela beaucoup – au lieu de créer chaque combinaison de tous les name s et tous les num s dans l'entrée, nous voulons les regrouper par l' item qu'ils proviennent.

Pour ce faire, nous allons construire une sorte d'arborescence de préfixe pour notre structure de sortie qui contiendra des générateurs d'objets, chacun de ces récursivley étant un arbre pour la sous-structure de sortie respective à nouveau.

 function multiGroupBy(arr, by) { return arr.reduce(function(res, x) { var p = by(x); (res[p] || (res[p] = [])).push(x); return res; }, {}); } function group(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 array array.push({ generator: rule.getter.slice(0, comb), next: { setter: rule.setter.slice(1), getter: rule.getter.slice(comb) } }) else if (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; } function product(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; } 

Maintenant, notre group(parse(script)) règles group(parse(script)) ressemble à ceci:

 [{ "set": ["outputModel","items"], "cross": [{ "get": ["items"], "cross": [{ "set": ["item","name"], "get": ["item","name"] }, { "get": ["item","skus"], "cross": [{ "set": ["item","sku"], "get": ["sku","num"] }] }] }] }] 

Et c'est une structure avec laquelle nous pouvons réellement travailler, puisqu'elle indique clairement clairement l'intention de combiner ensemble tous les tableaux imbriqués et les objets qui s'y trouvent. En interprétons de manière dynamique, construisons une sortie pour une entrée donnée:

 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) return val; 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) // && !cross return 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 crosser return 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; } 

Et cela fonctionne bien avec

 var result = transform(group(parse(script)), items); // your expected result 

Mais nous pouvons faire mieux, et beaucoup plus 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 crosser return function(it, cb) { var arrName = descriptor.get[descriptor.get.length-1], itName = String.fromCharCode(it); var inner = 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+";"); } 

Voici donc ce que vous obtiendrez à la fin:

 > var example = compile(group(parse("outputModel.items[].name = items[].name;outputModel.items[].sku = items[].skus[].num;"))) function(items) { var outputModel_items = []; for (var i = 0; i < items.length; i++) { var item = items[i]; var skus = item.skus; for (var j = 0; j < skus.length; j++) { var sku = skus[j]; outputModel_items.push({"name": item.name,"sku": sku.num}); } } return {"items": outputModel_items}; } > var flatten = compile(group(parse("as[]=bss[][]"))) function(bss) { var as = []; for (var i = 0; i < bss.length; i++) { var bs = bss[i]; for (var j = 0; j < bs.length; j++) { var b = bs[j]; as.push(b); } } return as; } > var parallelRecords = compile(group(parse("x.as[]=y[].a; x.bs[]=y[].b"))) function(y) { var x_as = []; for (var i = 0; i < y.length; i++) { var y = y[i]; x_as.push(ya); } var x_bs = []; for (var i = 0; i < y.length; i++) { var y = y[i]; x_bs.push(yb); } return {"as": x_as,"bs": x_bs}; } 

Et maintenant, vous pouvez facilement transmettre vos données d'entrée à cette fonction créée dynamiquement et elle sera transformée assez rapidement 🙂