Map and filter in one method? - javascript

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.

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);

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.

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;
}, []);

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
}, []);

Related

Nodejs find multiple value

I have two lists:
lista_source: 'B10L-A2,AABan38711$B10L-A2,AABan38811$B12A-A,AABan38912$B14-A2,AABan39314$B16B-A,AABan39616$B12A-A,AABan39818$B16L-B,AABan39919$B16L-B,AABan40019$B12A-A,AABan41112'
second_list: 'B10L-A2,B12A-A,B16L-B'
As a result I would like to get the following list (or similar one):
result = [B10L-A2:AABan38711,AABan38811],[B12A-A:AABan38912,AABan41112,AABan39818],[B16L-B:AABan39919,AABan40019]
In short, I'm looking for multiple values for the 2nd lists items.
I tried the filter function and write it to csv file but does not really work.
const first_list_object= first_list.split('$');
const second_list_object= second_list.split(',');
for (let i = 0; i < second_list_object.length; i++) {
let results= first_list_object.filter(x => x.includes(second_list_object[i]));
console.log(results);
writer = csvWriter({ sendHeaders: false });
writer.pipe(fs.createWriteStream(__dirname + '/lista.csv', { flags: 'a' }));
writer.write({
results
});
}
How should I solve it? Is there any better solution than filter?
A javascript object as output. If you need, I can convert this to .csv too.
const lista_source = 'B10L-A2,AABan38711$B10L-A2,AABan38811$B12A-A,AABan38912$B14-A2,AABan39314$B16B-A,AABan39616$B12A-A,AABan39818$B16L-B,AABan39919$B16L-B,AABan40019$B12A-A,AABan41112'
const second_list = 'B10L-A2,B12A-A,B16L-B'
// convert data to arrays
const source = lista_source.split(",")
const second = second_list.split(",")
// filter out source list items (into seperate object value) for each second list item
const res = second.reduce((obj, sec_key) => {
// get item, if string is not exact key name and string includes key name
const filtered = source.filter(key => key !== sec_key && key.includes(sec_key))
return ({...obj, [sec_key]: filtered })
}, {})
console.log(res)
Assuming that structure of the strings are gonna be like in question, I wrote same basic regex to split on and then add them accordingly to object. See if that's what you want.
Edit:
After rereading your question, I realized that comma actually doesn't separate values in your string but dolar sign instead (kinda weird but ok). I also added an if to take only values present in second list.
const lista_source = 'B10L-A2,AABan38711$B10L-A2,AABan38811$B12A-A,AABan38912$B14-A2,AABan39314$B16B-A,AABan39616$B12A-A,AABan39818$B16L-B,AABan39919$B16L-B,AABan40019$B12A-A,AABan41112'
const second_list = 'B10L-A2,B12A-A,B16L-B'.split(',')
const array = lista_source.match(/B[0-9]{2}[A-Z]-[A-Z][0-9]?,[A-Za-z0-9_]{10}/g)
let result = {}
for (let value of array) {
let [key, s] = value.split(',')
// only take keys form contained in second list
if(!second_list.includes(key)){
continue
}
key in result ? result[key] = [s, ...result[key]] : result[key] = [s]
}
console.log(result)

Filter on an object with an array inside

