Javascript: определить неизвестную длину массива и динамически отображать

собираюсь сделать все возможное, чтобы объяснить, что я пытаюсь сделать.

у меня есть две модели, моя и ответ api, который я получаю. Когда приходит ответ api элементов, мне нужно сопоставить его с моей моделью и вставить все элементы. Это, конечно, просто. Вот в чем проблема, мне нужно сделать это, не зная, с чем я имею дело. Мой код будет передан в двух строках: один из путей сопоставления моделей и один из путей сопоставления ответов api.

вот два пути

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

в основном для всех items на apiPath, нажмите на items на myPath и установлено uniqueName

все сводится к тому, что мой код понятия не имеет, когда два элемента должны быть сопоставлены, или даже если они содержат массив или простое поле для путей к полю. Они могут даже содержать несколько массивов, например:

******************** пример *************************

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;

************************ вот ожидаемый результат выше

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

мне будет предоставлен набор путей для каждого значения, которое будет отображаться. В приведенном выше случае мне были переданы два набора путей, потому что я сопоставляю два значения. Для создания единого массива в моей модели ему пришлось бы пересечь оба набора массивов.

вопрос-Как я могу динамически обнаруживать массивы и правильно перемещать данные независимо от того, как выглядят два пути модели? Возможно?

5 ответов


Как упоминалось в комментариях, нет строгого определения формата ввода, это трудно сделать с идеальной обработкой ошибок и обрабатывать все угловые случаи.

вот моя длительная реализация, которая работает на вашем примере, но может потерпеть неудачу в некоторых других случаях:

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));

таким образом, вы определили небольшой язык для определения некоторых правил адресации и манипуляции данными. Давайте подумаем о подходе, который позволит вам сказать

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

на


я заимствовал более ранний ответ и сделал улучшения, чтобы решить оба ваших примера, и это должно быть общим. Хотя, если вы планируете запустить это последовательно с 2 наборами входных данных, то поведение будет таким, как я изложил в своих комментариях к вашему исходному вопросу.

    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);

(старое решение: https://jsfiddle.net/d7by0ywy/):

вот мой новый обобщенное решение когда вы знаете два объекта для обработки заранее (называется inp и out здесь). Если вы не знаете их заранее, вы можете использовать трюк в старом решении, чтобы назначить объекты с обеих сторон = до inp и out (https://jsfiddle.net/uxdney3L/3/).

ограничения: должно быть одинаковое количество массивов с обеих сторон и массив должен содержать объектов. В противном случае это было бы неоднозначно, вам пришлось бы придумать лучшую грамматику для выражения правил (или почему у вас нет функций вместо правил?) если вы хотите, чтобы он был более сложным.

пример двусмысленности: out.items[].sku=inp[].skus[].num назначаете ли вы массив значений num to sku или вы назначаете массив объектов с помощью num собственность?

данные:

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

Ну, интересная проблема. Программно построение вложенных объектов из строки доступа к свойству (или реверс) не большая проблема, даже делая это с несколько дескрипторов параллельно. Где это становится сложным, это массивы, которые требуют итерации; и это уже не так смешно, когда он попадает на разные уровни на сторонах сеттера и геттера и нескольких строк дескриптора параллельно.

так сначала нам нужно различить уровни массива каждого описания метода доступа в скрипте и проанализировать текст:

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

при этом текстовый ввод может быть сделан в полезную структуру данных, и теперь выглядит так:

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

геттеры уже хорошо преобразуются во вложенные циклы, такие как

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

и именно туда мы и направляемся. Каждое из этих правил относительно легко обрабатывать, копируя свойства объектов и повторяя массив для array, но вот наша самая важная проблема: у нас есть несколько правил. Основным решением, когда мы имеем дело с итерацией нескольких массивов, является создание их декартова произведения и это действительно то, что нам нужно. Однако, мы хотим ограничить это много - вместо того, чтобы создавать различные комбинации всех nameS и все nums на входе, мы хотим сгруппировать их по item что они берутся.

для этого мы построим какое-то дерево префиксов для нашего структура вывода, которая будет содержать генераторы объектов, каждый из которых рекурсивно является деревом для соответствующей выходной подструктуры.

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

теперь, наш набор правил group(parse(script)) выглядит так:

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

и это структура, с которой мы действительно можем работать, поскольку теперь она ясно передает намерение о том, как сопоставить все эти вложенные массивы и объекты в них. Давайте динамически интерпретировать это, создавая вывод для данного ввод:

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

и это действительно работает с

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

но мы можем сделать лучше, и гораздо более производительным:

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+";");
}

так вот, что вы получите в конце:

> 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(y.a);
    }
    var x_bs = []; 
    for (var i = 0; i < y.length; i++) {
        var y = y[i];
        x_bs.push(y.b);
    }
    return {"as": x_as,"bs": x_bs};
}

и теперь вы можете легко передать свои входные данные в эту динамически созданную функцию, и она будет преобразована довольно быстро : -)