1

The following code processes a list of file paths and should return only the file names (without extension) of XML files. Currently I got to this:

const filteredFiles = files
  .map(f => f.match(/.*\/(.*)\.xml/)) // map to regex match with capture
  .filter(v => v)                     // non-matches returned null and will be filtered out here
  .map(m => m[1])                     // map out the regex capture

I find this code quite cumbersome. Is there no way to combine the matching and filtering in a more "efficient" way? And by "efficient" I mean code-readable-efficient and not time-efficient as the input array holds 100 values at most but most of the time between 10 and 20.

Koen
  • 3,626
  • 1
  • 34
  • 55
  • Using foreach and pushing altered elements to filteredFiles in foreach might be what you want. I think it is more code-readable-efficient. – burakarslan Jul 06 '22 at 07:05
  • 2
    If are looking in this way to optimize your code or it should iterate only once. so you can use `.reduce` example `files.reduce((r, f) => { const value = f.match(/.*\/(.*)\.xml/); if (value?.[1]) { return [...r, value[1]]} return r;},[])` – Narendra Jadhav Jul 06 '22 at 07:06
  • This could be of interest: [How to chain map and filter functions in the correct order](https://stackoverflow.com/q/44198833) – VLAZ Jul 06 '22 at 07:17

4 Answers4

2

Map and filter, otherwise known as reduce

const rx = /\/(.*)\.xml$/;

const filteredFiles = files.reduce((arr, f) => {
  const match = f.match(rx);
  return match ? [...arr, match[1]] : arr;
}, []);
Phil
  • 157,677
  • 23
  • 242
  • 245
2

This doesn't solve your need of mapping and filtering out the non matching values in one shot... but it makes one step easier by using the optional chaining operator ?.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining

I also slightly changed the regex to allow the filename with no path specifed.

const files = [
  './path/to/filename_01.xml',
  'non_matching_value',
  './path/to/filename_02.xml',
  './path/to/filename_03.xml',
  './path/to/filename_04.xml',
  'filename_05.xml',
];

//Approach using filter over map to ignore non matching array items
//-------------------------------------------------------------------------------
const filteredFiles = files
  .map(filename => filename.match(/^(.*\/)?(.*)\.xml/)?.[2])
  .filter(filename => filename);
  
console.log(filteredFiles);

//Approach using flatMap as suggested by another user answering the same question
//-------------------------------------------------------------------------------
const filteredFiles2 = files.flatMap((f)=>{
  //here you are forced to put the match result in a dedicated variable..
  const match = f.match(/^(.*\/)?(.*)\.xml/);
  //because you need to use it both on the condition and the positive outcome
  return ( match ) ? [match[2]] : [];
});

console.log(filteredFiles2);

//Approach using flatMap as suggested by another user answering the same question
//AND using the null coealeshing operator to return empty array in case of non matching string
//-------------------------------------------------------------------------------
const filteredFiles3 = files.flatMap(f => f.match(/^(.*\/)?(.*)\.xml/)?.[2] ?? []);

console.log(filteredFiles3);
Diego D
  • 6,156
  • 2
  • 17
  • 30
  • Didn't think of optional chaining. Could be great in combination with `.flatMap(… ?? [])` (suggested by @mousetail) so I can drop the `.filter()` but don't know about code readability. – Koen Jul 06 '22 at 07:25
  • well it would be hard to fit `.?` in that exact scenario because the alternative option to returning a filled array it's an empty array. But to collapse the whole if statement a ternary operator would be suitable in that case: `return (match) ? [match[1]] : [];` – Diego D Jul 06 '22 at 07:44
  • `?.` would return `undefined` if n/a so I guess you could use the default operator here `.flatMap(f => f.match(/^(.*\/)?(.*)\.xml/)?.[2] ?? [])`? – Koen Jul 07 '22 at 08:12
  • 1
    yes you did it right.. I also updated my question showing all those 3 different approaches – Diego D Jul 07 '22 at 11:41
2

As I added in comment, you could use reduce method of array to achieve this in single iteration

Example

const regex = /\/(.*)\.xml$/;

const filteredFiles = files.reduce((r, f) => {
  const value = f.match(regex);
  if (value?.[1]) {
    return [...r, value[1]];//If matches found then return previous result + new value 
  }
  return r; // If not matches found then return previous result 
}, []);
Narendra Jadhav
  • 10,052
  • 15
  • 33
  • 44
1

You can (ab)use flat map:

const filteredFiles = files.flatMap((f)=>{
  let match = f.match('...');
  if (match) {
      return [match[1]]
  } else {
      return []
  }
})

Not sure if it's actually better than the original though.

mousetail
  • 7,009
  • 4
  • 25
  • 45