Поиск по вложенному массиву. TS

Нужно реализовать функцию, которая на вход принимает два массива и фильтрует первый согласно второму по заданным правилам.

Примеры:

Массив, который нужно отфильтровать
const response = [
  {
    name: {
      first: 'TestFirst',
      last: 'TestLast2',
    },
    },
    goal: 'goal_1',
    isTrue: true,
  },
  {
    name: {
      first: 'TestFirst2',
      last: 'TestLast2',
    },
    goal: 'goal_2',
    isTrue: true,
  },
];
Массив с фильтрами
const filter = [
  {
    property: 'name.first',
    in_values: ['TestFirst', 'TestFirst2'],
  },
  {
    property: 'name.last',
    not_in_values: ['TestLast2'],
  },
  {
    property: 'goal',
    in_values: ['goal_1', 'goal_2'],
  },
  {
    property: 'isTrue',
    in_values: [true],
  },
];
Результирующий массив
const result = [
  {
    name: {
      first: 'Test',
      last: 'Test',
    },
    goal: 'goal_1',
    isTrue: true,
  },
];

property - поле, которое нужно проверить.

in_values - массив значений. Если есть хоть одно совпадение, тогда запись попадает в результирующий массив

not_in_values - массив значений. Если есть хоть одно совпадение, тогда запись НЕ попадает в результирующий массив

Если все "правила" фильтрации были применены успешно, тогда запись попадает в результирующий массив.

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


Ответы (1 шт):

Автор решения: Jake
type Filter = {
  /** Путь к проверяемому полю, разделенный точками (path.to.field) */
  property: string;
  /** Поле должно содержать хотя бы одно из значений */
  in_values?: any[];
  /** Поле не должно содержать ни одного из значений */
  not_in_values?: any[];
};

const filters: Filter[] = [
  {
    property: 'name.test.foo',
    in_values: ['bar'],
  },
  {
    property: 'name.first',
    in_values: ['TestFirst1', 'TestFirst2'],
  },
  {
    property: 'name.last',
    not_in_values: ['TestLast2'],
  },
  {
    property: 'goal',
    in_values: ['goal_1', 'goal_2'],
  },
  {
    property: 'isTrue',
    in_values: [true],
  },
];

const response = [
  {
    name: {
      first: 'TestFirst1',
      last: 'TestLast1',
      test: {
        foo: 'bar',
      }
    },
    goal: 'goal_1',
    isTrue: true,
  },
  {
    name: {
      first: 'TestFirst2',
      last: 'TestLast2',
      test: {
        foo: 'foo',
      }
    },
    goal: 'goal_2',
    isTrue: true,
  },
];

/**
 * Получение вложенного поля по указанному пути
 * @example getNestedPropertyValue({ test: { foo: "bar" } }, "test.foo"); // "bar"
 */
function getNestedPropertyValue(object: any, path: string): any {
  for (const field of path.split('.')) {
    object = object[field];
  }

  return object;
}

/** Содержится ли значение value в указанном массиве values */
function getIsInValues(value: any, values: any[]) {
  return values.includes(value);
}

/** Проходит ли фильтрацию указанное значение */
function getIsAvailableItem(value: any, filter: Filter) {
  if (filter.in_values && !getIsInValues(value, filter.in_values)) {
    return false;
  }

  if (filter.not_in_values && getIsInValues(value, filter.not_in_values)) {
    return false;
  }

  return true;
}

/** Фильтрация массива */
function filter<Object>(array: Object[], filters: Filter[]): Object[] {
  const filteredArray: Object[] = [];

  // Проходим по массиву проверяемых значений
  itemsLoop:
  for (const item of array) {

    // Проходим по массиву фильтров
    for (const filter of filters) {
      // Получаем значение целевого поля по пути фильтра
      const targetFieldValue = getNestedPropertyValue(item, filter.property);

      // Пропускаем текущий элемент если значение целевого поля не проходит фильтр
      if (!getIsAvailableItem(targetFieldValue, filter)) {
        continue itemsLoop;
      }
    }

    // Добавляем элемент в массив результатов, так как он успешно прошел все фильтры
    filteredArray.push(item);
  }

  return filteredArray;
}

console.log(filter(response, filters));

Важно!

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

Если Вам нужно считать отсутствие полей как отрицательный результат фильтрации, замените указанные функции на эти.

function getNestedPropertyValue(object: any, path: string): any {
  for (const field of path.split('.')) {
    object = object[field];

    if (object === undefined) {
      return undefined;
    }
  }

  return object;
}

function filter<Object>(array: Object[], filters: Filter[]): Object[] {
  const filteredArray: Object[] = [];

  // Проходим по массиву проверяемых значений
  itemsLoop:
  for (const item of array) {

    // Проходим по массиву фильтров
    for (const filter of filters) {
      // Получаем значение целевого поля по пути фильтра
      const targetFieldValue = getNestedPropertyValue(item, filter.property);

      // Пропускаем текущий элемент если значение не найдено
      if (targetFieldValue === undefined) {
        continue itemsLoop;
      }

      // Пропускаем текущий элемент если значение целевого поля не проходит фильтр
      if (!getIsAvailableItem(targetFieldValue, filter)) {
        continue itemsLoop;
      }
    }

    // Добавляем элемент в массив результатов, так как он успешно прошел все фильтры
    filteredArray.push(item);
  }

  return filteredArray;
}
→ Ссылка