So, struggling a little with this one.
I have an object which looks like this:
const search = something.map((some) => {
const search = {
writer: some.writers, // (this is an array)
title: some.title.toLowerCase(),
reference: some.reference.toLowerCase(),
};
return search;
});
I am trying to do a search on all the text inside the values of the object.
The some.writer field is an array that can have numerous writers inside.
I am currently filtering through like this:
const filtered = search.filter((entry) => Object.values(entry).some((val) => typeof val === 'string'
&& val.includes(searchTerm)));
That filter looks for the search term inside the object and returns all strings which contain the term or part of the term.
The problem is, it only looks at the top level and does not go inside the writers array to search inside there too.
How can I do that?
See below for an option where you don't need the search array (provided you're doing full-string matches, not substrings), but if you keep the search array then When building it from the something array, I'd also make all the writers lower case (and I'd use toLocaleLowerCase for locale-awareness in the "to lower" operation):
const search = something.map((some) => {
const entry = {
writer: some.writers.map(writer => writer.toLocaleLowerCase()),
title: some.title.toLocaleLowerCase(),
reference: some.reference.toLocaleLowerCase(),
};
return entry;
});
Then when filtering, instead of building a bunch of intermediate arrays, I'd search more directly:
// I'm assuming `val` has already been put through `toLocaleLowerCase`
const filtered = search.filter(({title, reference, writers}) => {
return title === val || reference === val || writers.includes(val);
});
But if you want to make it dynamic so adding new properties to the entries works without modifying the search code (other than when you create search), then as has been pointed out in comments, you can use flat to flatten the array of values so writers is spread out into the top level of the array:
// I'm assuming `val` has already been put through `toLocaleLowerCase`
const filtered = search.filter(entry => Object.values(entry).flat().includes(val));
Both are fairly simple to tweak to make substring searches:
// I'm assuming `val` has already been put through `toLocaleLowerCase`
const filtered = search.filter(({title, reference, writers}) => {
return title.includes(val) || reference.includes(val) || writers.some(writer => writer.includes(val));
});
and
// I'm assuming `val` has already been put through `toLocaleLowerCase`
const filtered = search.filter(entry => Object.values(entry).flat().some(e => e.includes(val)));
You might also want to use normalize on the search values and val to handle discrepancies in the different ways some "characters" can be represented in Unicode. If so, basically change all the .toLocaleLowerCase() above to .normalize().toLocaleLowerCase().
If you're doing exact matches, you can avoid having to create the search array and doing all those toLowerCase/toLocaleLowerCase calls. This also has the advantage that the search results have the text in its original form. To do that you'd use an Intl.Collator object telling it that you want case-insensitive matching via the sensitivity option:
const {compare} = new Intl.Collator(undefined, {
sensitivity: "accent" // Or you might want `"base"`
});
const match = (a, b) => compare(a, b) === 0);
const filtered = something.filter(({title, reference, writers}) => {
return match(title, val) || match(reference, val) || writers.some(writer => match(writer, val));
});
You can use Array.prototype.flat to turn something like [1, [2, 3], 4] into [1, 2, 3, 4]:
const filtered = search.filter((entry) => Object.values(entry).flat().some((val) => typeof val === 'string' && val.includes(searchTerm)));
// ^^^^^^^

replace and filter method in one condition using JavaScript / node js [duplicate]

This question already has answers here:
Map and filter an array at the same time
(16 answers)
Closed 1 year ago.
I have an array which look like this
["home/work/abc.jpg",
"home/work/fish.pdf",
"home/work/fish.jpg",
"home/work/doc/animal.jpg",
"home/work/doc/animal.pdf"];
so I want to filter array which contain ".jpg" extension file so I filtered it out by using
array= array.filter((data)=>{
return data.indexOf(".jpg")>=0
});
so I got my expected value as
[ "home/work/abc.jpg",
"home/work/fish.jpg",
"home/work/doc/animal.jpg"
]
and I replace "home/work/" by using map function
let rep="home/work/";
array = array.map((data)=>{
data.replace(rep,"")
});
and got my value as
[ "abc.jpg",
"fish.jpg",
"doc/animal.jpg"
]
but the problem is I have to use two method to filter and replace them is there any possibility I can merge this two method and minimise my code
array= array.filter((data)=>{
return data.indexOf(".jpg")>=0
});
let rep="home/work/";
array = array.map((data)=>{
data.replace(rep,"")
});
expected output
[ "abc.jpg",
"fish.jpg",
"doc/animal.jpg"
]
By using any chaining method ?
You can chain off of the filtered array without creating another variable, and by using implicit return to make things more concise:
const filenames = ["home/work/abc.jpg",
"home/work/fish.pdf",
"home/work/fish.jpg",
"home/work/doc/animal.jpg",
"home/work/doc/animal.pdf"];
const rep="home/work/";
const result = filenames
.filter(file => file.includes('.jpg'))
.map(file => file.replace(rep, ''));
console.log(result);
To actually do it in a single iteration, you'd have to give up on the chaining, and use reduce or a standard iteration method.
const filenames = ["home/work/abc.jpg",
"home/work/fish.pdf",
"home/work/fish.jpg",
"home/work/doc/animal.jpg",
"home/work/doc/animal.pdf"];
const rep="home/work/";
const result = [];
for (const file of filenames) {
if (file.includes('.jpg')) result.push(file.replace(rep, ''));
}
console.log(result);
You can obtain same result using the same two method that you've used but inside reduce
const arr = [
"home/work/abc.jpg",
"home/work/fish.pdf",
"home/work/fish.jpg",
"home/work/doc/animal.jpg",
"home/work/doc/animal.pdf",
];
const result = arr.reduce((acc, curr) => {
if (curr.indexOf(".jpg") >= 0) {
acc.push(curr.replace("home/work/", ""));
}
return acc;
}, []);
console.log(result);
Try this. This will return the same output you are looking for.
Using both .map and .filter combined.
let array = array
.filter((data) => data.indexOf(".jpg")>=0)
.map((data) => data.replace("home/work/",""))

