Как отфильтровать несколько свойств родительского дочернего массива, который может иметь глубину в несколько уровней дерева - PullRequest
7 голосов
/ 04 апреля 2020

TL; DR; Чтобы упростить задачу, как я могу отфильтровать несколько свойств родительского дочернего массива, который может иметь глубину в несколько уровней дерева. Это для библиотеки данных с открытым исходным кодом, используемой несколькими сотнями пользователей.

Итак, у меня есть массив ссылок на родительские / дочерние элементы, у дочерних элементов также могут быть сами дочерние элементы и т. Д. Нет ограничений на глубину уровня дерева. , Кроме того, мне нужно иметь возможность фильтровать не только свойство, имеющее древовидную структуру, но и любые свойства ie, которые являются столбцами в сетке этого массива.

Например, у меня есть этот массив, который представляет список файловых проводников

const myFiles = [
    {id: 11, file: "Music", parentId: null },
    {id: 12, file: "mp3", parentId: 11 },
    {id: 14, file: "pop", parentId: 12 },
    {id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85, parentId: 14, },
    {id: 16, file: "rock", parentId: 12 },
    {id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16, },
    {id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null, },
    {id: 21, file: "Documents", parentId: null, },
    {id: 2, file: "txt", parentId: 21 },
    {id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2, },
    {id: 4, file: "pdf", parentId: 21 },
    {id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 },
    {id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4, },
    {id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4, },
    {id: 7, file: "xls", parentId: 21 },
    {id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7, },
    {id: 9, file: "misc", parentId: 21 },
    {id: 10, file: "something.txt", dateModified: "2015-02-26", size: 0.4, parentId: 9, },
]

Массив выглядит плоским, но на самом деле это древовидная структура, которая представлена ​​в сетке данных как показано ниже.

Я обнаружил, что частично работает, это l oop по всему массиву и добавление полного списка файлов, который может иметь каждый элемент, включая самого себя, например, если в Documents есть дочерний PDF, который имеет собственный дочерний файл Map.pdf, тогда отображение дерева может быть представлено как ["Documents", "PDF", "map.pdf"], и мы сохраняем это в родительском объекте, а затем в следующем дочернем хранилище [" PDF "," map.pdf "] и, наконец, о последнем дочернем элементе, который мы храним [" map.pdf "] следующим образом

    {id: 21, file: "Documents", parentId: null, treeMap: ["Documents", "PDF", "map.pdf"] }
    {id: 4, file: "pdf", parentId: 21, treeMap: ["PDF", "map.pdf"] }
    {id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4, treeMap: ["map.pdf"] }

, и этот метод позволяет мне сделать это

export function modifyDatasetToAddTreeMapping(items: any[], treeViewColumn: Column, dataView: any) {
  for (let i = 0; i < items.length; i++) {
    items[i]['treeMap'] = [items[i][treeViewColumn.id]];
    let item = items[i];

    if (item['parentId'] !== null) {
      let parent = dataView.getItemById(item['parentId']);

      while (parent) {
        parent['treeMap'] = dedupePrimitiveArray(parent['treeMap'].concat(item['treeMap']));
        item = parent;
        parent = dataView.getItemById(item['parentId']);
      }
    }
  }
}

export function dedupePrimitiveArray(inputArray: Array<number | string>): Array<number | string> {
  const seen = {};
  const out = [];
  const len = inputArray.length;
  let j = 0;
  for (let i = 0; i < len; i++) {
    const item = inputArray[i];
    if (seen[item] !== 1) {
      seen[item] = 1;
      out[j++] = item;
    }
  }
  return out;
}

Затем библиотека данных использует метод Filter, который я могу использовать следующим образом: columnFilters - это объект, содержащий 1 или более фильтров, например, const columnFilters = { file: 'map', size: '>3' }

. Сеть данных - это библиотека (SlickGrid. ) и он использует метод фильтра как * 1 020 *

function treeFilter(dataView: any, item: any) {
    const columnFilters = { file: this.searchString.toLowerCase(), size: 2 };
    let filterCount = 0;

    if (item[parentPropName] !== null) {
      let parent = dataView.getItemById(item['parentId']);
      while (parent) {
        if (parent.__collapsed) {
          return false;
        }
        parent = dataView.getItemById(parent['parentId']);
      }
    }

    for (const columnId in columnFilters) {
      if (columnId !== undefined && columnFilters[columnId] !== '') {
        filterCount++;

        if (item.treeMap === undefined || !item.treeMap.find((itm: string) => itm.endsWith(columnFilters[columnId]))) {
          return false;
        }
      }
    }
    return true;
  }

При вызове modifyDatasetToAddTreeMapping() все работает нормально, если я хочу фильтровать по столбцу File, но если я добавляю больше фильтров столбца, он не работает должным образом. Например, если вы посмотрите на 2-й экран печати, вы увидите, что я ввел «карту», ​​и это отобразит «Документы> PDF> map.pdf», и это здорово, но если добавить файл размером менее 3 МБ, он не должен не отображать «map.pdf», и поскольку этот файл не отображается, а «Documents> PDF» не содержит слова «map», то ничего не должно отображаться, так как вы можете видеть, что фильтр работает не так, как должен.

