Typescript - поиск подходящих ключей и путей в многомерном объекте - PullRequest
1 голос
/ 10 октября 2019

Короче говоря;Как бы я мог получить все значения общего ключа в нескольких объектах?

Дольше: мне дали следующие объекты, хранящиеся в массиве с именем collection:

[
    names: {
        "one": "square";
        "two": {
            "three": "circle"
            "four": {
                "five": "triangle"
                }
            }
        }
    shapes: {
        "one": "[]";
        "two": {
            "three": "()"
            "four": {
                "five": "/\"
                }
            }
        }
]

Я создал систему меню в Angular (v8.xx), которая считывает ключи от объектов names. Когда я нажимаю на пункт меню для «круга», я надеюсь получить пару ключ / значение и их пути для использования в окне редактирования. Это должно произойти для каждого элемента. Пример:

onClick(menuItem){
    const paths = collection.search(menuItem.key)
    console.log(paths) \\ expecting: ["names.two.three", "shapes.two.three"]
    openEditor(paths)
}

openEditor(paths){
    for(path in paths){
        display.Name(path.key)
        inputfield.value(path.value)
    }
|----------
|three: 
|circle
|----------
|three:
|()
|----------

Я пытался создать рекурсивную функцию сам, но до сих пор не достиг никакого возможного результата. Я также попробовал удивительные примеры Скотта, хотя angular / typcript, к сожалению, выдает ошибку при определении ps в assocPath():

Argument of type 'any[]' is not assignable to parameter of type '[any, ...any[]]'.
      Property '0' is missing in type 'any[]' but required in type '[any, ...any[]]'.

Кроме того, я также посмотрел на эти ответы:

1: StackOverflow: Javascript / JSON получить путь к данному подузлу?

2: StackOverflow: получить «путь» объекта JSON в JavaScript

У 1-й ошибки есть ошибка, связанная с path переменной блочной области, используемой до ее объявления, и в настоящее время я устраняю неисправность 2-й в моем сценарии.

Ответы [ 2 ]

1 голос
/ 10 октября 2019

Обновление

Я явно вырезал и вставил какой-то код из другого места, когда изначально писал это, так как включал две функции, которые вообще не использовались. Теперь они удалены.

Я также добавил ниже объяснение того, как я могу, шаг за шагом, преобразовать одну из этих функций из стиля ES5 в современную версию JS в ответе. Я считаю, что ES6 + является серьезным улучшением, и мне нравится функция, так как я написал ее гораздо лучше, но для тех, кто изучает современный JS, такое объяснение может быть полезным.

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


Мне действительно неясно, что вы ищете. Мое лучшее предположение состоит в том, что вы хотите принять массив объектов, подобных приведенному выше, и вернуть функцию, которая принимает значение, подобное "circle" или "()", и возвращает путь к этому значению на одном из этих объектов, а именно ['two', 'three']. Но это предположение может быть далеко.

Вот версия, которая делает это, основываясь на нескольких повторно используемых функциях:

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

const findLeafPaths = (o, path = [[]]) => 
  typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path).map(p => [k, ...p])
      ) 
    : path


// Main function
const makeSearcher = (xs) => {
  const structure = xs .reduce (
    (a, x) => findLeafPaths (x) .reduce ((a, p) => ({...a, [path (p, x)]: p}), a),
    {}
  )
  return (val) => structure[val] || [] // something else? or throw error?
}


// Demonstration
const objs = [
  {one: "square", two: {three: "circle", four: {five: "triangle"}}}, 
  {one: "[]", two: {three: "()", four: {five: "/\\"}}}
]

const searcher = makeSearcher(objs)

console .log (searcher ('()'))        //~> ['two', 'three']
console .log (searcher ('circle'))    //~> ['two', 'three']
console .log (searcher ('triangle'))  //~> ['two', four', 'five']
console .log (searcher ('[]'))        //~> ['one']
console .log (searcher ('heptagon'))  //~> []
        

Мы начнем с двух вспомогательных функций, path и findLeafPaths. Это все многоразовые функции. Первый заимствует свои API из Ramda , хотя это отдельная реализация:

  • path принимает список узлов (например, ['two', 'three']) и объекти возвращает значение по этому пути, если все узлы на этом пути существуют

  • findLeafPaths берет объект и, рассматривая его как дерево, возвращает пути всех конечных узлов. Таким образом, для вашего первого объекта он вернет [['one'], ['two', 'three'], ['two', 'four', 'five']]. Опять же, мы игнорируем массивы, и я даже не уверен, что нам нужно для их поддержки.

