Convert string parameter to an array of one element - javascript

I wrote a function which has to support two types of a paramter names for a list of values. Internally it deals with the parameter as an array.
A single name is given as string and multiples names are given as an array of strings.
// simplified example
let doSome = names => names.map(name => name.toUpperCase())
names(['Bart', 'Lisa'])
// [ 'BART', 'LISA' ]
names('Homer')
// TypeError: names.map is not a function
I found a solution using Array.of() in combination with flatten() which needs some babel configuration.
doSome = names => Array.of(names).flatten().map(name => name.toUpperCase());
Is there an idiomatic way in JavaScript to get an array without a type check?

You can use Array.concat(), since concat accepts both arrays and non arrays:
const names = (v) => [].concat(v).map(name => name.toUpperCase())
console.log(names(['Bart', 'Lisa'])) // [ 'BART', 'LISA' ]
console.log(names('Homer')) // ['HOMER']

Short solution:
[names].flat()
If names is an array, it will be left as-is. Anything else will be converted to an array with one element.
This works because .flat() only flattens one level by default.
If names is not an array, [names] makes it an array with one element, and .flat() does nothing more because the array has no child arrays.
If names is an array, [names] makes it an array with one child array, and .flat() brings the child array back up to be a parent array.
Alternative
This is more self-explanitory:
names instanceof Array ? names : [names]
This uses a simple ternary statement to do nothing to it if it is an array already or make it an array if it is not already.

You might not be able to implement it this way if you already have code depending on this function. Still, it would probably be cleaner to allow your function to accept a variable number of arguments with rest parameters.
It means you can call the function as names('Homer') or names('Bart', 'Lisa'):
function names(...args){
return args.map(name => name.toUpperCase());
}
console.log(names('Bart', 'Lisa')); // [ 'BART', 'LISA' ]
console.log(names('Homer')); // ['HOMER']
If you really want to call the function with an array as argument, you can use the spread syntax :
console.log(names(...['Bart', 'Lisa'])); // [ "BART", "LISA" ]
If you use it with a string, you'll get back an array of characters, though:
console.log(names(...'Homer')); // [ "H", "O", "M", "E", "R" ]

Why not just check if the input is an array or not using isArray()?
I made another solution using this approach, also I put a control inside the map() so this don't fail when the name argument is null or undefined.
const names = x => (Array.isArray(x) ? x : [x]).map(name => name && name.toUpperCase());
console.log(JSON.stringify( names(['Bart', 'Lisa']) ));
console.log(JSON.stringify( names('Homer') ));
console.log(JSON.stringify( names('') ));
console.log(JSON.stringify( names(null) ));
console.log(JSON.stringify( names([null]) ));
console.log(JSON.stringify( names([undefined, "Roger", "Bob", null]) ));

Maybe an maybe upcoming method of Array#flat would help in this case (works actually only in Chrome and FF).
const names = unknown => [unknown].flat().map(name => name.toUpperCase())
console.log(names(['Bart', 'Lisa']));
console.log(names('Homer'));

Related

How do you use map and reduce methods to return a key that contains a string? [duplicate]

I have an array of objects called rows, where each object has a string property called name.
I would like to get the longest name. How can I filter rows and grab the row with the longest name, or grab the name itself. Thank you!
Lodash or vanilla JS both work for my purposes. If there are 2 strings tied for the longest length, grabbing either string works for my purposes.
Just use Array.prototype.reduce and keep the longest string.
rows.reduce((longest, current) => longest.name.length > current.name.length ? longest : current);
This should work just fine:
rows.sort((x,y) => y.name.length - x.name.length)[0]
This will mutate the data. You can make a copy of it if you don't want to sort the original rows array. But keep an eye out for not mutating any returned object either as that will mutate the original too. If you don't want that, then you need to do a deep copy.
For example,
const rows = [{name: 'abcde'}, {name: 'abc'}, {name: 'abcd'}];
// spreading rows will essentially make a copy of `rows`
const result = [...rows].sort((x,y) => y.name.length - x.name.length)[0]
console.log(result); // this will print `{name: 'abcde'}`
result.name = 123; // mutating the resulting object after making a copy
console.log(rows); // this will be mutated now
Same with slice() and concat(). They will make a shallow copy too.
const rows = [
{
name: "bar",
},
{
name: "foobar",
},
{
name: "ba",
},
];
const firstItem = rows
.slice()
.sort((a, b) => (a.name.length > b.name.length ? -1 : 1))[0];
console.log(firstItem.name); // Outputs 'foobar'
Note: I'm slicing the array to avoid mutating it in place

