2

I have a function to filter results by several params, such as name, status, type and date range (startDate and endDate). I would like to be able to filter results by these params together, but only if they are present, i. e. I can pass name and status, but don't pass type. I don't know how to do this with a date range. Now the filter is working only if I pass startDate and endDate, in all other cases, even if other params are present and there is corresponding data in the array, it returns null. How can I make startDate and endDate optional?

Here is my filter:

if (params.name || params.status || params.type || params.startDate && params.endDate) {
  const startDate = new Date(params.startDate).setHours(0,0,0);
  const endDate = new Date(params.endDate).setHours(23,59,59);
  dataSource = tableListDataSource.filter(
    (data) =>
      data.name.match(new RegExp(params.name, 'ig')) &&
      data.status.includes(params.status || '') &&
      data.type.includes(params.type || '') &&
      (
        new Date(data.createdAt).getTime() > startDate && new Date(data.createdAt).getTime() < endDate
      )
  );
}

Thank you for your help!

EDIT:

I'm using this filter inside a function on backend:

function getRule(req, res, u) {
  let realUrl = u;

  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
    realUrl = req.url;
  }

  const params = parse(realUrl, true).query;

  if (params.name || params.status || params.type || params.startDate && params.endDate) {
    const startDate = new Date(params.startDate).setHours(0,0,0);
    const endDate = new Date(params.endDate).setHours(23,59,59);
    dataSource = tableListDataSource.filter(
      (data) =>
        data.name.match(new RegExp(params.name, 'ig')) &&
        data.status.includes(params.status || '') &&
        data.type.includes(params.type || '') &&
        (
          new Date(data.createdAt).getTime() > startDate && new Date(data.createdAt).getTime() < endDate
        )
    );
  }

  const result = {
    data: dataSource,
    success: true,
  };

  return res.json(result);
}
jupiteror
  • 1,085
  • 3
  • 24
  • 50
  • 1) || converts comparing values to boolean. So even if you set a value to 0 your check for that value will fail. Use ?? instead or params.hasOwnProperty("type"), ... 2) Thats a perfect case for predefined params from object destruction. Your function could look like function ({name = null, type = null, status = null} = {}) {...} simply write a programm for it. Inside the filter() for each non null option make a check. If all checks have passed return true, otherwise on the first fail return false. – Kai Lehmann Feb 08 '21 at 22:05
  • @KaiLehmann Thank you very much for your reply. Could you please make an example of such function and post it as an answer? – jupiteror Feb 08 '21 at 22:21
  • @KaiLehmann params are passed in an url like this: `/api/order?startDate=2021-02-09&endDate=2021-03-15`, so it's not an object – jupiteror Feb 09 '21 at 06:12
  • 2
    Does this answer your question? [Filter collection with optional values](https://stackoverflow.com/questions/51497918/filter-collection-with-optional-values) – Kai Lehmann Feb 14 '21 at 10:18
  • @KaiLehmann Thank you very much, I'll check it out! – jupiteror Feb 14 '21 at 10:31

2 Answers2

1

By using your current approach, you can make the startDate and endDate optional by doing as following;

&& (
  (params.startDate && params.endDate) ? 
    (new Date(data.createdAt).getTime() > startDate && new Date(data.createdAt).getTime() < endDate) :
    true
)

So, what above does is basically check if params.startDate and params.endDate have no falsy values;

  • If they don't, then do your existing filter with the dates;
  • Otherwise if one of them do have falsey value, ignore the date-related filter by returning true.

This is how your final code will look like;

if (params.name || params.status || params.type || params.startDate && params.endDate) {
  const startDate = new Date(params.startDate).setHours(0,0,0);
  const endDate = new Date(params.endDate).setHours(23,59,59);
  dataSource = tableListDataSource.filter(
    (data) =>
      data.name.match(new RegExp(params.name, 'ig')) &&
      data.status.includes(params.status || '') &&
      data.type.includes(params.type || '') &&
      (
        (params.startDate && params.endDate) ? 
          (new Date(data.createdAt).getTime() > startDate && new Date(data.createdAt).getTime() < endDate) :
          true
      )
  );
}

Edit:

Normally, I'd suggest that filtering should not be done in FE, but rather in BE stack. Therefore you could only fetch the needed data along with pagination supports.

However, if you insist doing it in FE - I'd suggest that to encapsulate both filter function and handling params to filter data source. Blacklist everything and whitelist only the accepted params, and scale along the way as needed.

The following is an example of how I'd do it.

Note that; the filterDataSource complexity increases with the amount of fields you would support to be filtered. The fields iteration inside it equals to stacking multiple if conditions with extra steps.

/** 
 * @description Filters dataSource with provided fields
 * @param dataSource An array containing the data source
 * @param fields An key-value pair containing { [dataSourceField]: callbackFn(value) | "string" | number }
 */
const filterDataSource = (dataSource, fields) => {
  if (dataSource && dataSource.length) {
    return dataSource.filter((row) => {
      const rowFiltered = [];

      /**
       * @todo Scale the type of filter you want to support and how you want to handle them
       */
      for (const fieldName in fields) {
        if (Object.hasOwnProperty.call(fields, fieldName) && Object.hasOwnProperty.call(row, fieldName)) {
          const filter = fields[fieldName];

          if (typeof filter === 'function') {
            /** Call the callback function which returns boolean */
            rowFiltered.push(!!filter(row));
          }
          else if (typeof filter === 'object' && filter instanceof RegExp) {
            /** Predicate by regex */
            rowFiltered.push(!!row[fieldName].match(filter));
          }
          else if (typeof filter === 'string') {
            /** Exact match of string */
            rowFiltered.push(!!row[fieldName].match(new RegExp(filter, 'ig')));
          }
          else if (typeof filter === "number") {
            /** Exact match of number */
            rowFiltered.push(row[fieldName] === filter);
          }
        }
      }

      /** If this row is part of the filter, ONLY return it if all filters passes */
      if (rowFiltered.length > 0) {
        /** This will check if all filtered return true */
        return rowFiltered.every(Boolean);
      }
      else {
        /** If this row is NOT part of the filter, always return it back */
        return true;
      }
    });
  }

  return dataSource;
}


/**
 * @description Filter your datasource with pre-defined filter function for supported params
 * @param dataSource An array of object containing the data
 * @param params A set of object containing { [param]: value }
 * @todo Safely guard the wathched params here, encode them if needed.
 */
const filterDataByParams = (dataSource, params) => {
  const fieldsToFilter = {};

  if (params.name) {
    fieldsToFilter['name'] = new RegExp(params.name, 'ig');
  }
  
  if (params.status) {
    fieldsToFilter['status'] = params.status;
  }
  
  if (params.type) {
    fieldsToFilter['type'] = params.type;
  }
  
  if (params.startDate && params.endDate) {
    /**
     * Assuming createdAt is EPOCH
     * @todo What is the type of row.createdAt and params.startDate? 
     * @todo Adjust the logic here and apply validation if needed.
     */
    const startMillis = new Date(params.startDate).getTime() / 1e3, // Millis / 1e3 = EPOCH
      endMillis = new Date(params.endDate).getTime() / 1e3; // Millis / 1e3 = EPOCH

    /** Should we give a nice warning if invalid date value is passed? */
    if (isNaN(startMillis) && isNaN(endMillis)) {
      console.error('Invalid date params passed. Check it!');
    }

    /** Random defensive - remove or add more */
    if (startMillis && endMillis && startMillis > 0 && endMillis > 0 && startMillis < endMillis) {
      fieldsToFilter['createdAt'] = (row) => {
        return row.createdAt >= startMillis && row.createdAt <= endMillis;
      };
    }
  }

  if (Object.keys(fieldsToFilter).length) {
    return filterDataSource(dataSource, fieldsToFilter);
  }
  else {
    return [...dataSource];
  }
}


/** 1k Set of mocked data source with createdAt between 1 Jan 2019 to 13 February 2021 */
fetch('https://api.jsonbin.io/b/6027ee0987173a3d2f5c9c3d/3').then((resp) => {
  return resp.json();
}).then((mockDataSource) => {
  mazdaFilteredData = filterDataByParams(mockDataSource, {
    'name': 'Mazda',
    'startDate': '2019-05-04T19:06:20Z',
    'endDate': '2020-08-09T19:06:20Z'
  });
  
  hondaFilteredData = filterDataByParams(mockDataSource, {
    'name': 'honda',
    'startDate': '2019-10-05T00:00:00Z',
    'endDate': '2020-12-09T23:23:59Z'
  });
  
  mercedezFilteredData = filterDataByParams(mockDataSource, {
    'name': 'merce',
    'startDate': '2020-01-01T00:00:00Z',
    'endDate': '2021-12-31T23:23:59Z'
  })
  
  console.log({mazdaFilteredData, hondaFilteredData, mercedezFilteredData});
});
choz
  • 17,242
  • 4
  • 53
  • 73
  • Which is the same as `params.startDate === undefined || params.endDate === undefined || new Date() ...`. – Marco Luzzara Feb 11 '21 at 19:09
  • 1
    @MarcoLuzzara Not really, [falsies](https://stackoverflow.com/questions/19839952/all-falsey-values-in-javascript/19839953#19839953) are not just `undefined`. And I know what you mean, but you still need to return `truthy` value in order to ignore the date filter. In your code above, it'll return `falsy` one to the `.filter` method - which `tableListDataSource` will be an empty array since the filter will all return `falsy` – choz Feb 11 '21 at 19:21
  • Right, I got your point. I simply assumed they were `undefined` if not provided. – Marco Luzzara Feb 11 '21 at 19:28
  • That's exactly what I was looking for! Thank you very much for your answer and the explanation! – jupiteror Feb 12 '21 at 10:07
  • Just a few things: 1) first part is a hell in readability and 2) you don't use >= and <= so it will fail if is is the exact same timestamp. 3) the (params.name || params.status || params.type || params.startDate && params.endDate) will fail if name, status or type is one of the following: undefined, null, 0, "0" or null. So whenever you serach for the status 0 (which is pretty standard) it will fail. This code is not fail save for regular use cases and should be avoided in production code. – Kai Lehmann Feb 12 '21 at 22:45
  • @KaiLehmann I wouldn't do that if I were him. I am simply putting him in track to answer his question which he was struggling with the filters. Why don't you suggest your approach as an answer? – choz Feb 13 '21 at 02:36
  • @choz I'm she :) So what would you do instead? – jupiteror Feb 13 '21 at 06:56
  • @jupiteror Apologize for the fail courtesy - I've edited my answer to give you some thoughts regarding one of many ways you could achieve your goal. – choz Feb 13 '21 at 15:44
  • Again. if (param.name) will autoconvert the value to a Boolean. That means values like "", 0, "0" will still not be noticed. That is simply a bug. Use params.hasOwnProperty("name") to detect if a key is present. And don't compare xyz === undefined. You're comparing it to the undefined pointer which is non standard and can cause errors. Use typeof xyz === 'undefined' instead. That is save. – Kai Lehmann Feb 13 '21 at 20:12
  • https://stackoverflow.com/questions/2778901/how-to-compare-variables-to-undefined-if-i-don-t-know-whether-they-exist – Kai Lehmann Feb 13 '21 at 20:17
  • @KaiLehmann The first time I see it, I expected params as in query params. And no, `"0"` will be noticed, since its not falsey value. – choz Feb 14 '21 at 01:47
  • 1
    @choz Thank you very much for your update! Actually, I'm using this filter on backend, I have updated my question to show the whole function. I'm trying to incorporate your code, but I get a syntax error. I will try some more. Anyway thank you very much for your help! – jupiteror Feb 14 '21 at 10:28