Основная функция - makeSearcher. Он берет массив объектов вроде этого:

[
  {one: "square", two: {three: "circle", four: {five: "triangle"}}}, 
  {one: "[]", two: {three: "()", four: {five: "/\\"}}}
]

и преобразует их в структуру, которая выглядит следующим образом:

{
  'square'   : ['one']
  'circle'   : ['two', 'three']
  'triangle' : ['two', 'four', 'five']
  '[]'       : ['one']
  '()'       : ['two', 'three']
  '/\\'      : ['two', 'four', 'five']
}

, а затем возвращает функцию, которая просто ищет значенияиз этой структуры.

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

Преобразование ES5 в современный JS

Здесь мы показываем серию преобразований из ES5 в современный код Javascript. Обратите внимание, что я на самом деле написал их в другом порядке, поскольку ES6 + - это то, что естественно для меня после работы в нем в течение нескольких лет. Тем не менее, это может быть полезно для пользователей ES5.

Мы собираемся конвертировать версию findLeafPaths. Вот версия, которая, я думаю, пропускает все функции ES6 +:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const keys = Object .keys (o)
    const entries = keys .map (key => [key, o [key]])
    const partialPaths = entries .map ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k].concat(p)
      })
    })
    return partialPaths.reduce(function(a, b) {
      return a.concat(b)
    }, [])
  }
  return path || [[]]
}

Первое, что мы делаем, это используем Object.entries, чтобы заменить танец получением ключей объекта и затем отобразить ихчтобы получить пары [key, value]:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    const partialPaths = entries .map (function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k] .concat (p)
      })
    })
    return partialPaths.reduce(function (a, b) {
      return a .concat (b)
    }, [])
  }
  return path || [[]]
}

Далее, шаблон отображения, затем сглаживание путем сокращения с конкатенацией имеет встроенный метод Array, flatMap. Мы можем упростить это, используя:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap (function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k] .concat (p)
      })
    })
  }
  return path || [[]]
}

Теперь мы можем настроить это, чтобы использовать современный синтаксис spread вместо concat:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k, ...p]
      })
    })
  }
  return path || [[]]
}

Функции стрелок еще больше упростят ситуацию. Здесь мы заменяем самый внутренний вызов функции стрелкой:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (p => [k, ...p])
    })
  }
  return path || [[]]
}

Мы повторяем это выражение path || [[]] в двух местах. Мы могли бы использовать параметр по умолчанию , чтобы иметь только один:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      return findLeafPaths (v, path) .map (p => [k, ...p])
    })
  }
  return path
}

Теперь мы заменим следующее выражение функции (предоставляется entries.flatmap()) стрелкой:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap (
      ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
    )
  }
  return path
}

entries - это временная переменная, которую мы используем только один раз в строке после ее определения. Мы можем легко удалить это:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    return Object .entries (o) .flatMap (
      ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
    )
  }
  return path
}

С функциональной точки зрения работа с выражениями предпочтительнее, чем с утверждениями. Они более восприимчивы к анализу и не зависят от внешнего порядка. Следовательно, я выберу условное выражение («троичное выражение») для if-else. Поэтому я предпочитаю эту версию:

const findLeafPaths = function (o, path = [[]]) {
  return typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
      ) 
    : path
}

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

const findLeafPaths = (o, path = [[]]) => 
  typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
      ) 
    : path

Очевидно, мы могли бы сделатьто же самое происходит и с path и makeSearcher.

Обратите внимание , что каждый шаг этого уменьшает количество строк или символов функции. Это хорошо, но это не самый важный момент. Более уместно, что каждая версия, возможно, проще, чем предыдущая. Это не означает, что это более знакомо, только то, что меньше идей переплетаются вместе. (Рич Хикки Simple Made Easy говорит отличную идею объяснить разницу между этими часто путаемыми понятиями.)

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

0 голосов
/ 15 октября 2019

Я решил текущую точку отключения;

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

и функции теперь работают как задумано. Результат из searcher() может дополнительно вернуть один путь, если вы дадите только значение, которое хотите найти. например, searcher("circle") возвращает: ["one", "two", "three"].

...