How to filter array of arrays of array of objects?

I am trying to filter an array of arrays of objects, but it does not work.
constructNewGrid(filterText){
if(searchText == ""){
this.constructedGrid = this.fullGrid;
return;
}
this.constructedGrid = [];
this.constructedGrid = this.fullGrid.filter(array => array.filter(obj => {
if(obj.name == filterText)
return obj;
}));
console.log(this.constructedGrid);
}
I want to return an array of arrays with the correct object if found one but when I console log it constructedGridis the same array but why?
The code should go to the first array should look up whether there is an object with the name equals to the filter text it should return the array with the corrosponding object and then the next array should be checked and so on.
This, would be the format:
[
[[{name: string}], empty, empty], // array of lenth 3
[empty, empty, [{name: string}], empty], // array of length 4
...
]
If one object is found it should push it in an array separately such that if two objects where found in the same array, it should put them both in separately push them in two separat arrays and those should be in one single array:
Result should be
[
[[obj1]],
[[obj2]],
...
]
It seems in possible for me. I got sometime an error that I am out of memory haha...
You'll need map in addition to filter, because filter just decides whether to keep what's there, it doesn't change what's there. map does. Something like this:
this.constructedGrid = this.fullGrid
// Map the inner array to one that only has matching entries
.map(array => array.filter(obj => obj && obj.name === filterText))
// Remove blank inner arrays from the overall array
.filter(array => array.length);
Live Example:
const fullGrid = [
[[{name: "target"}], , ],
[, null, [{name: "target"}], undefined],
];
const filterText = "target";
const constructedGrid = fullGrid
// Map the inner array to one that only has matching entries
.map(array => array.filter(obj => obj && obj[0] && obj[0].name === filterText))
// Remove blank inner arrays from the overall array
.filter(array => array.length);
console.log(constructedGrid);
.as-console-wrapper {
max-height: 100% !important;
}
Note that you only need that second filter if you want to remove arrays that are completely empty from the outer array. If you want to leave them, just remove that call. Edit: From your reply to my question on the question, it sounds like you want to remove that second .filter.
Note the guard in the first filter, the obj && obj[0] && part. It's there because you said sometimes array entries are "empty." I don't know if you literally meant empty — a sparse array — or entries that are undefined. If you literally meant empty (a sparse array), you don't need the guard, but it's probably best to have it.
As of ES2020, you could use the optional chaining operator instead:
.filter(obj => obj?.[0]?.name === filterText)
obj?.[0]?.name evaluates to undefined if obj is null or undefined, or obj[0] is null or undefined; it evaluates to obj[0].name otherwise. Since undefined === filterText will be false, entries with a null or undefined obj will be left out. Again, though, new feature, you'll need to check support on your target browsers if you're not transpiling.

Find index of object in javascript using its property name

I want to find Index of javascript array of objects using objects property name. My code is :-
const checkbox = [{'mumbai': true},{'bangalore': true},{'chennai': true},{'kolkata': true}];
How can i find index of chennai? Can i acheive using lodash?
You can use .findIndex()
const checkbox = [
{'mumbai': true},
{'bangalore': true},
{'chennai': true},
{'kolkata': true}
];
const finder = (arr, key) => arr.findIndex(o => key in o);
console.log(finder(checkbox, 'chennai'));
console.log(finder(checkbox, 'kolkata'));
console.log(finder(checkbox, 'delhi'));
checkbox.map((v,i) => Object.keys(v).indexOf("chennai") !== -1 ? i : -1).filter(v => v !== -1)[0]
Will give you the index of "chennai", replace it with any other key to get a different index.
What this does is:
Map the array to an array indicating only indices which contain objects with the wanted key
Filter only the indices which you want
Get the first one (you can use the rest as well if there are multiple entries matching your search)
This works in every browser since it only uses .map() , Object.keys() and .filter()

