group array of objects by property first letter - javascript

im struggling a little with this, been a while since ive coded javascript ... trying to convert this
items = {
"data": [
{
"name" : "john"
},
{
"name" : "james"
},
{
"name" : "joe"
},
{
"name" : "brian"
},
{
"name" : "bojan"
},
{
"name" : "billy"
},
{
"name" : "dean"
},
{
"name" : "darren"
},
{
"name" : "doug"
}
]
}
into this format
items = {
"data": [
{
letter: "j"
names : ["john", "james", "joe"]
},
{
letter: "b"
names : ["brian", "bojan", "billy"]
},
{
letter: "j"
names : ["dean", "darren", "doug"]
},
]
}
I've been trying to do this using reduce but not having much look.... is there a simpler way to to do it?

You can use reduce to create an object with the letters as keys from which you can extrapolate the array of objects you need by iterating over the object entries using map.
const items = {"data":[{"name":"john"},{"name":"james"},{"name":"joe"},{"name":"brian"},{"name":"bojan"},{"name":"billy"},{"name":"dean"},{"name":"darren"},{"name":"doug"}]};
// `reduce` over the data to produce an object
// with letter keys, and array values where the names are added
const obj = items.data.reduce((acc, c) => {
const letter = c.name[0];
acc[letter] = (acc[letter] || []).concat(c.name);
return acc;
}, {})
// `map` over the object entries to return an array of objects
items.data = Object.entries(obj).map(([letter, names]) => {
return { letter, names }
}).sort((a, b) => a.letter > b.letter);
console.log(items);

Vanilla javascript implementation:
const items = {
"data": [
{
"name" : "john"
},
{
"name" : "james"
},
{
"name" : "joe"
},
{
"name" : "brian"
},
{
"name" : "bojan"
},
{
"name" : "billy"
},
{
"name" : "dean"
},
{
"name" : "darren"
},
{
"name" : "doug"
}
]
}
const transformed = {
data:[]
}
const findByLetter = (letter) => (element) => element.letter === letter;
for(let i = 0; i < items.data.length; i++){
const letter = items.data[i].name.split("")[0];
const elIndex = transformed.data.findIndex(findByLetter(letter));
if(elIndex > -1){
transformed.data[elIndex].names.push(items.data[i].name);
}else{
transformed.data.push({
letter,
names: [items.data[i].name],
});
}
};
console.log(transformed);

Use one reduce():
const items = {"data":[{"name":"john"},{"name":"james"},{"name":"joe"},{"name":"brian"},{"name":"bojan"},{"name":"billy"},{"name":"dean"},{"name":"darren"},{"name":"doug"}]};
let res = items.data.reduce((acc, item) => {
let l = item.name[0];
if (acc.data.filter(e => e.letter == l)[0] === undefined) acc.data.push({'letter': l, names: [] });
acc.data.filter(e => e.letter == l)[0].names.push(item.name);
return acc;
}, {"data": []})
console.log(res)

Related

ES6 reduce array of objects with conditions