0
function getPresentParams(param, checkList) {
  // we start with an empty Array
  let output = [];
  // for each item in checkList
  checkList.forEach(item => {
  
    // if the item is an array, multiple params are needed 
    if (Array.isArray(item)) {
        // the item is an itemList
      let allItemsPresent = true;
      for (const itemListItem of item) {
        // if one of the list is not there
        if (!param.hasOwnProperty(itemListItem)) {
            allItemsPresent = false;
          // then stop the loop
            break;
          }
      }
      // else all are matching
      if (allItemsPresent) {
        // add all to our output
                output.push(...item);
      }
    }
    // else if the item is not an Array
    else {
        // simple check if the param is present
        if (param.hasOwnProperty(item)) {
        output.push(item);
      }
    }
  })
  return output;
}

const params = {type: "car", color:"red", tires:4};

// any of "type", "tires" or "windows" should be searched
console.log(getPresentParams(params, ["type", "tires", "windows"]));
// output is ["type", "tires"]

// only show matches with "color" AND "type"
console.log(getPresentParams(params, [["color", "type"]]));
// output is ["color", "type"]

// show matches with "color" or ["windows" AND "tires"]
console.log(getPresentParams(params, ["color", ["windows", "tires"]]));
// output is ["color"]

with this function you get an Array with all present params you are searching for. By passing 2 or more paramNames as Array it will only add them to the list if ALL of them are present.

