Refactor dynamic filter functions

Posted on

Problem

I create this menu filter component:

Menu

with this menu, in practice, you can filter on a cards list feeling free to choose different filters at the same time; the cards list returned will be the results of all options selected.

to generate this kind of filter I filter on filtered option and so on, in other words like so:

return props.tasks.filter(x => {
    if (props.key.includes('task-status')) {
      return props.status.some(status => x['task-status'] === status)
    }
    return true
  }).filter(x => {
    if (props.key.includes('task-priority')) {
      return props.priority.reduce((vres, val) => vres.concat(val), []).some(priority => x['task-priority'] === priority)
    }
    return true;
  }).filter(x => {
    if (props.key.includes('task-actual-owner')) {
      return props.users.some(user => x['task-actual-owner'] === user)
    }
    return true;
  }).filter(x => {
    if (props.key.includes('task-expiration-time')) {
      if (x['task-expiration-time'] === null) {
        return false;
      }

      const f = props.expiration.flat();
      return x['task-expiration-time']['java.util.Date'] >= f[0] && x['task-expiration-time']['java.util.Date'] <= f[1]
    }
    return true;
  })

So the entire list of cards come from this array of objects:

array = [
  {
    "task-id": 142,
    "task-name": "Task",
    "task-subject": "",
    "task-description": "",
    "task-status": "Ready",
    "task-priority": 0,
    "task-is-skipable": false,
    "task-actual-owner": null,
    "task-created-by": null,
    "task-created-on": {
      "java.util.Date": 1606322625000
    },
    "task-activation-time": {
      "java.util.Date": 1606322625000
    },
    "task-expiration-time": {
      "java.util.Date": 1200832320000
    },
    "task-proc-inst-id": 187,
    "task-proc-def-id": "businessProcess.main",
    "task-container-id": "businessProcess_1.0.1-SNAPSHOT",
    "task-parent-id": -1,
    "correlation-key": "187",
    "process-type": 1
  },
  {
    "task-id": 141,
    "task-name": "Task",
    "task-subject": "",
    "task-description": "",
    "task-status": "InProgress",
    "task-priority": 9,
    "task-is-skipable": false,
    "task-actual-owner": "john.doe",
    "task-created-by": null,
    "task-created-on": {
      "java.util.Date": 1606322577000
    },
    "task-activation-time": {
      "java.util.Date": 1606322577000
    },
    "task-expiration-time": {
      "java.util.Date": 1674200580000
    },
    "task-proc-inst-id": 186,
    "task-proc-def-id": "businessProcess.main",
    "task-container-id": "businessProcess_1.0.1-SNAPSHOT",
    "task-parent-id": -1,
    "correlation-key": "186",
    "process-type": 1
  },
  {
    "task-id": 140,
    "task-name": "Task",
    "task-subject": "",
    "task-description": "",
    "task-status": "Reserved",
    "task-priority": 6,
    "task-is-skipable": false,
    "task-actual-owner": "peter.griffin",
    "task-created-by": null,
    "task-created-on": {
      "java.util.Date": 1606322524000
    },
    "task-activation-time": {
      "java.util.Date": 1606322524000
    },
    "task-expiration-time": {
      "java.util.Date": 1863598080000
    },
    "task-proc-inst-id": 185,
    "task-proc-def-id": "businessProcess.main",
    "task-container-id": "businessProcess_1.0.1-SNAPSHOT",
    "task-parent-id": -1,
    "correlation-key": "185",
    "process-type": 1
  },
  {
    "task-id": 139,
    "task-name": "Task",
    "task-subject": "",
    "task-description": "",
    "task-status": "Reserved",
    "task-priority": 0,
    "task-is-skipable": false,
    "task-actual-owner": "homer.simpson",
    "task-created-by": null,
    "task-created-on": {
      "java.util.Date": 1606322446000
    },
    "task-activation-time": {
      "java.util.Date": 1606322446000
    },
    "task-expiration-time": null,
    "task-proc-inst-id": 184,
    "task-proc-def-id": "businessProcess.main",
    "task-container-id": "businessProcess_1.0.1-SNAPSHOT",
    "task-parent-id": -1,
    "correlation-key": "184",
    "process-type": 1
  },
  {
    "task-id": 138,
    "task-name": "Task",
    "task-subject": "",
    "task-description": "",
    "task-status": "Reserved",
    "task-priority": 0,
    "task-is-skipable": false,
    "task-actual-owner": "jim.carrey",
    "task-created-by": null,
    "task-created-on": {
      "java.util.Date": 1606322412000
    },
    "task-activation-time": {
      "java.util.Date": 1606322412000
    },
    "task-expiration-time": null,
    "task-proc-inst-id": 183,
    "task-proc-def-id": "businessProcess.main",
    "task-container-id": "businessProcess_1.0.1-SNAPSHOT",
    "task-parent-id": -1,
    "correlation-key": "183",
    "process-type": 1
  }
]