I'm stucked in a (in my opinion) complex reduce method.
Given is an array of objects.
const data =
[
{
"key" : "test1",
"value" : 32,
"type" : "OUT"
},
{
"key" : "test1",
"value" : 16,
"type" : "OUT"
},
{
"key" : "test1",
"value" : 8,
"type" : "IN"
},
{
"key" : "test2",
"value" : 32,
"type" : "OUT"
},
{
"key" : "test2",
"value" : 16,
"type" : "IN"
},
{
"key" : "test2",
"value" : 8,
"type" : "OUT"
},
];
I want to get the sum of values of each object grouped by key attribute. There are two type attributes (IN, OUT) where OUT should be interpreted as negative value.
So in the example above, I'm expecting following result object:
//-32 - 16 + 8 = -40
{
"key" : "test1",
"value" : -40,
"type" : "-"
},
//-32 + 16 - 8 = -24
{
"key" : "test2",
"value" : -24,
"type" : "-"
},
I'm grouping the data using the groupBy function of this SO answer.
Now I'm trying to get the sum using reduce with a filter, like in this SO answer.
However, it delivers me the wrong sums (16 and 8) + since I use filter - only one type is considered.
Here is my code:
const data =
[
{
"key" : "test1",
"value" : 32,
"type" : "OUT"
},
{
"key" : "test1",
"value" : 16,
"type" : "OUT"
},
{
"key" : "test1",
"value" : 8,
"type" : "IN"
},
{
"key" : "test2",
"value" : 32,
"type" : "OUT"
},
{
"key" : "test2",
"value" : 16,
"type" : "IN"
},
{
"key" : "test2",
"value" : 8,
"type" : "OUT"
},
];
//group by key
const groupBy = function(xs, key) {
return xs.reduce(function(rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
const grouped = groupBy(data,"key");
for (const [key, value] of Object.entries(grouped))
{
let x = value.filter(({type}) => type === 'OUT')
.reduce((sum, record) => sum + record.value)
console.log(x);
}
//const filtered = grouped.filter(({type}) => type === 'OUT');
console.log(Object.values(grouped));
Question 1:
Why does the reduce give me the wrong sum for type OUT?
Question 2:
Is there a way to consider both types (IN, OUT) without doing the same procedure again?
You can combine the grouping + counting in 1 reduce() if you set the default value to 0, you can always add (or remove) the value from the current key (type)
const data = [{"key" : "test1", "value" : 32, "type" : "OUT"}, {"key" : "test1", "value" : 16, "type" : "OUT"}, {"key" : "test1", "value" : 8, "type" : "IN"}, {"key" : "test2", "value" : 32, "type" : "OUT"}, {"key" : "test2", "value" : 16, "type" : "IN"}, {"key" : "test2", "value" : 8, "type" : "OUT"}, ];
const res = data.reduce((p, c) => {
(p[c['key']] = p[c['key']] || { ...c, value: 0 });
p[c['key']].value =
(c.type === 'IN')
? (p[c['key']].value + c.value)
: (p[c['key']].value - c.value);
return p;
},{});
console.log(res)
Output:
{
"test1": {
"key": "test1",
"value": -40,
"type": "OUT"
},
"test2": {
"key": "test2",
"value": -24,
"type": "OUT"
}
}
I would break this into two problems:
How to reduce each data value (reduce)
How to evaluate existing/new values (switch)
This way your code is less-coupled and it affords you with greater extensibility. Adding a new operator is as simple as adding a new case in the switch.
const reduceValue = (type, existingValue, newValue) => {
switch (type) {
case 'IN' : return existingValue + newValue;
case 'OUT' : return existingValue - newValue;
default : return existingValue; // or throw new Error(`Unsupported type: ${type}`)
}
};
const processValues = (data) =>
data.reduce((acc, { key, type, value }) => {
acc[key] ??= { key, type: '-', value: 0 };
acc[key].value = reduceValue(type, acc[key].value, value);
return acc;
},{});
const testData = [
{ "key" : "test1", "value" : 32, "type" : "OUT" },
{ "key" : "test1", "value" : 16, "type" : "OUT" },
{ "key" : "test1", "value" : 8, "type" : "IN" },
{ "key" : "test2", "value" : 32, "type" : "OUT" },
{ "key" : "test2", "value" : 16, "type" : "IN" },
{ "key" : "test2", "value" : 8, "type" : "OUT" }
];
console.log(processValues(testData))
.as-console-wrapper { top: 0; max-height: 100% !important; }
I would create 2 functions for applying the sign and store them in a variable.
const applySign = { "IN": nr => +nr, "OUT": nr => -nr };
Then do a simple for...of loop (with object destructuring). If there is no running total at the moment for the current key, set the initial value to 0 (using nullish coalescing assignment ??=). Finally add the current value with applied sign to the running total.
const sums = {};
for (const { key, value, type } of data) {
sums[key] ??= 0;
sums[key] += applySign[type](value);
}
const data = [
{ key: "test1", value: 32, type: "OUT" },
{ key: "test1", value: 16, type: "OUT" },
{ key: "test1", value: 8, type: "IN" },
{ key: "test2", value: 32, type: "OUT" },
{ key: "test2", value: 16, type: "IN" },
{ key: "test2", value: 8, type: "OUT" },
];
const applySign = { "IN": nr => +nr, "OUT": nr => -nr };
const sums = {};
for (const { key, value, type } of data) {
sums[key] ??= 0;
sums[key] += applySign[type](value);
}
console.log(sums);
With a few simple tweaks you can change the above in the output you're looking for:
const sums = {};
for (const { key, value, type } of data) {
sums[key] ??= { key, value: 0 };
sums[key].value += applySign[type](value);
}
const expected = Object.values(sums);
This gives you the base answer, though the type properties that you expect are currently missing. To add them you'll have to do another loop and check the final sum result.
for (const sum of expected) {
sum.type = sum.value < 0 ? "-" : "+";
}
const data = [
{ key: "test1", value: 32, type: "OUT" },
{ key: "test1", value: 16, type: "OUT" },
{ key: "test1", value: 8, type: "IN" },
{ key: "test2", value: 32, type: "OUT" },
{ key: "test2", value: 16, type: "IN" },
{ key: "test2", value: 8, type: "OUT" },
];
const applySign = { "IN": nr => +nr, "OUT": nr => -nr };
const sums = {};
for (const { key, value, type } of data) {
sums[key] ??= { key, value: 0 };
sums[key].value += applySign[type](value);
}
const expected = Object.values(sums);
console.log(expected);
// add type based on the value sign (don't know why)
for (const sum of expected) {
sum.type = sum.value < 0 ? "-" : "+";
}
console.log(expected);
If type is a static "-" and was not supposed to depend on the sign of value, then you can add it when you initially create the sum object.
sums[key] ??= { key, value: 0, type: "-" };

Move field names in object containing _ (underscore) to as a nested property after splitting the field name by _

i have an object
{
"name" : "foo",
"tag_name" : "grocery",
"tag_id" : "id",
"tag_nested_id" : "123",
}
I want the output as
{
"name" : "foo",
"tag" : {
"name" : "grocery",
"id" : "id",
"nested" : {
"id" : "123"
}
}
}
Is there any easy way to achieve this using lodash/underscore?
No external libraries required, vanilla JS split on _ and .reduce
const data = {
"name" : "foo",
"tag_name" : "grocery",
"tag_id" : "id",
"tag_nested_id" : "123"
};
const result = Object.entries(data).reduce((object, [key, val]) => {
const splitKey = key.split('_');
const last = splitKey.pop();
splitKey.reduce((obj, x) => obj[x] ??= {}, object)[last] = val;
return object;
}, {})
console.log(JSON.stringify(result, null, 2));
The answer by mani is as good as it gets, but with Underscore, we can create a version that is backwards compatible with older environments:
var data = {
"name" : "foo",
"tag_name" : "grocery",
"tag_id" : "id",
"tag_nested_id" : "123"
};
var result = _.reduce(data, function(object, val, key) {
var splitKey = key.split('_');
var last = splitKey.pop();
_.reduce(splitKey, function(obj, part) {
return obj[part] = obj[part] || {};
}, object)[last] = val;
return object;
}, {})
console.log(JSON.stringify(result, null, 2));
<script src="https://cdn.jsdelivr.net/npm/underscore#1.13.4/underscore-umd-min.js"></script>

i have an array and i need to change an array format using javascript

I have an array object,
const Value = [ {
"NAME" : "XCODE",
"XXXCLASS" : [ {
"V1" : "JOHN",
"V2" : "MAD"
},{
"V1" : "KIRAN",
"V2" : "TOY"
} ]
} ]
I tried it by using forEach method. i dont know, its correct way using javascript.
let arry:any = [];
let objVal:any = {};
Value.forEach((value, index) => {
value.XXXCLASS.forEach( (value, index) =>{
arry.push(value.V1);
});
value.NAME+= ":["+arry+"]";
});
What i mean, dynamically create array with name of "NAME" property with values of "V1" property values. for yours references, kindly check below format. I Need to change this below format,
const Value = {
XCODE: ['JOHN','KIRAN']
};
CODE APPRECIATED.
You can reduce the Value array to an object with NAMEs as the properties and an array of V1 names as property values.
const Value = [ {
"NAME" : "XCODE",
"XXXCLASS" : [ {
"V1" : "JOHN",
"V2" : "MAD"
},{
"V1" : "KIRAN",
"V2" : "TOY"
}]
}]
const result = {}
Value.reduce((obj, value) => {
obj[value.NAME] = value.XXXCLASS.map(xclass => (xclass.V1))
return obj
}, result)
console.log(result)
const values = [ {
"NAME" : "XCODE",
"XXXCLASS" : [ {
"V1" : "JOHN",
"V2" : "MAD"
}],
"YYYCLASS" : [{
"V1" : "KIRAN",
"V2" : "TOY"
} ]
} ]
const result = values.reduce((map, val) => {
let people = map[val.NAME] || [];
Object.keys(val).reduce((array, key) => {
if (key === 'NAME') { return array; }
if (key.includes('CLASS')) {
array.push(val[key][0].V1);
}
return array;
}, people);
map[val.NAME] = people
return map;
}, {});
console.log(result);
Just one more way to do:
const Value = [ {
"NAME" : "XCODE",
"XXXCLASS" : [ {
"V1" : "JOHN",
"V2" : "MAD"
},{
"V1" : "KIRAN",
"V2" : "TOY"
} ]
} ]
var obj = Value[0];
var res = {};
Object.values(obj).map(i=>{
if(typeof i=== "string"){
res[i] = true;
}else{
res['XCODE'] = i.map(a=>a.V1)
}
})
console.log(res)
You could take the other objects out of the items and map new object with the wanted parts.
const
data = [{ NAME: "XCODE", XXXCLASS: [{ V1: "JOHN", V2: "MAD" }, { V1: "KIRAN", V2: "TOY" }] }],
result = data.map(({ NAME, ...o }) => ({ [NAME]: Object
.values(o)
.flatMap(a => a.flatMap(({ V1 }) => V1))
}));
console.log(result);

How to return the given input values not available in array using mongodb query

I am having one input arrays,EX: let UPID = ["0","1","10"]. i have to check members.regularStudent whether given input values available or not ?, suppose not available means i have to push one array and return the results
My documents:
{
"_id" : "5bb20d7556db6915846da67f",
"members" : {
"regularStudent" : [
"3",
"4"
]
}
},
{
"_id" : "5bb20d7556db6915846da55f",
"members" : {
"regularStudent" : [
"1",
"2"
]
}
}
My Expected Output
[
"0",
"10"
]
My Code:
let UPID = ["0","1","10"]
db.Groups.find(
/*{
"members.regularStudent": { $nin: UPIDs }
}*/
)
.forEach(function(objects){
print(objects)
})
I had updated mycode, kindly see top on my question section, print(objects) means i am having the my objects, based on this variable can you update your answer,
** print(objects) **
{
"_id" : "5bb20d7556db6915846da67f",
"members" : {
"regularStudent" : [
"3",
"4"
]
}
},
{
"_id" : "5bb20d7556db6915846da55f",
"members" : {
"regularStudent" : [
"1",
"2"
]
}
}
You could use map method in combination with filter.
let UPID = ["0","1","10"];
let docs = [{ "_id" : "5bb20d7556db6915846da67f", "members" : { "regularStudent" : [ "3", "4" ] } },
{ "_id" : "5bb20d7556db6915846da55f", "members" : { "regularStudent" : [ "1", "2" ] } }]
let ids = [].concat(...docs.map(elem => elem.members.regularStudent));
console.log(UPID.filter(id => !ids.includes(id)));
Here I use forEach to iterate through the data to get all of the regularStudent data into one array then use filter to filter out the data from UPID array.
const UPID = ["0", "1" , "10"]
let data = [
{
"_id" : "5bb20d7556db6915846da67f",
"members" : {
"regularStudent" : [
"3",
"4"
]
}
},
{
"_id" : "5bb20d7556db6915846da55f",
"members" : {
"regularStudent" : [
"1",
"2"
]
}
}
]
let resularStudents = []
data.forEach(d => {
d.members.regularStudent.forEach(rs => {
resularStudents.push(rs)
})
})
var result = UPID.filter(
function(d) {
return this.indexOf(d) < 0;
},
resularStudents
);
console.log(result);

javascript return property value from nested array of objects based on condition

i have an array of objects, in which each object could have an array of objects inside.
var mylist = [
{
"email" : null,
"school" : "schoolA",
"courses": [
{
"name" : 'ABC',
"type" : "chemistry"
},
{
"name" : 'XYZ',
"type": "math"
}
]
},
{
"email" : null,
"school": "schoolB"
}
];
i want to return course name if one of the course type is chemistry.
The course types are unique and even if they are some duplicates, we return the first one.
var result = mylist.some(function (el) {
el.courses && el.courses.some(function(u) {
if (u.type === 'chemistry') {
return u.name;
};
})
});
console.log('outcome:', result);
my code is not working at this stage.
The some callback should return a truthy or falsy value, which tells some whether to keep going (true = stop), and some returns a boolean, not a callback return value.
Probably simplest in this case just to assign directly to result:
var result;
mylist.some(function(el) {
return (el.courses || []).some(function(course) {
if (course.type === "chemistry") {
result = course.name;
return true;
}
return false;
});
});
Live Example:
var mylist = [
{
"email" : null,
"school" : "schoolA",
"courses": [
{
"name" : 'ABC',
"type" : "chemistry"
},
{
"name" : 'XYZ',
"type": "math"
}
]
},
{
"email" : null,
"school": "schoolB"
}
];
var result;
mylist.some(function(el) {
return (el.courses || []).some(function(course) {
if (course.type === "chemistry") {
result = course.name;
return true;
}
return false;
});
});
console.log(result);
I stuck to ES5 syntax since you didn't use any ES2015+ in your question, but in ES2015+, simplest probably to use nested for-of loops:
let result;
outer: for (const el of mylist) {
for (const course of el.courses || []) {
if (course.type === "chemistry") {
result = course.name;
break outer;
}
}
}
Live Example:
const mylist = [
{
"email" : null,
"school" : "schoolA",
"courses": [
{
"name" : 'ABC',
"type" : "chemistry"
},
{
"name" : 'XYZ',
"type": "math"
}
]
},
{
"email" : null,
"school": "schoolB"
}
];
let result;
outer: for (const el of mylist) {
for (const course of el.courses || []) {
if (course.type === "chemistry") {
result = course.name;
break outer;
}
}
}
console.log(result);
You could use reduce() method to iterate through each object in array and then find() method to find if some course matches type.
var mylist = [{"email":null,"school":"schoolA","courses":[{"name":"ABC","type":"chemistry"},{"name":"XYZ","type":"math"}]},{"email":null,"school":"schoolB"}]
const course = mylist.reduce((r, {courses}) => {
if (courses && !r) {
const course = courses.find(({type}) => type == 'chemistry');
if (course) r = course.name;
}
return r;
}, null)
console.log(course)

Categories