Then you can simply check in your filter function if the param is present, make your check and if it fails return false.

const foundParams = getPresentParams(params, ["type", "status", ["startDate", "endDate"], "name"]);

And then in your filter function:

if (foundParams.includes("startDate") && foundParams.includes("endDate")) {
    // if your dates are not matching return false
}
if (foundParams.includes("name")) {
    // if your name isn't matching return false
}
// and so on

// at least if all checks have passed
return true;

Thats only one solution. There are some more like an object with key: arrow functions and some iteration. But I think with this solution you get an better idea what you are doing

Kai Lehmann
  • 508
  • 4
  • 17
  • Thank you for your reply, but I get `params.hasOwnProperty is not a function` when running `getPresentParams` function in here `// simple check if the param is present`. – jupiteror Feb 10 '21 at 16:26
  • Oh, its a typo. It should be param not params. I'll correct that – Kai Lehmann Feb 11 '21 at 17:04
  • @jupiteror btw thanks for asking for code and then bountiing a non fail-save code *thumbsup* – Kai Lehmann Feb 12 '21 at 22:40
  • I fixed the typo, but your code still didn't work. I'm new to programming, so I could not get an idea just from the comments, that's why I asked for a code example. – jupiteror Feb 13 '21 at 06:57
  • So the problem is not that you want to learn how to solve the problem. The problem is you wanted code to copy and paste without understanding what goes on. – Kai Lehmann Feb 13 '21 at 20:15
  • I learn best of all on actual examples, because I can read them, test and try. – jupiteror Feb 14 '21 at 09:41
  • I gave you an example. You wanted ready to use code. – Kai Lehmann Feb 14 '21 at 10:15