Array.reduce() on an array of objects -- getting back strings of single letters

I'm working to understand Array.reduce() in JavaScript. I have an array of objects that I'm trying to apply .reduce() to, but I'm getting back an array of single letter strings.
Goal:
["Stuff", "necklace", "ring", "bracelet"]
Current Array of Objects
const productArray =
[
{
id: 1,
productTitle: "Necklace"
},
{
id: 2,
productTitle: "Ring"
},
{
id: 3,
productTitle: "Bracelet"
}
]
Function call
const newStuff = productArray.reduce(function(a, currentValue) {
return [...a, ...currentValue.productTitle];
}, ["Stuff"])
Actual result:
What do I need to do to specify that I don't want "productTitle" broken down into single-letter strings? I have been looking for resources regarding .reduce() on an array of objects but I haven't found anything very helpful. Any pointers?
To concatenate an array and value when using spread to create a new array, you spread the previous array to the new array, and add the new item without spreading it.
const productArray = [{"id":1,"productTitle":"Necklace"},{"id":2,"productTitle":"Ring"},{"id":3,"productTitle":"Bracelet"}];
const newStuff = productArray.reduce((a, currentValue) =>
[...a, currentValue.productTitle], []);
console.log(newStuff);
In this case, it's better to use Array.map():
const productArray = [{"id":1,"productTitle":"Necklace"},{"id":2,"productTitle":"Ring"},{"id":3,"productTitle":"Bracelet"}];
const newStuff = productArray.map((currentValue) => currentValue.productTitle);
console.log(newStuff);
Do not spread the title, pass it as it is:
const newStuff = productArray.reduce(function(a, currentValue) {
return [...a, currentValue.productTitle];
}, ["Stuff"]);
...currentValue.productTitle spreads into an array of individual letters, you only want to spread a variable, the aggregate here.
Basically a string is iterable, because the iterator is implemented and returns an array of single characters, if using spread syntax ....
Spread syntax allows an iterable such as an array expression or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.
console.log([...'foo']);
Other answers have pointed out why your code is wrong. But I do want to also note that what you're doing is already covered by Array.prototype.concat:
const productArray = [{"id":1,"productTitle":"Necklace"},{"id":2,"productTitle":"Ring"},{"id":3,"productTitle":"Bracelet"}];
const newStuff = productArray.reduce((a, val) => a.concat(val.productTitle), ['Struff']);
console.log(newStuff);
(And of course, as another answer has mentioned, this sounds more like a use for map than reduce, which might not matter since you're using this to learn reduce.)
The use of spread in this case is unnecessary and inefficient as it creates a new accumulator array from the previous one on every iteration. You can remove spread (and fix your issue) and use concat instead to keep it as a one-liner.
However, since you're just adding one new value on each iteration, you should use push. It requires one more line of code but is likely more efficient than using concat.
var productArray = [{id: 1,productTitle: "Necklace"},
{id: 2,productTitle: "Ring"},
{id: 3,productTitle: "Bracelet"}
];
// Using concat
var newStuff = productArray.reduce((acc, value) =>
acc.concat(value.productTitle),
["Stuff"]);
console.log(newStuff);
// Using push
var newStuff = productArray.reduce((acc, value) => {
acc.push(value.productTitle);
return acc;
}, ["Stuff"]);
console.log(newStuff);

A more elegant way to write this .map call?

I have some code that essentially takes an array of objects and just adds an additional key to each item. I want to be able to express this as tersely as possible as an experiment.
let fruits = [
{"type" : "orange"},
{"type" : "apple"},
{"type" : "banana"}
];
console.log(fruits.map((fruit) => {
fruit.price = "$1.00";
return fruit;
}));
Currently, this works, but it's certainly no one liner and the return statement is still in there, and I feel like there's a way to get rid of it given the fat arrow syntax.
One approach would be to use Object.assign to extend the object and also return the resulting newly-created object:
console.log(fruits.map(fruit => Object.assign(fruit, { price: "1.00" })));
Babel REPL Example
This removes the need for the return keyword, but it's hardly the biggest space-saver. It is also equivalent to what you already have (in that the original fruit object is modified. As joews points out below, if you wanted to leave the original array in-tact you can use an empty target object like so:
Object.assign({}, fruit, { price: "1.00"});
This will ensure that your original array is unmodified (which may or may not be what you want).
Finally, combining this with the spread operator gives us:
console.log(fruits.map(fruit => ({...fruit, price: "1.00" })));
You can also use .forEach() instead of .map() to directly modify fruits if you don't need the original version of fruits
fruits.forEach((fruit) => fruit.price = "$1.00");
http://www.es6fiddle.net/igwdk0gk/
Could do something like this, not recommended for readibility but technically one line.
fruits.map(fruit => (fruit.price = "$1.00") && fruit);
As others have mentioned this method just adds a property to the object and does not copy it. A simple way to keep this as a one liner, use a map and actually create a copy would be:
fruits.map(fruit => Object.assign({price: "$1.00"}, fruit));
Object.assign() will assign all the properties of fruit to the object { price: "$1.00" } and return it.
Live example:
"use strict";
let log = function() {
output.textContent += [].join.call(arguments, ' ') + '\n\n';
};
log('# MAP (OR FOREACH) WITHOUT ASSIGN');
let fruits = [
{"type" : "orange"},
{"type" : "apple"},
{"type" : "banana"}
];
let newfruits = fruits.map(fruit => (fruit.price = "$1.00") && fruit);
log('fruits', JSON.stringify(fruits));
log('newfruits', JSON.stringify(newfruits));
log('^-- Both are modified since newfruits its a new array with the same objects');
log('# MAP WITH ASSIGN');
fruits = [
{"type" : "orange"},
{"type" : "apple"},
{"type" : "banana"}
];
newfruits = fruits.map(fruit => Object.assign({price: "$1.00"}, fruit));
log('fruits', JSON.stringify(fruits));
log('newfruits', JSON.stringify(newfruits));
log('^-- Only newfruits is modified since its a new array with new objects');
pre {
word-wrap: break-word;
}
<pre id="output"></pre>
There are many ways to do this:
Side effects with the comma operator
If you want to do it inline you can use the comma operator (though it's a little obscure):
fruits.map((fruit) => (fruit.price = "$1.00", fruit))
We could also use && since assignment returns the assigned value and "$1.00" is truthy but the comma operator is more general, since we can also set false or 0 and have everything continue to work.
Higher-ordered functions
It's probably better to make a helper function, however:
// We're currying manually here, but you could also make the signature
// setter(name, value) and use your function of choice to curry when you need to.
function setter(name) {
return (value) => (obj) => {
obj[name] = value;
return obj;
}
}
Then you can use:
fruits.map(setter("price")("$1.00"))
Embrace mutability
As #Suppen points out in their comment, because normal JavaScript objects are mutable you can also avoid the map and use forEach instead:
fruits.forEach(fruit => fruit.price = "$1.00");
// Each element in fruits has been modified in-place.
A mapping function should almost always be pure. If you are only going to modify the objects, a simple loop will do better (for (let fruit of fruits) fruit.price = …; console.log(fruits);).
So when you're returning a new object, a one-liner will be easy:
console.log(fruits.map(({type}) => ({type, price:"$1.00"})));
If you've got many properties, or properties you don't know, then Object.assign({}, …) is your friend (as in #joews' comment to #RGraham's answer).

Categories

Resources