and the array of objects created by the multiple options choose can be like this (for example):

[
  {
    key: "task-status",
    label: "Waiting For",
    status: "Ready"
  },
  {
    key: "task-status",
    label: "Picked Up",
    status: "Reserved"
  }
  {
    key: "task-priority",
    label: "Low",
    priority: [0, 1, 2, 3, 4]
  },
  {
    key: "task-actual-owner",
    label: "John Doe",
    owner: "john.doe"
  },
  {
    key: "task-expiration-time",
    expiration: [1611598726000, 1611609526000]
  }
];

All works properly but I’m trying to find a way to refactor the first function…(return props.tasks.filter(x => { ...)
Is there a way to achieve this?

Solution

There are two main things you can do to clean it up a bit:

  1. Instead of chaining a bunch of .filter()s together, create a list of the filter functions you wish to use, then only use the selected ones.
  2. You can break up the function a bit by pulling out and naming each of the filter functions. This can make it easier to understand at a glance what logic your function is doing.

Here’s one way to code it up after applying those two ideas.
(The following is untested, but should give an idea)

function applyFilters(props) {
  const filters = [];
  if (props.key.includes('task-status')) {
    filters.push(createFilterFor.status(props));
  }
  if (props.key.includes('task-priority')) {
    filters.push(createFilterFor.priority(props));
  }
  if (props.key.includes('task-actual-owner')) {
    filters.push(createFilterFor.owner(props));
  }
  if (props.key.includes('task-expiration-time')) {
    filters.push(createFilterFor.expirationTime(props));
  }

  return props.tasks.filter(combineFilters(filters));
}

const combineFilters = filters => (
  entry => filters.every(filter => filter(entry))
);

const createFilterFor = {
  status: ({ status }) => (
    task => status.some(statusText => task['task-status'] === statusText)
  ),
  priority: ({ priority }) => (
    task => priority.flat(1).some(priorityText => task['task-priority'] === priorityText)
  ),
  owner: ({ users }) => (
    task => users.some(user => task['task-actual-owner'] === user)
  ),
  expirationTime: ({ expiration }) => (
    task => {
      if (!task['task-expiration-time']) return false;

      const taskExpiration = task['task-expiration-time']['java.util.Date'];
      const [start, end] = expiration.flat();
      return taskExpiration >= start && taskExpiration <= end;
    }
  ),
};

If you find yourself adding a lot of filter functions that follow this pattern, you could add a layer of abstraction that allows you to remove the if-then chain, but this makes the code more unreadable and less flexible.

Update

Based on @DiddyO’s feedback, I think I understand what the goal of this question is better. Here’s an updated, much more simplified version that should do what was asked (assuming I understood the comment right).

(Once again, this is untested and may have a bug or two, but it should give an idea of how to go about accomplishing this task)

const applyFilters = props => (
  props.tasks.filter(createFilterFromProps(props))
);

const createFilterFromProps = props => (
  entry => Object.entries(filters).every(
    ([key, filter]) => !props.key.includes(key) || filter(props, entry[key])
  )
);

const filters = {
  'task-status': ({ status }, taskStatus) => status.some(statusText => taskStatus === statusText),
  'task-priority': ({ priority }, taskPriority) => priority.flat(1).some(priorityText => taskPriority === priorityText),
  'task-actual-owner': ({ users }, actualOwner) => users.some(user => user === actualOwner),
  'task-expiration-time': ({ expiration }, taskExpiration) => {
    if (!taskExpiration) return false;
    const [start, end] = expiration.flat();
    return taskExpiration['java.util.Date'] >= start && taskExpiration['java.util.Date'] <= end;
  },
};
```

Leave a Reply

Your email address will not be published. Required fields are marked *