Filter array of objects based on nested properties with Ramda

44 views Asked by At

I have this data structure:

const data = [
    { name: 'John', age: 36, color: {red: 243, green: 22, blue: 52} },
    { name: 'Jane', age: 28, color: {red: 23, green: 62, blue: 15} },
    { name: 'Lisa', age: 42, color: {red: 89, green: 10, blue: 57} }
]

And I want to filter it based on a list of conditions, like this (filter property is mapped to a predicate function from ramda):

const definitions = [
    {
        id: 'name',
        filter: 'includes',
        path: [],
    },
    {
        id: 'age',
        filter: 'gte',
        path: [],
        value: 36
    }
]

Using this code works fine for properties that is directly on the object:

const Operations = {
    includes,
    equals,
    gte
}

const filters = reduce((a, c) => {
    a[c.id] = c.value ? Operations[c.filter](c.value) : always(true);
    return a;
}, {}, definitions)

filter(where(filters), data);

See Ramda playground link

Problem is, I want to introduce other filter conditions that operate on nested structures. For that I introduced a path property, that would take an array of strings that represents the path where the property is located on the object:

const definitions = [
    {
        id: 'name',
        filter: 'includes',
        path: [],
    },
    {
        id: 'age',
        filter: 'gte',
        path: [],
        value: 36
    },
    {
        id: 'red',
        filter: 'gte',
        path: ['color', 'red'],
        value: 40
    },
    {
        id: 'blue',
        filter: 'gte',
        path: ['color', 'blue']
    }
]

This should result in (filters only take effect when there is a value exist in the definition):

{
    age: 36,
    color: {
        blue: 52,
        green: 22,
        red: 243
    },
    name: "John"
}

However, I can't make it work with where. I could pre-map the array to bring the nested properties to the uppermost level of the objects, but I guess there is a more elegant way.

2

There are 2 answers

0
marchello On

I found out eventually, just have to drop where and change it to use allPass:

const filtered = filter(allPass(values(activeFilters)), data);

And change the reducer to:

reduce((a, c) => {
    a[c.id] = (val) => {
        const getPath = c.path ? path([...c.path, c.id]) : path([c.id])
        return c?.value ? FilterOperations[c.active](c.value)(getPath(val)) : always(true);
    }
    return a;
}, {}, definitions)

0
Hitmands On

I'd suggest pathSatisfies but you'd have to switch gte for lte.

const createPredicates = R.reduce((res, def) => {
  const predicate = def.value 
    ? R.pathSatisfies(R[def.filter](def.value), def.path)
    : R.T;

  return [...res, predicate];
}, []);

const data = [
  { name: 'John', age: 36, color: {red: 243, green: 22, blue: 52} },
  { name: 'Jane', age: 28, color: {red: 23, green: 62, blue: 15} },
  { name: 'Lisa', age: 42, color: {red: 89, green: 10, blue: 57} }
];

const definitions = [
  {
    id: 'name',
    filter: 'includes',
    path: [],
  },
  {
    id: 'age',
    filter: 'lte',
    path: ['age'],
    value: 36
  },
  {
    id: 'red',
    filter: 'lte',
    path: ['color', 'red'],
    value: 40
  },
  {
    id: 'blue',
    filter: 'lte',
    path: ['color', 'blue']
  }
];

const fn = R.filter(
  R.allPass(createPredicates(definitions)),
);

console.log(
  fn(data),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.29.0/ramda.js" integrity="sha512-J4Em3sC41YRNdTfPiA2QTHFVyPP7Qqpac9s+sb4p4bdfZfiJai2s1EXZcDB+V0r23kYc42p/cFVlrbz7/1Zwjg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>