Таким образом, с текущей реализацией у меня есть 2 проблемы: 1. Неправильное поведение, когда узел дерева не отображается, его родительский элемент не должен отображаться. 2. Необходимость вызова modifyDatasetToAddTreeMapping() - дополнительный вызов. это может не понадобиться 3. это также изменяет исходный массив, я мог бы глубоко клонировать массив, но это было бы еще одним снижением производительности

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

Наконец, намеревается использовать это со SlickGrid, который может иметь 10 или даже 50 тысяч строк, поэтому он должен быть быстрым. Вы можете увидеть это SlickGrid demo , но их реализация фильтрации не верна, также я нашел способ добавить отображение в этом другом SO Ответ

ПРИМЕЧАНИЕ: I также хотел бы отметить, что решение этой проблемы принесет пользу, возможно, нескольким сотням (или тысячам) пользователей, поскольку оно будет реализовано в Angular -Slickgrid и Aurelia-Slickgrid которые оба являются открытым исходным кодом lib и используются как минимум 300+ пользователями.

enter image description here

Фильтрация со словом "карта" не должна ничего возвращать здесь, так как ни у одного из узлов / потомков нет этого текста. enter image description here

РЕДАКТИРОВАТЬ

Лучший код - это вставить любой код, выполняющий работу, в обычный JS filter, что означает конечное решение будет метод myFilter, который будет filter метод обратного вызова. Причина, по которой я застрял в этом, заключается в том, что я использую внешнюю библиотеку lib SlickGrid , и мне нужно использовать то, что эта библиотека доступна в качестве доступных методов publi c.

function myFilter(item, args) {
  const columnFilters = args.columnFilters;

  // iterate through each items of the dataset
  // return true/false on each item
}

// to be used as a drop in
dataView.setFilterArgs({ columnFilters: this._columnFilters });
dataView.setFilter(myFilter.bind(this));

Если у меня const columnFilters = { file: "map", size: "<3.2" };, ожидаемый результат массива будет 4 строки

// result
[
  {id: 21, file: "Documents", parentId: null },
  {id: 4, file: "pdf", parentId: 21, },
  {id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 },
  {id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4, }
]

Если у меня есть const columnFilters = { file: "map", size: "<3" };, ожидаемый результат массива будет 3 строки

// result
[
  {id: 21, file: "Documents", parentId: null },
  {id: 4, file: "pdf", parentId: 21, },
  {id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 },
]

и, наконец, если у меня будет const columnFilters = { file: "map", size: ">3" };, то ожидаемый результат будет пустым массивом, потому что ни один из файл имеет этот символ и размер файла.

РЕДАКТИРОВАТЬ 2

Исходя из ответа @ AlexL, он начинает работать. Всего лишь пара настроек, но это выглядит очень многообещающе уже enter image description here

РЕДАКТИРОВАТЬ 3

Благодаря великолепной работе Алекса, его ответ помог мне слить это в мой Open Исходная библиотека Теперь у меня есть 2 живых демо с Parent / Child ref (плоский набор данных) и с Hierarchical Dataset (набор данных дерева). Я мог бы sh Я мог бы проголосовать более одного раза:)

1 Ответ

5 голосов
/ 08 апреля 2020

У меня есть способ сделать это. Это должно быть довольно производительным, но мы также можем поменять карту и уменьшить et c. для старых добрых циклов for для дальнейшей оптимизации скорости (я видел различные блоги и статьи, сравнивающие скорость forEach, map et c. с for-l oop и циклы for, похоже, побеждают)

Вот демоверсия (также здесь: https://codepen.io/Alexander9111/pen/abvojzN):

const myFiles = [
  { id: 11, file: "Music", parentId: null },
  { id: 12, file: "mp3", parentId: 11 },
  { id: 14, file: "pop", parentId: 12 },
  { id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85,  parentId: 14 },
  { id: 16, file: "rock", parentId: 12 },
  { id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16 },
  { id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null },
  { id: 21, file: "Documents", parentId: null },
  { id: 2, file: "txt", parentId: 21 },
  { id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2 },
  { id: 4, file: "pdf", parentId: 21 },
  { id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 },
  { id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4 },
  { id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4 },
  { id: 7, file: "xls", parentId: 21 },
  { id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7 },
  { id: 9, file: "misc", parentId: 21 },
  { id: 10,  file: "something.txt", dateModified: "2015-02-26", size: 0.4,  parentId: 9 }
];

//example how to use the "<3" string - better way than using eval():
const columnFilters = { file: "map", size: "<3.2" }; //, size: "<3" 
const isSizeValid = Function("return " + myFiles[11].size + "<3")();
//console.log(isSizeValid);

const myObj = myFiles.reduce((aggObj, child) => {
  aggObj[child.id] = child;
  //the filtered data is used again as each subsequent letter is typed
  //we need to delete the ._used property, otherwise the logic below
  //in the while loop (which checks for parents) doesn't work:
  delete aggObj[child.id]._used;
  return aggObj;
}, {});

function filterMyFiles(myArray, columnFilters){
  const filteredChildren = myArray.filter(a => {
    for (let key in columnFilters){
      //console.log(key)      
      if (a.hasOwnProperty(key)){
        const strContains =  String(a[key]).includes(columnFilters[key]);
        const re = /(?:(?:^|[-+<>=_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-<>=]?\d+)?\s*))+$/;
        const comparison = re.test(columnFilters[key]) && Function("return " + a[key] + columnFilters[key])();
        if (strContains || comparison){
          //don't return true as need to check other keys in columnFilters
        }else{
          //console.log('false', a)
          return false;
        }
      } else{
        return false;
      }           
    }
    //console.log('true', a)
    return true;
  })
  return filteredChildren;
}

