I am having a little trouble differentiating between spread and rest. Could someone explain to me if either spread or rest is being used within the reduce functions return statement?
This is what I am not understanding:
return [currentValue, ...accumulator]
inside of
let str = 'KING';
const reverseStr = str => {
return str
.split('')
.reduce((accumulator, currentValue) => {
return [currentValue, ...accumulator];
}, [])
.join('');
};
Rest syntax always creates (or assigns to) a variable, eg:
const [itemOne, ...rest] = arr;
// ^^ created ^^^^
Spread syntax only results in another array (or object) expression - it doesn't put anything into variables. Here, you're using spread syntax to create a new array composed of currentValue and the values of accumulator.
return [currentValue, ...accumulator];
is like
return [currentValue, accumulator[0], accumulator[1], accumulator[2] /* ... */];
You're spreading the items of accumulator into the array that's being returned.
Related
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
}, []);
I am trying to write a js flat function like Array.prototype.flat()
using concat+recursion. However, my code is always throw a acc.concat is not a function error. I don't know what's wrong with my code. Any one can help me with this issue? Thank you so much in advance!
function flatten (arr) {
return arr.reduce((acc, val) => {
if (Array.isArray(val)) {
acc.concat(flatten(val));
} else {
return val;
}
return acc;
}, []);
}
I see a couple issues:
Array#concat is not an in-place function. It returns a new array which needs to be assigned or returned acc = acc.concat(flatten(val)); or return acc.concat(flatten(val));.
vals don't get added to the accumulator at any point. return val; breaks the normal contract of the callback function which is to return the accumulator for the next element to use.
const flatten = a =>
a.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? flatten(val) : val)
, [])
;
console.log(flatten([1,2,3,4,[5,6,[7,[8,9]]]]));
Additionally, I think reduce is not the most appropriate function here. Whenever I find that I'm reducing onto an array and not filtering any elements, I know I can refactor to a Array#map, which is a more specific reduce. Prefer reduce only when filter or map aren't easily usable, such as when creating objects.
const flatten = a =>
[].concat(...a.map(val => Array.isArray(val) ? flatten(val) : val))
;
console.log(flatten([1,2,3,4,[5,6,[7,[8,9]]]]));
This avoids all of the rather awkward accumulator business.
But if you don't have access to the spread operator and are looking for full compatibility, then reduce is again useful because it enables us to call concat on each element:
function flatten(a) {
return a.reduce(function (acc, val) {
return acc.concat(Array.isArray(val) ? flatten(val) : val);
}, []);
}
console.log(flatten([1,2,3,4,[5,6,[7,[8,9]]]]));
This is because you do the step to terminate the recursion before doing the recursion and it's tripping you up.
When you do if (Array.isArray(val)) you should decide whether to treat val as an array and go into flatten again or not, in which case you can just continue treating it as normal. However, in both cases you want to add val to the accumulator, yet in the else branch, you simply do return val, so on the next execution of the callback, acc would be equal to val which we already know is not an array.
Instead, in both cases, you should be adding to the array. The only difference is that one time you should recursively flatten the other not. This can be simplified if the terminating condition is pulled to be part of flatten and not the reduce callback:
function flatten (data) {
if(!Array.isArray(data)) return data;
return data.reduce((acc, val) => acc.concat(flatten(val)), []);
}
const input = [[1], [2, [[3]]]];
console.log(flatten(input));
So, both times you call flatten but if the value is not an array, you simply return it, so you treat it as a normal acc.concat(1) - adding a plain value. If you get an array, then you recursively unwrap and flatten it. And since now flatten is not guaranteed to get an array, I've renamed the parameter to data to avoid confusion.
I'm trying to use the array reduce function to return a 2D array of objects. The input is a comma separated values. The first row of the string is used as the title row. I'm analyzing the solution and I don't understand the notation. Specifically I don't understand the "=> ((obj[title] =
values[index]), obj), {})" portion in the code below. I'm looking to have someone explain it to me. For me it seems like we're initializing obj to be a an object. After that I'm lost.
const CSV_to_JSON = (data, delimiter = ',') => {
const titles = data.slice(0, data.indexOf('\n')).split(delimiter);
return data
.slice(data.indexOf('\n') + 1)
.split('\n')
.map(v => {
const values = v.split(delimiter);
return titles.reduce(
(obj, title, index) => ((obj[title] = values[index]), obj)
, {});
});
};
console.log(CSV_to_JSON('col1,col2\na,b\nc,d')); // [{'col1': 'a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}];
console.log(CSV_to_JSON('col1;col2\na;b\nc;d', ';')); // [{'col1': a', 'col2': 'b'}, {'col1': 'c', 'col2': 'd'}]
It's an (ab)use of the comma operator, which takes a list of comma-separated expressions, evaluates the first expression(s), discards them, and then the whole (...) resolves to the value of the final expression. It's usually something only to be done in automatic minification IMO, because the syntax looks confusing.
The .reduce there
return titles.reduce((obj, title, index) => ((obj[title] =
values[index]), obj), {});
is equivalent to
return titles.reduce((obj, title, index) => {
obj[title] = values[index];
return obj;
}, {});
which makes a lot more sense - it turns an array of titles (eg ['foo', 'bar']) and an array of values (eg ['fooVal', 'barVal']), and uses .reduce to transform those into a single object, { foo: 'fooVal', bar: 'barVal' }.
The first argument to the .reduce callback is the accumulator's initial value (the second argument to .reduce), or the value that was returned on the last iteration - the code above passes {} as the initial value, assigns a property to the object, and returns the object on every iteration. .reduce is the usually the most appropriate method to use to turn an array into an object, but if you're more familiar with forEach, the code is equivalent to
const obj = {};
titles.forEach((title, index) => {
obj[title] = values[index];
});
return obj;
While the comma operator can be useful when code-golfing, it's probably not something that should be used when trying to write good, readable code.
const objectFromPairs = arr => arr.reduce((a, v) => ((a[v[0]] = v[1]), a), {});
console.log(objectFromPairs([['a', 1], ['b', 2]])); // {a: 1, b: 2}
I can't wrap my head around this. What does callback (a, v) => ((a[v[0]] = v[1]), a) do - isn't a reducer's callback supposed to be just a function, why is there assignment followed by a comma then accumulator? How do I make sense of this?
When it's (a, v) => a[v[0]] = v[1], why does it return 2? Shouldn't it return {a: 1} on the first iteration, then {b: 2}, so shouldn't we end up with {b: 2} instead of just 2?
I can't wrap my head around this.
Understandable – it uses the relatively obscure (and proscribed!) comma operator. You can expand it to the equivalent statements to make it more readable:
const objectFromPairs = arr => arr.reduce((a, v) => {
a[v[0]] = v[1];
return a;
}, {});
Still kind of an abuse of reduce, though. This is a generic function; it doesn’t have to be golfed.
const objectFromPairs = pairs => {
const object = {};
pairs.forEach(([key, value]) => {
object[key] = value;
});
return object;
};
Starting from the inside and working out:
(a, v) => ((a[v[0]] = v[1]), a)
That's a reduce callback that takes an "accumulator" parameter (a) and a "value" parameter (v). What the body of the function does is employ a comma operator expression statement to get around the need to use curly braces and an explicit return. The first subexpression in the comma operator expression, (a[v[0]] = v[1]), splits up a two-value array into a name for an object property and a value. That is, v[0] becomes a name for a property in the accumulator object, and v[1] is that property's value.
Now that's used in a .reduce() call made on an array, with {} as the initial value for the .reduce() accumulator. Thus, that function builds up properties with values taken from the source array, whose elements clearly need to be arrays themselves, because that's what the callback expects.
It looks like the key to your confusion is that comma operator. An arrow function in JavaScript only has an implied return value when its function body is just a single expression. The comma operator is a way to "cheat" a little bit: you can string together several expressions, with the overall result being the value of the last expression. For an arrow function, that's handy.
I try to compare two strings in array on equal symbols or char,this code works, but how to implement it in ES6 with reduce method, if I have more than two strings an array. I need to return true if the string in the first element of the array contains all of the letters of the string in the second element of the array. But how to create the more flexible function if I have more than 2 elments in the array.
function mutation(arr) {
var arr2 = arr.map(item => item.toLowerCase().split(''));
for (i=0;i<arr2[1].length;i++) {
if (arr2[0].indexOf(arr2[1][i]) < 0)
return false;
}
return true;
}
mutation(["hello", "hey"]);
#Palaniichuk I thought your original algorithm was pretty solid. To handle your request I was able to create a solution that uses reduce.
I do have one question for you. Should the array increase in size, how would the strings be evaluated?
The reason I ask is because using a helper function like this might help you scale this algorithm. Of course it all depends on how the input changes. How the inputs are evaluated.
function makeStr(string) {
const reducer = string.split('').reduce((a, b) => {
a[b] = a[b] + 1 || 1;
return a;
}, {});
return Object.keys(reducer).sort().join('');
}
function secondMutation(arr) {
const array = [...arr].map(makeStr);
return array[0].includes(array[1]);
};
console.log(secondMutation(["hello", "hell"]));