Filter array with multiple startsWith and endsWith arguments

Browsed several different questions/answers surrounding this question, but most rely on includes or indexOf
Problem: How any array to filter (names in this case). I need to filter it using 2 different arrays, one of which has startsWith criteria and the other has endsWith criteria
var names = ['BOB','CATHY','JAKOB','AARON','JUSTICE','BARBARA','DANIEL','BOBBY','JUSTINE','CADEN','URI','JAYDEN','JULIE']
startPatterns = ['BO','JU']
endPatterns = ['EN','ICE']
//res = ['BOB','JUSTICE','JUSTINE','JULIE','JAYDEN','JUSTICE']
Obviously you cannot do names.filter(d => d.startsWith(startPatterns)) because startPatterns is not a string but a array. Something like this isn't working and is terrible slow too:
res=[]
names.forEach(d => {
endPatterns.forEach(y => d.endsWith(y) ? res.push(d) : '')
startPatterns.forEach(s => d.startsWith(s) ? res.push(d) : '')})
console.log(res)
You can use Array.prototype.some on the pattern arrays to achieve this:
let filtered = names.filter(name => (
startPatterns.some(pattern => name.startsWith(pattern)) ||
endPatterns.some(pattern => name.endsWith(pattern))
))
The logic here being "Return true if the name begins with at least one of the start patterns OR ends with at least one of the end patterns".
You could build a regular expression and check the string against the pattern.
var names = ['BOB','CATHY','JAKOB','AARON','JUSTICE','BARBARA','DANIEL','BOBBY','JUSTINE','CADEN','URI','JAYDEN','JULIE'],
startPatterns = ['BO','JU'],
endPatterns = ['EN','ICE'],
regexp = new RegExp(`^(${startPatterns.join('|')})|(${endPatterns.join('|')})$`),
result = names.filter(s => regexp.test(s));
console.log(result);
A non regular expression approach with an array with the methods and wanted values.
var names = ['BOB','CATHY','JAKOB','AARON','JUSTICE','BARBARA','DANIEL','BOBBY','JUSTINE','CADEN','URI','JAYDEN','JULIE'],
patterns = [['startsWith', ['BO','JU']], ['endsWith', ['EN','ICE']]],
result = names.filter(s => patterns.some(([k, values]) => values.some(v => s[k](v))));
console.log(result);

Remove last letter from map value

So I'm getting this from backend:
{"Item":{"userEmail":"b","Username":"bUsername","Push":"sdsdsd","Password":"sdsds","Buddy":{"datatype":"SS","contents":{"Drake":"Drake","Ola":"Ola","b":"b","d":"d"}}}}
I use Object.Keys to narrow down the contents to:
Drake,Ola,b,d
Which I then map to give:
[{"id":"Drake"},{"id":"Ola"},{"id":"b"},{"id":"d"}]
Which is then used on my Angular Front-end as .id. I want to remove the last letter from each value i.e leaving Drak,Ol etc. I've tried many ways but have failed, how can I achieve this please so that the id has those values?
EDIT
I also want to now get that value that was cut AND add it such that the end product will be [{"id":"Drak",valueThatWasCut:"e"}]
You could iterate the object's keys and build with the short string a new object.
var data = {"Item":{"userEmail":"b","Username":"bUsername","Push":"sdsdsd","Password":"sdsds","Buddy":{"datatype":"SS","contents":{"Drake":"Drake","Ola":"Ola","b":"b","d":"d"}}}},
ids = Object.keys(data.Item.Buddy.contents).reduce(function (r, k) {
var n = k.slice(0, -1);
return n ? r.concat({ id: n }) : r;
}, []);
console.log(ids);
Perhaps something like :
var arr = [{"id":"Drake"},{"id":"Ola"},{"id":"b"},{"id":"d"}];
var result = arr.map(x => x.id.slice(0,-1));
console.log(result); // [ 'Drak', 'Ol', '', '' ]
Create a temporary contents object and change in that.
Then just set this in the original object. ES6 spread operators would save the rest of data without respecifying all keys and values.
let items = {"Item:{"userEmail":"b","Username":"bUsername","Push":"sdsdsd","Password":"sdsds","Buddy":{"datatype":"SS","contents":{"Drake":"Drake","Ola":"Ola","b":"b","d":"d"}}}};
let contents = items.Item.Buddy.contents;
let contentsNew = Object.keys(contents).map((content) => {
return {[content.substring(0, content.length-1)]: content.substring(0, content.length-1), valueThatWasCut: content[content.length-1]};
});
items = {...items, Item: {...items.Item,Buddy:{...items.Item.Buddy,contents: contentsNew}}};
console.log(items);

Categories