Это была интересная проблема.Вот что я придумал:
// 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..."
.Если это реальная проблема, мы, безусловно, могли бы использовать другие формы дедупликации.