Как получить основную структуру для файла JSON? - PullRequest
0 голосов
/ 19 сентября 2019

У меня есть файл JSON следующим образом:

[
    {
        "dog": "lmn",
        "tiger": [
            {
                "bengoltiger": {
                    "height": {
                        "x": 4
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "width": {
                        "a": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ]
    },
    {
        "dog": "pqr",
        "tiger": [
            {
                "bengoltiger": {
                    "width": {
                        "m": 3
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "height": {
                        "n": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ],
        "lion": 90
    }
]

Я хочу преобразовать это, чтобы получить все возможные свойства любого объекта на любом уровне вложенности.Для массивов первый объект должен содержать все свойства.Значения тривиальны, но приведенное ниже решение учитывает первое обнаруженное значение для любого свойства.(Например, «lmn» сохраняется для свойства «dog») Ожидаемый результат:

[
    {
        "dog": "lmn",
        "tiger": [
            {
                "bengoltiger": {
                    "height": {
                        "x": 4,
                        "n": 8
                    },
                    "width": {
                        "a": 8,
                        "m": 3
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b",
                    "b": 3
                }
            }
        ],
        "lion": 90
    }
]

Вот рекурсивная функция, которую я пробовал до того, как эта проблема с вложенностью поразила меня

function consolidateArray(json) {
    if (Array.isArray(json)) {
      const reference = json[0];
      json.forEach(function(element) {
        for (var key in element) {
          if (!reference.hasOwnProperty(key)) {
            reference[key] = element[key];
          }
        }
      });
      json.splice(1);
      this.consolidateArray(json[0]);
    } else if (typeof json === 'object') {
      for (var key in json) {
        if (json.hasOwnProperty(key)) {
          this.consolidateArray(json[key]);
        }
      }
    }
  };
  
var json = [
    {
        "dog": "lmn",
        "tiger": [
            {
                "bengoltiger": {
                    "height": {
                        "x": 4
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "width": {
                        "a": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ]
    },
    {
        "dog": "pqr",
        "tiger": [
            {
                "bengoltiger": {
                    "width": {
                        "m": 3
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "height": {
                        "n": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ],
        "lion": 90
    }
];
consolidateArray(json);
alert(JSON.stringify(json, null, 2));

Ответы [ 2 ]

1 голос
/ 19 сентября 2019

Общая логика с использованием этого нового JNode IIFE с комментариями - спросите кого-нибудь более умного, если вы не понимаете что-то, как я; -)

И уровень начинается с 1, так как нет корневого объекта @start.

var json;
function DamnDemo() {
    json = DemoJSON();
    var it = new JNode(json), it2 = it;
    var levelKeys = []; /* A bit crazy structure:
    [
      levelN:{
               keyA:[JNode, JNode,...],
               keyB:[JNode, JNode,...],
               ...
             },
      levelM:...
    ]
    */
    do {
        var el = levelKeys[it.level]; // array of level say LevelN or undefined
        el = levelKeys[it.level] = el || {}; // set 2 empty it if does not exist
        el = el[it.key] = el[it.key] || []; // key array in say levelN
        el.push(it); // save current node indexing by level, key -> array
    } while (it = it.DepthFirst()) // traverse all nodes
    for(var l1 in levelKeys) { // let start simply by iterating levels
        l2(levelKeys[l1]);
    }
    console.log(JSON.stringify(json, null, 2));
}

function l2(arr) { // fun starts here...
    var len = 0, items = []; // size of arr, his items to simple Array
    for(var ln in arr) { // It's a kind of magic here ;-) Hate recursion, but who want to rewrite it ;-)
        if (arr[ln] instanceof JNode) return 1; // End of chain - our JNode for traverse of length 1
        len += l2(arr[ln]);
        items.push(arr[ln]);
    }
    if (len == 2) { // we care only about 2 items to move (getting even 3-5)
        //console.log(JSON.stringify(json));
        if (!isNaN(items[0][0].key) || (items[0][0].key == items[1][0].key)) { // key is number -> ignore || string -> must be same
            console.log("Keys 2B moved:", items[0][0].key, items[1][0].key, "/ level:", items[0][0].level);
            var src = items[1][0]; // 2nd similar JNode
            moveMissing(items[0][0].obj, src.obj); // move to 1st
            //console.log(JSON.stringify(json));
            if (src.level == 1) { // top level cleaning
                delete src.obj;
                delete json[src.key]; // remove array element
                if (!json[json.length-1]) json.length--; // fix length - hope it was last one (there are other options, but do not want to overcomplicate logic)
            } else {
                var parent = src.parent;
                var end = 0;
                for(var i in parent.obj) {
                    end++;
                    if (parent.obj[i] == src.obj) { // we found removed in parent's array
                        delete src.obj; // delete this empty object
                        delete parent.obj[i]; // and link on
                        end = 1; // stupid marker
                    }
                }
                if (end == 1 && parent.obj instanceof Array) parent.obj.length--; // fix length - now only when we are on last element
            }
        } else console.log("Keys left:", items[0][0].key, items[1][0].key, "/ level:", items[0][0].level); // keys did not match - do not screw it up, but report it
    }
    return len;
}

function moveMissing(dest, src) {
    for(var i in src) {
        if (src[i] instanceof Object) {
            if (!dest[i]) { // uff object, but not in dest
                dest[i] = src[i];
            } else { // copy object over object - let it bubble down...
                moveMissing(dest[i], src[i]);
            }
            delete src[i];
        } else { // we have value here, check if it does not exist, move and delete source
            if (!dest[i]) {
                dest[i] = src[i];
                delete src[i];
            }
        }
    }
}

// JSON_Node_Iterator_IIFE.js
'use strict';
var JNode = (function (jsNode) {

    function JNode(json, parent, pred, key, obj, fill) {
        var node, pred = null;
        if (parent === undefined) {
            parent = null;
        } else if (fill) {
            this.parent = parent;
            this.pred = pred;
            this.node = null;
            this.next = null;
            this.key = key;
            this.obj = obj;
            return this;
        }
        var current;
        var parse = (json instanceof Array);
        for (var child in json) {
            if (parse) child = parseInt(child);
            var sub = json[child];
            node = new JNode(null, parent, pred, child, sub, true);
            if (pred) {
                pred.next = node;
                node.pred = pred;
            }
            if (!current) current = node;
            pred = node;
        }
        return current;
    }

    JNode.prototype = {
        get hasNode() {
            if (this.node) return this.node;
            return (this.obj instanceof Object);
        },
        get hasOwnKey() { return this.key && (typeof this.key != "number"); },
        get level() {
            var level = 1, i = this;
            while(i = i.parent) level++;
            return level;
        },
        Down: function() {
            if (!this.node && this.obj instanceof Object) {
                this.node = new JNode(this.obj, this);
            }
            return this.node;
        },
        Stringify: function() { // Raw test stringify - #s are taken same as strings
            var res;
            if (typeof this.key == "number") {
                res = '[';
                var i = this;
                do {
                    if (i.node) res += i.node.Stringify();
                    else res += "undefined";
                    i = i.next;
                    if (i) res += ','
                } while(i);
                res += ']';
            } else {
                res = '{' + '"' + this.key + '":';
                res += (this.node?this.node.Stringify():this.hasNode?"undefined":'"'+this.obj+'"');
                var i = this;
                while (i = i.next) {
                    res += ',' + '"' + i.key + '":';
                    if (i.node) res += i.node.Stringify();
                    else {
                        if (i.obj instanceof Object) res += "undefined";
                        else res += '"' + i.obj + '"';
                    }
                };
                res += '}';
            }
            return res;
        },
        DepthFirst: function () {
            if (this == null) return 0; // exit sign
            if (this.node != null || this.obj instanceof Object) {
                return this.Down(); // moved down
            } else if (this.next != null) {
                return this.next;// moved right
            } else {
                var i = this;
                while (i != null) {
                    if (i.next != null) {
                        return i.next; // returned up & moved next
                    }
                    i = i.parent;
                }
            }
            return 0; // exit sign
        }
    }

    return JNode;
})();

// Fire test
DamnDemo();
function DemoJSON() {
    return [
    {
        "dog": "lmn",
        "tiger": [
            {
                "bengoltiger": {
                    "height": {
                        "x": 4
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "width": {
                        "a": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ]
    },
    {
        "dog": "pqr",
        "tiger": [
            {
                "bengoltiger": {
                    "width": {
                        "m": 3
                    }
                },
                "indiantiger": {
                    "paw": "a",
                    "foor": "b"
                }
            },
            {
                "bengoltiger": {
                    "height": {
                        "n": 8
                    }
                },
                "indiantiger": {
                    "b": 3
                }
            }
        ],
        "lion": 90
    }
]
;}
0 голосов
/ 22 сентября 2019

Это была интересная проблема.Вот что я придумал:

// Utility functions

const isInt = Number.isInteger

const path = (ps = [], obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const assoc = (prop, val, obj) => 
  isInt (prop) && Array .isArray (obj)
    ? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
    : {...obj, [prop]: val}

const assocPath = ([p = undefined, ...ps], val, obj) => 
  p == undefined
    ? obj
    : ps.length == 0
      ? assoc(p, val, obj)
      : assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)


// Helper functions

function * getPaths(o, p = []) {
  if (Object(o) !== o || Object .keys (o) .length == 0) yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, isInt (Number (k)) ? Number (k) : k])
}

const canonicalPath = (path) =>
  path.map (n => isInt (Number (n)) ? 0 : n)

const splitPaths = (xs) => 
  Object .values ( xs.reduce ( 
    (a, p, _, __, cp = canonicalPath (p), key = cp .join ('\u0000')) => 
      ({...a, [key]: a [key] || {canonical: cp, path: p} })
    , {}
  ))


// Main function

const canonicalRep = (data) => splitPaths ([...getPaths (data)]) 
  .reduce (
    (a, {path:p, canonical}) => assocPath(canonical, path(p, data), a),
    Array.isArray(data) ? [] : {}
  ) 


  // Test

const data = [{"dog": "lmn", "tiger": [{"bengoltiger": {"height": {"x": 4}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"width": {"a": 8}}, "indiantiger": {"b": 3}}]}, {"dog": "pqr", "lion": 90, "tiger": [{"bengoltiger": {"width": {"m": 3}}, "indiantiger": {"foor": "b", "paw": "a"}}, {"bengoltiger": {"height": {"n": 8}}, "indiantiger": {"b": 3}}]}]

console .log (
  canonicalRep (data)
)

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

  • isInt - это просто псевдоним функции первого класса для Number.isInteger

  • path находит вложенное свойство объекта по заданному пути

    path(['b', 1, 'c'], {a: 10, b: [{c: 20, d: 30}, {c: 40}], e: 50}) //=> 40 
    
  • assoc возвращает новый объект, клонирующий ваш оригинал, но со значением определенного свойства, установленным или замененнымс поставляемым.

    assoc('c', 42, {a: 1, b: 2, c: 3, d: 4}) //=> {a: 1, b: 2, c: 42, d: 4}
    

    Обратите внимание, что внутренние объекты доступны по ссылке, где это возможно.

  • assocPath делает то же самое, но с более глубоким путем, создавая узлы по мере необходимости.

    assocPath(['a', 'b', 1, 'c', 'd'], 42, {a: {b: [{x: 1}, {x: 2}], e: 3})
        //=> {a: {b: [{x: 1}, {c: {d: 42}, x: 2}], e: 3}} 
    

За исключением isInt, они заимствуют свои API от Ramda .(Отказ от ответственности: я - автор Рамды.) Но это уникальные реализации.

Следующая функция, getPaths, является адаптацией одной из другого ответа SO .В нем перечислены все пути в вашем объекте в формате, используемом path и assocPath, возвращая массив значений, которые являются целыми числами, если соответствующий вложенный объект является массивом, и строки в противном случае.В отличие от функции, из которой была заимствована, она возвращает только пути к конечным значениям.

Для вашего исходного объекта она возвращает итератор для этих данных:

[
  [0, "dog"], 
  [0, "tiger", 0, "bengoltiger", "height", "x"], 
  [0, "tiger", 0, "indiantiger", "foor"], 
  [0, "tiger", 0, "indiantiger", "paw"], 
  [0, "tiger", 1, "bengoltiger", "width", "a"], 
  [0, "tiger", 1, "indiantiger", "b"], 
  [1, "dog"], 
  [1, "lion"], 
  [1, "tiger", 0, "bengoltiger", "width", "m"], 
  [1, "tiger", 0, "indiantiger", "foor"], 
  [1, "tiger", 0, "indiantiger", "paw"], 
  [1, "tiger", 1, "bengoltiger", "height", "n"], 
  [1, "tiger", 1, "indiantiger", "b"]
]

Если бы я хотел потратитьбольше времени на этом, я бы заменил эту версию getPaths на версию без генератора, просто чтобы сохранить этот код согласованным.Это не должно быть сложно, но я не заинтересован в том, чтобы тратить на это больше времени.

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

[{
  canonical: [0, "dog"],
  path:      [0, "dog"]
}, {
  canonical: [0, "tiger", 0, "bengoltiger", "height", "x"],
  path:      [0, "tiger", 0, "bengoltiger", "height", "x"]
}, {
  canonical: [0, "tiger", 0, "indiantiger", "foor"], 
  path:      [0, "tiger", 0, "indiantiger", "foor"]
}, {
  canonical: [0, "tiger", 0, "indiantiger", "paw"],
  path:      [0, "tiger", 0, "indiantiger", "paw"]
}, {
  canonical: [0, "tiger", 0, "bengoltiger", "width", "a"], 
  path:      [0, "tiger", 1, "bengoltiger", "width", "a"]
}, {
  canonical: [0, "tiger", 0, "indiantiger", "b"], 
  path:      [0, "tiger", 1, "indiantiger", "b"]
}, {
  canonical: [0, "lion"], 
  path:      [1, "lion"]
}, {
  canonical: [0, "tiger", 0, "bengoltiger", "width", "m"], 
  path:      [1, "tiger", 0, "bengoltiger", "width", "m"]
}, {
  canonical: [0, "tiger", 0, "bengoltiger", "height", "n"], 
  path:      [1, "tiger", 1, "bengoltiger", "height", "n"]
}]

Обратите внимание, что эта функция также удаляет дубликатыканонические тропы.Изначально у нас были и [0, "tiger", 0, "indiantiger", "foor"], и [1, "tiger", 0, "indiantiger", "foor"], но вывод содержит только первый.

Он делает это, сохраняя их в объекте под ключом, созданным путем соединения пути вместе с непечатаемымсимвол \u0000.Это был самый простой способ выполнить эту задачу, но возможен крайне маловероятный режим отказа 1 , поэтому, если бы мы действительно хотели, мы могли бы сделать более сложную проверку дубликатов.Я бы не стал беспокоиться.

Наконец, основная функция, canonicalRep, создает представление из вашего объекта, вызывая splitPaths и сворачивая результат, используя canonical, чтобы указать, куда поместить новыйданные и применение функции path к вашему свойству path и исходному объекту.

Наш конечный результат, как и было запрошено, выглядит следующим образом:

[
    {
        dog: "lmn",
        lion: 90,
        tiger: [
            {
                bengoltiger: {
                    height: {
                        n: 8,
                        x: 4
                    },
                    width: {
                        a: 8,
                        m: 3
                    }
                },
                indiantiger: {
                    b: 3,
                    foor: "b",
                    paw: "a"
                }
            }
        ]
    }
]

Что меня поразилов том, что я видел это как интересную задачу программирования, хотя я не мог себе представить, как это можно использовать на практике.Но теперь, когда я его кодировал, я понимаю, что это решит проблему в моем текущем проекте, которую я отложил несколько недель назад.Я, вероятно, реализую это в понедельник!


1 Этот режим сбоя может произойти, если у вас есть определенные узлы, содержащие этот разделитель, \u0000.Например, если у вас есть пути [...nodes, "abc\u0000", "def", ...nodes] и [...nodes, "abc", "\u0000def", ...nodes], они оба будут отображаться в "...abc\u0000\u0000def...".Если это реальная проблема, мы, безусловно, могли бы использовать другие формы дедупликации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...