const initFiltered = filterMyFiles(myFiles, columnFilters);

const finalWithParents = initFiltered.map(child => {
  const childWithParents = [child];
  let parent = myObj[child.parentId];
  while (parent){
    //console.log('parent', parent)
    parent._used || childWithParents.unshift(parent)
    myObj[parent.id]._used = true;
    parent = myObj[parent.parentId] || false;    
  }
  return childWithParents;
}).flat();

console.log(finalWithParents)
.as-console-wrapper { max-height: 100% !important; top: 0; }

В основном настроить объект для последующего использования для поиска всех родителей.

Затем выполнить один фильтр (т.е. одну итерацию массива) и Отфильтруйте те, которые соответствуют условиям в объекте columnFilters.

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

свести массив с помощью .flat () (предполагается, что одна последняя итерация), и тогда мы закончили.

Любые вопросы, сообщите мне.

Обновление - Для -l oop подход плюс попытка сократить итерации по массиву

Вырезать пару итераций :) (https://codepen.io/Alexander9111/pen/MWagdVz):

const myFiles = [
  { id: 11, file: "Music", parentId: null },
  { id: 12, file: "mp3", parentId: 11 },
  { id: 14, file: "pop", parentId: 12 },
  { id: 15, file: "theme.mp3", dateModified: "2015-03-01", size: 85,  parentId: 14 },
  { id: 16, file: "rock", parentId: 12 },
  { id: 17, file: "soft.mp3", dateModified: "2015-05-13", size: 98, parentId: 16 },
  { id: 18, file: "else.txt", dateModified: "2015-03-03", size: 90, parentId: null },
  { id: 21, file: "Documents", parentId: null },
  { id: 2, file: "txt", parentId: 21 },
  { id: 3, file: "todo.txt", dateModified: "2015-05-12", size: 0.7, parentId: 2 },
  { id: 4, file: "pdf", parentId: 21 },
  { id: 22, file: "map2.pdf", dateModified: "2015-05-21", size: 2.9, parentId: 4 },
  { id: 5, file: "map.pdf", dateModified: "2015-05-21", size: 3.1, parentId: 4 },
  { id: 6, file: "internet-bill.pdf", dateModified: "2015-05-12", size: 1.4, parentId: 4 },
  { id: 7, file: "xls", parentId: 21 },
  { id: 8, file: "compilation.xls", dateModified: "2014-10-02", size: 2.3, parentId: 7 },
  { id: 9, file: "misc", parentId: 21 },
  { id: 10,  file: "something.txt", dateModified: "2015-02-26", size: 0.4,  parentId: 9 }
];

const columnFilters = { file: "map", size: "<3.2" };
console.log(customLocalFilter(myFiles, columnFilters));

function customLocalFilter(array, filters){  
  const myObj = {};
  for (let i = 0; i < myFiles.length; i++) {
    myObj[myFiles[i].id] = myFiles[i];
    //the filtered data is used again as each subsequent letter is typed
    //we need to delete the ._used property, otherwise the logic below
    //in the while loop (which checks for parents) doesn't work:
    delete myObj[myFiles[i].id]._used;
  }

  const filteredChildrenAndParents = [];
  for (let i = 0; i < myFiles.length; i++) {
    const a = myFiles[i];
    let matchFilter = true;
    for (let key in columnFilters) {
      if (a.hasOwnProperty(key)) {
        const strContains = String(a[key]).includes(columnFilters[key]);
        const re = /(?:(?:^|[-+<>!=_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-<>!=]?\d+)?\s*))+$/;
        const comparison =
          re.test(columnFilters[key]) &&
          Function("return " + a[key] + columnFilters[key])();
        if (strContains || comparison) {
          //don't return true as need to check other keys in columnFilters
        } else {
          matchFilter = false;
          continue;
        }
      } else {
        matchFilter = false;
        continue;
      }
    }
    if (matchFilter) {
      const len = filteredChildrenAndParents.length;
      filteredChildrenAndParents.splice(len, 0, a);
      let parent = myObj[a.parentId] || false;
      while (parent) {
        //only add parent if not already added:
        parent._used || filteredChildrenAndParents.splice(len, 0, parent);
        //mark each parent as used so not used again:
        myObj[parent.id]._used = true;
        //try to find parent of the current parent, if exists:
        parent = myObj[parent.parentId] || false;
      }
    }
  }
  return filteredChildrenAndParents;
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
...