I have an array of products which in turn have an array of categories. I want to extract distinct values of the type property on category object.
Both the Lodash and native versions below do the job.
I want to make a generic function which takes the path of property and return unique values.
Essentially I am looking at something terse like
map(products, property("categories[].type") but heres the longer version(s)
import { compact, flatten, map, property, uniq } from "lodash";
export const getAllTypes1 = (products) => {
return uniq(
compact(map(flatten(map(products, property("categories"))), "type"))
);
};
export const getAllTypes2 = (products) => {
const types = [];
products.forEach((product) => {
product.categories.forEach((category) => {
if (!types.some((t) => t === category.type)) {
types.push(category.type);
}
});
});
return types;
};
Example data
const product1 = {
name: 'Wilson Orange',
price: 72.50,
categories: [{
type: 'flash sale',
discountable: false,
},{
type: 'tennis',
discountable: true,
}]
};
const product2 = {
name: 'Babolat Green',
price: 65.50,
categories: [{
type: 'tennis',
discountable: true,
}]
};
const products = [product1, product2];
Result
const result = getAllTypes2(products);
console.log(result); // ["flash sale", "tennis"]
Here's a working example
Here's a vanilla JS function that takes the path without needing [] and automatically checks arrays wherever it finds one.
How it works is:
Create an empty Set to easily remove duplicates
Turn the path string to an array of properties -> props
Call a recursive function recurse(currObj, props) which:
Checks if the currObj is an array, and if it is:
a. Recurses again with the array values as currObj
b. Use the same props since we didn't check an object in the path
Check if we're at the last prop in the path, if yes
a. Add the property's value in the current object to the set
Otherwise. recurse with currObj[currProp], and the remaining props
Convert the set to an array and return it.
const product1 = {
name: 'Wilson Orange',
price: 72.5,
categories: [
{
type: 'flash sale',
discountable: false,
},
{
type: 'tennis',
discountable: true,
},
],
};
const product2 = {
name: 'Babolat Green',
price: 65.5,
categories: [
{
type: 'tennis',
discountable: true,
},
],
};
const products = [product1, product2];
function getProperties(array, path) {
const props = path.split('.');
const values = new Set();
function recurse(currObj, props) {
const currProp = props[0]
const nextProps = props.slice(1);
if (Array.isArray(currObj)) {
for (let val of currObj) {
recurse(val, props);
}
return
}
if (nextProps.length === 0) {
values.add(currObj[currProp])
} else {
recurse(currObj[currProp], nextProps)
}
}
recurse(array, props);
return [...values];
}
console.log(getProperties(products,'categories.type'))
console.log(getProperties(products,'price'))
console.log(getProperties(products,'name'))
It's not a property path string, but it's pretty terse and expressive:
const pipe = (...fs) => fs.reduceRight(
(next, f) => x => f(x, next), x => x,
);
const getAllTypes = pipe(
(x, next) => [...new Set(x.flatMap(next))],
(x, next) => x.categories.map(next),
(x) => x.type,
);
// equivalent to
// const getAllTypes =
// x => [...new Set(x.flatMap(y => y.categories.map(z => z.type)))];
const products = [{
name: 'Wilson Orange',
price: 72.50,
categories: [{
type: 'flash sale',
discountable: false,
}, {
type: 'tennis',
discountable: true,
}]
}, {
name: 'Babolat Green',
price: 65.50,
categories: [{
type: 'tennis',
discountable: true,
}]
}];
console.log(getAllTypes(products));
The parameter x => x allows pipe to be called without any arguments and return the identity function. It also allows the last function argument of pipe to accept a next parameter for consistency, i.e. (x, next) => next(x.type) would have been equivalent to (x) => x.type.
Reference:
Array.prototype.flatMap
Array.prototype.map
Array.prototype.reduceRight
Set
Related
Hi all I have the following code
the data that I want to transform.
const obj = {
numbers: {
label: "main numbers",
pageTitle: "Numbers",
key: "1",
items: {
firstNumber: {
label: "first number",
pageTitle: "first",
key: "first"
},
secondNumber: {
label: "second number",
pageTitle: "second",
key: "second"
}
}
},
letters: {
label: "main Letters",
pageTitle: "Letters",
key: "2",
items: {
firstLetter: {
label: "first Letter",
pageTitle: "first",
key: "first"
}
}
},
signs: {
label: "main sign",
pageTitle: "Sign",
key: "3"
}
};
In my obj variable I have 3 other objects
numbers object which has items property which includes 2 other objects.
letters object which has items property which includes only one object.
signs object.
I need to transform my obj to the following way.
[
{
label:"main numbers",
pageTitle:"Numbers",
key:1,
children: [{label,pageTitle,key},{label,pageTitle,key}]
},
{
label:"main Letters",
pageTitle:"Letters",
key:1,
children: [{label,pageTitle,key}]
},
{
label:"main sign",
pageTitle:"Sign",
key:1,
children: []
},
]
for that transformation, I wrote the following code.
const transformedData = Object.values(obj).map((menuitem) => menuitem);
const data = [];
transformedData?.map((x) => {
const newData = {};
newData.label = x.label;
newData.pageTitle = x.pageTitle;
newData.key = x.key;
newData.children = x?.Object?.values(items)?.map((el) => {
newData.children.label = el.label;
newData.children.pageTitle = el.pageTitle;
newData.children.key = el.key;
});
data.push(newData);
});
Everything was working, but for children instead of printing an array it prints undefined.
Please help me to resolve this issue.
I created a function for your case.
const convert = data =>
Object.values(data)?.map(x => ({
label: x.label,
pageTitle :x.pageTitle ,
key: x.pathname,
children: x.items
? Object.values(x.items || {}).map(el => ({ label: el.label,
key:el.pathname,pageTitle:el.pageTitle }))
: null,
}));
You can use like const items = convert(obj).
xdoesn't have Objects. Change it to:
newData.children = Object.values(x.items)?.map(/*...*/);
Is this what you're after?
const transformedData = Object.values(obj).map((menuitem) => menuitem);
const data = [];
transformedData?.map((x) => {
const newData = {};
newData.label = x.label;
newData.pageTitle = x.pageTitle;
newData.key = x.key;
if(x.hasOwnProperty('items')){
newData.children = Object.values(x.items).map((el) => {
const obj={
label:el.label,
pageTitle:el.pageTitle,
key:el.key
}
return obj
})};
data.push(newData);
});
console.log(data)
Your code return undefined because inside map you didn't return anything so newData.children was never populated with anything.
Also, I think accessing and assigning newData.children.label was problematic since there was no newData.children yet. So we declare a temp obj inside map and we return it
Lastly we need to check if items is a property that exists in the first place.
My brain froze with this advanced filtering. This task has exceeded my basic knowledge of filter, map etc.
Here I have an array with nested objects with array:
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
You may have seen this sort of style if you've worked with React Native (RN). This question is not for RN. I need to perform a filter on the name property in the nested array and when I get a match, I must return the format as the DATA variable.
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
console.log(results);
};
My limited knowledge of deep filtering returns the basic filtering for the data array but need to retain the structure for DATA. The expected results I'd expect:
// I'm now querying for "ZAMASU"
const handleFiltering = (value='ZAMA') => {
const _value = value.toLowerCase();
const results = DATA.map(o => {
return o.data.filter(o => o.name.toLowerCase().indexOf(_value) != -1)
});
// console.log(results) should now be
// [
// {
// title: 'Dragon Balls Z',
// data: [
// { id: 2, name: 'Zamasu' }
// ]
// }
// ];
};
What comes to mind is the use of {...DATA, something-here } but my brain has frozen as I need to get back the title property. How to achieve this, please?
Another solution would be first use filter to find only objects containing the name in data passed through the argument, subsequently mapping data.
Here is your adjusted filter method
const handleFiltering = (value) => {
const _value = value.toLowerCase();
const results = DATA.filter((obj) =>
obj.data.some((character) => character.name.toLowerCase() === _value)
).map((obj) => ({
title: obj.title,
data: obj.data.filter(
(character) => character.name.toLowerCase() === _value
),
}));
console.log(results);
};
You can use reduce method of array. First find out the object inside data array and then add that to accumulator array as new entry by preserving the original structure.
const DATA = [
{
title: 'Spongebob',
data: [
{ id: 1, name: 'Mr Crabs', where: 'tv' },
{ id: 2, name: 'Sandy' }
]
},
{
title: 'Dragon Balls Z',
data: [
{ id: 1, name: 'GoKu' },
{ id: 2, name: 'Zamasu' }
]
}
];
let handleFiltering = (value='tv') => {
return DATA.reduce((acc,d) => {
let obj = d.data.find(a => a.name?.toLowerCase().includes(value.toLowerCase())
|| a.where?.toLowerCase().includes(value.toLowerCase()));
obj ? acc.push({...d, data:[obj]}) : null;
return acc;
}, []);
}
let result = handleFiltering();
console.log(result);
I have two arrays as listed below. I'm trying to create a new array of objects by using the field key in array_1 and the values in array_2.
const result = []
array_1 = [{ name: "Color" , field: "color"}, {name: "Shape", field: "shape" }, { name: "Whatever", field: "whatever" }]
array_2 = [["green", "rectangular", "whatever1"], ["yellow", "circle", "whatever2"]]
The result should be:
console.log(result)
// [{color:"green", shape:"rectangular", whatever: "whatever1"},
// { color:"yellow", shape: "circle", whatever:"whatever2"}]
I did this at my final trial:
const rowObj = {}
const result = array.map((subarray) => subarray.map((cell, index) => {
console.log(cell,index)
rowObj[columns[index].field] = cell
return rowObj
}))
Basically, I was overwriting the same object.
Thanks,
One way to do it is to map() over the array_2 and in each iteration:
Create a new object
Iterate over the array_1 to fill the newly created object. You can use the index parameter of the forEach() method's callback function to get the field property from the objects inside array_1.
and then return that object from the callback function of the map() method.
const array_1 = [
{ name: 'Color', field: 'color' },
{ name: 'Shape', field: 'shape' },
{ name: 'Whatever', field: 'whatever' },
];
const array_2 = [
['green', 'rectangular', 'whatever1'],
['yellow', 'circle', 'whatever2'],
];
const result = array_2.map(arr => {
const o = {};
arr.forEach((str, idx) => {
o[array_1[idx].field] = str;
});
return o;
});
console.log(result);
You can use array.map to iterate both arrays and take advantage of Object.fromEntries to build new objects based on the order of array elements:
array_1 = [{ name: "Color" , field: "color"}, {name: "Shape", field: "shape" }, { name: "Whatever", field: "whatever" }]
array_2 = [["green", "rectangular", "whatever1"], ["yellow", "circle", "whatever2"]]
let result = array_2.map(
x => Object.fromEntries(
array_1.map((y,i) => ([y.field, x[i]]))))
console.log(result);
Explanation
You could create a function that creates a constructor based on the descriptions of your object's fields like this:
function createConstructor(fieldsDescriptor) {
return function(fields) {
fieldsDescriptor.forEach((descriptor, index) => {
this[descriptor.field] = fields[index]
})
}
}
Then you could, for example, make a sampleConstructor that creates objects based on the field names of array_1:
const SampleConstructor = createConstructor(array_1)
And then, for each entry in array_2 you could apply your SampleConstructor:
const result = array_2.map(fields => new SampleConstructor(fields))
Motivation
Creating a dedicated constructor adds some clear semantics to your app, shows readers what you are doing and also stores constructor information in the created objects at runtime.
When you later want to know which constructor made which objects you can just call object.constructor and use this information to determine what kind of objects they are.
For example calling result[0].constructor == SampleConstructor will be true because SampleConstructor is the constructor that created the first result.
Demo
Here is a full demo
const array_1 = [{ name: "Color" , field: "color"}, {name: "Shape", field: "shape" }, { name: "Whatever", field: "whatever" }]
const array_2 = [["green", "rectangular", "whatever1"], ["yellow", "circle", "whatever2"]]
function createConstructor(fieldsDescriptor) {
return function(fields) {
fieldsDescriptor.forEach((descriptor, index) => {
this[descriptor.field] = fields[index]
})
}
}
const SampleConstructor = createConstructor(array_1)
const results = array_2.map(fields => new SampleConstructor(fields))
console.log(results)
const EmptyConstructor = createConstructor([])
console.log(results[0].constructor == SampleConstructor)
console.log(results[0].constructor == EmptyConstructor)
You can try this
array_1 = [
{ name: 'Color', field: 'color' },
{ name: 'Shape', field: 'shape' },
{ name: 'Whatever', field: 'whatever' }
];
array_2 = [
['green', 'rectangular', 'whatever1'],
['yellow', 'circle', 'whatever2']
];
const keys = array_1.map(item => item.field);
const output = [];
array_2.forEach(item => {
const temp = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = item[i];
temp[key] = value;
}
output.push(temp);
});
console.log(output);
I have two arrays that have properties: minValue, maxValue, avgValue. I would like to write a very clean approach to identifying which objects match (the displayName) in the array and then assign the values of the new array to the old (minPrice, maxPrice, avgPrice).
I have this code so far
export interface Player {
displayName: string;
MinPrice: number;
MaxPrice: number;
AvgPrice: number;}
export const MERGE_PLAYER_STATS = (playerStat: Player[], auctionStat: Player[]): any => {
const reducer = (playerStat, auctionStat) => {
playerStat.MinPrice = auctionStat.minPrice,
playerStat.MaxPrice = auctionStat.maxPrice,
playerStat.AvgPrice = auctionStat.avgPrice,
playerStat.Team = auctionStat.team;
}
return reducer(playerStat, auctionStat =>
filter(auctionStat, playerStat => auctionStat.displayName.includes(playerStat.displayName)));
}
Input: two different set of player arrays that have common display Names.
playerStat: [] = [];
auctionStat: [] = [];
playerStat.push( {
displayName: "Josh Allen",
minPrice: "",
maxPrice: "",
avgPrice: ""
},
{
displayName: "No One",
minPrice: "",
maxPrice: "",
avgPrice: ""
});
auctionStat.push( {
displayName: "Josh Allen",
minPrice: 1,
maxPrice: 2,
avgPrice: 1
},
{
displayName: "No One 2",
minPrice: 1,
maxPrice: 1,
avgPrice: 2
});
The output should only have Josh Allen stats being updated from blank values to 1,2,1 respectively.
Please let me know what your clean approach this. FYI, this code is not returning what I want it to.
The simple approach would be to simply write a mapping function that copies in new objects based on matching keys.
Given an interface:
interface MyInterface {
key: string;
value: number;
}
And two arrays:
const existingArray: MyInterface[] = [
{ key: 'a', value: 1 },
{ key: 'b', value: 2 },
{ key: 'c', value: 3 }
];
const newArray: MyInterface[] = [
{ key: 'b', value: 20 },
{ key: 'c', value: 30 },
{ key: 'd', value: 40 }
];
Expected output
I want to update the existingArray with the values from newArray where the keys match.
If a value isn't found in newArray, the existing item is untouched.
If a new value is found in newArray, it isn't merged in to oldArray
All property values are to be overwritten
My expected output is:
const expected = [
{ key: 'a', value: 1 },
{ key: 'b', value: 20 },
{ key: 'c', value: 30 }
];
Solution
I would achieve this by using this merge function. It is just a javascript array map:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string): T[] {
// start with the existing array items
return existing.map(item => {
// get the key using the callback
const key: string = getKey(item);
// find the matching item by key in the updated array
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
// if a matching item exists, copy the property values to the existing item
Object.assign(item, matching);
}
return item;
});
}
I have made it generic to allow you to use it with any type. All you need to do it provide the 2 arrays, and a callback to identify the key.
I would use it to created a merged array like this:
const merged = this.merge(existingArray, newArray, x => x.key);
DEMO: https://stackblitz.com/edit/angular-nn8hit
A word of warning
This wouldn't scale well with very large arrays. If you are using large arrays, I would optimise by creating indexes of type Map<string, T>, to keep the number of loops fixed. This implementation is out of scope of this question.
Edit:
If you only want to update specific values, you could pass in an update callback:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string,
update: (item, matching) => void
): T[] {
return existing.map(item => {
const key: string = getKey(item);
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
update(item, matching);
}
return item;
});
}
And call it like this:
const merged = this.merge(existingArray, newArray, x => x.key,
(item, matching) => item.value = matching.value);
Use Object.assign()
Ex: oldArray = Object.assign(oldArray, newArray)
playerStat.forEach((initItem) => {
let result = auctionStat.filter((item) => (
initItem.displayName === item.displayName
))
// let me suppose the same displayName only has one
result.length && Object.assign(initItem, result[0])
})
I have a plain JavaScript array of objects, say e.g.
const drawings = [
{
name: "Foo",
category: "widget"
},
{
name: "Bar",
category: "widget"
},
{
name: "Bar",
category: "fidget"
},
]
etc, where both the name and category have duplicates. What I want to end up with is essentially a list of objects (this is to meet the interface for a 3rd party library), where each object represents a name, and then for each category there is a property that is either true or false, depending on the original list. So for the example the output would be:
const output = [
{
name: "Foo",
widget: true,
fidget: false
},
{
{
name: "Bar",
widget: true,
fidget: true
},
]
I would first go through and make an object of your categories with the categories as keys and default values as false.
Then you can assign this to each object and set the correct keys to true as you go through.
const drawings = [{name: "Foo",category: "widget"},{name: "Bar",category: "widget"},{name: "Bar",category: "fidget"},]
// make category object where everything is false
let category_obj = drawings.reduce((a, item) => (a[item.category] = false, a), {})
let output = drawings.reduce((a, {name, category}) => {
// assign cat
if (!a.hasOwnProperty(name)) a[name] = Object.assign({}, {name}, category_obj)
// set to true if the correct category
a[name][category] = true
return a
}, {})
// the above makes an object, but you only want the array of values
console.log(Object.values(output))
If you already know the categories or if you have infered them as you suggested, you could use Array.reduce() like such:
drawings.reduce(function(acc, curr) {
if (!acc.some(elt => elt.name === curr.name)) {
acc.push({name: curr.name, widget: false, fidget: false})
}
const i = acc.findIndex(elt => elt.name === curr.name)
acc[i][curr.category] = true
return acc
}, [])