Javascript cleanest way to distinct array based on object property - javascript

So I have an array of statuses and I want to get distinct values based on an element property ProductOrderStatusTypeID. I have one implementation but I want the cleanest way of doing that.
My list:
[{
StatusID : 1,
Name : "Open",
ProductOrderStatusType : 'Initial'
ProductOrderStatusTypeID : 1
},{
StatusID : 3,
Name : "Closed",
ProductOrderStatusType : 'Final'
ProductOrderStatusTypeID : 2
},
{
StatusID : 3,
Name : "Pending",
ProductOrderStatusType : 'Initial'
ProductOrderStatusTypeID : 1
}]
Expected output:
[{
ProductOrderStatusType : 'Initial',
ProductOrderStatusTypeID : 1
},{
ProductOrderStatusType : 'Final',
ProductOrderStatusTypeID : 2
}]
My code:
function getDistinctProductOrderStatusType(statuses) {
return statuses.reduce(function (result, status) {
var existingStatus = result.filter(function (res) {
return res.ProductOrderStatusTypeID === status.ProductOrderStatusTypeID;
})[0];
if (!existingStatus) {
result.push({
ProductOrderStatusTypeID: status.ProductOrderStatusTypeID,
ProductOrderStatusType: status.ProductOrderStatusType
});
}
return result;
}, []);
}

const stat = [{
StatusID: 1,
Name: "Open",
type: 'Initial', // !!! I renamed properties for simplicity sake
typeID: 1
}, {
StatusID: 3,
Name: "Closed",
type: 'Final',
typeID: 2
}, {
StatusID: 3,
Name: "Pending",
type: 'Initial',
typeID: 1
}
];
const distinctStat = arr => arr.reduce((a, {type, typeID}) =>
a.some(ob => ob.typeID === typeID) ? a : a.concat({type, typeID})
, []);
console.log( distinctStat(stat) );
In case you find it more easier to name the properties you're not interested in collecting (like i.y.e: StatusID and Name) than you could go for the rest element:
const distinctStat = arr => arr.reduce((a, {StatusID, Name, ...r}) =>
a.some(ob => ob.typeID === r.typeID) ? a : a.concat(r)
, []);
(same output as above)
Info:
Although we could, in the above code Array.prototype.some()
is used instead of Array.prototype.find()
because we don't need to return the entire object but just a boolean (as quickly as possible).

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: "-" };

Get the parents from a json where string in javascript

I have this:
{ 'Payment' : {
'Referenced' : 'referenced payment',
'Conciliate' : 'conciliate payment',
'Multiple' : 'multiple payment'
}
}
but can change in all moment for random nodes or add more, like:
{ 'Payment' : {
'Referenced' : 'referenced payment',
'Conciliate' : 'conciliate payment',
'Multiple' : {
'mult1' : 'example1',
'mult2' : 'example1'
},
'Inventory' : {
'datastorage' : 'dt1'
}
}
All nodes can be asigned randomly, and I need to search by value, I can pass:
referenced payment
and need:
Payment/Referenced
or I send:
example1
and I need:
Payment/Multiple/mult1
I don't know if exist something like that.
// Function
const findPath = (obj, query) => {
const makeArray = (obj, path = []) => {
const pairs = Object.entries(obj);
return pairs.map(([key, value]) =>
typeof value === "object"
? makeArray(value, [...path, key])
: { path: [...path, key], value }
);
};
return (
makeArray(obj)
.flat(Infinity)
.find(({ path, value }) => value === query)?.path.join("/") ?? null
);
};
// Usage
const path1 = findPath(
{
Payment: {
Referenced: "referenced payment",
Conciliate: "conciliate payment",
Multiple: {
mult1: "example1",
mult2: {
test: 123,
},
},
Inventory: {
datastorage: "dt1",
},
},
},
123
);
const path2 = findPath(
{
Payment: {
Referenced: "referenced payment",
Conciliate: "conciliate payment",
Multiple: {
mult1: "example1",
},
Inventory: {
datastorage: "dt1",
},
},
},
"referenced payment"
);
console.log("123: " + path1);
console.log("referenced payment: " + path2);
Explanation
The first step is converting the object into a linear array of the object tree and its paths. This is done recursively. Then, the array is flattened in order to be iterated through with Array.prototype.find, if the value matches the query, the path is returned, if no match was found it returns null.
Credits
Thanks to #Bravo for suggesting path array instead of template literal

Extract from array of objects some properties

How can I extract from an array of objects some properties like without for loop, with map or filter?
Example:
obj = [
{ 'cars' : 15, 'boats' : 1, 'smt' : 0 },
{ 'cars' : 25, 'boats' : 11, 'smt' : 0 }
]
extractFunction(obj, ['cars' , 'boats']) -> { 'cars' : [15,25], 'boats' : [1,11]}
You can do it with reduce:
* As you can see, the benefit of this approach (according to the other answers) is that you loop the keys only one time.
const extractFunction = (items, keys) => {
return items.reduce((a, value) => {
keys.forEach(key => {
// Before pushing items to the key, make sure the key exist
if (! a[key]) a[key] = []
a[key].push(value[key])
})
return a
}, {} )
}
obj = [
{ 'cars' : 15, 'boats' : 1, 'smt' : 0 },
{ 'cars' : 25, 'boats' : 11, 'smt' : 0 }
]
console.log(extractFunction(obj, ['cars', 'boats']))
You could take a dynamic approach by using the keys for mapping the values.
function extractFunction(array, keys) {
return array.reduce(
(r, o) => (keys.forEach(k => r[k].push(o[k])), r),
Object.assign(...keys.map(k => ({ [k]: [] })))
);
}
console.log(extractFunction([{ cars: 15, boats: 1, smt: 0 }, { cars: 25, boats: 11, smt: 0 }], ['cars', 'boats']));

Does JQuery have a sort_by function similar to Ruby's

So given a list of items like so:
item_1 = {id:1, categories: {"category_A" => 1, "category_B" => {"sub_category_A" => 3, "sub_category_B" => 1}}}
item_2 = {id:2, categories: {"category_B" => {"sub_category_A" => 1, "sub_category_B" => 2}}}
Where the numeric value is that items order in a given sub or main category. Now, given a sub or main category, I want to sort the items by the order number. In Ruby I'd write...
# Given category_B and sub_category_A
items.sort_by { |i| i.categories["category_B"]["sub_category_A"] }
# which would return...
[item_2, item_1]
Also want to add, the key is if an item does NOT have the relevant passed category_B and sub_category_A, it should be excluded entirely from output.
You don't need jQuery; JavaScript arrays have a filter() function you can use to limit yourself to valid items and a sort() function that can take a comparing function as its argument:
var item_1 = {
id:1,
categories: {
"category_A" : 1,
"category_B" : {
"sub_category_A" : 3,
"sub_category_B" : 1
}
}
};
var item_2 = {
id:2,
categories: {
"category_B" : {
"sub_category_A" : 1,
"sub_category_B" : 2
}
}
};
var item_3 = {
id: 3,
categories : {
"category_A" : 2
}
};
[item_1,item_2,item_3].filter(function(entry) {
return entry.categories.category_B;}).sort(function(left, right) {
return left.categories["category_B"]["sub_category_A"] -
right.categories["category_B"]["sub_category_A"]
});
// or in the more readable ES6 style
[item_1,item_2,item_3]
.filter((entry) => entry.categories.category_B)
.sort((left, right) => left.categories["category_B"]["sub_category_A"] - right.categories["category_B"]["sub_category_A"]
);
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
You can use Array#Sort in JavaScript.
a.categories["category_B"]["sub_category_A"] - b.categories["category_B"]["sub_category_A"]
Is the comparison of 2 elements in Array.
UPDATE: to excluded from the output. You can use Array#filter it before sorting
Let's see my example
var arr = [{
id:1,
categories:
{
"category_A" : 1,
"category_B" : {
"sub_category_A" : 3,
"sub_category_B" : 1
}
}
},{
id:3,
categories: {
"category_C" : {
"sub_category_A" : 1,
"sub_category_B" : 2
}
}
},{
id:2,
categories: {
"category_B" : {
"sub_category_A" : 1,
"sub_category_B" : 2
}
}
}];
var result = arr.filter(a => a.categories["category_B"])
.sort((a, b) => {
return a.categories["category_B"]["sub_category_A"] - b.categories["category_B"]["sub_category_A"];
})
console.log(result);
Neither ECMAScript nor jQuery have sortBy, but LoDash does and so does Underscore.
It's also not hard to supply your own:
Array.prototype.sortBy = function sortBy(keyFn) {
const compare = (a, b) => a > b ? 1 : (a < b ? -1 : 0);
return this.
map(el => [keyFn(el), el]).
sort(([a, ], [b, ]) => compare(a, b)).
map(([, el]) => el);
};
const item1 = { id: 1, categories: { category_A: 1, category_B: { sub_category_A: 3, sub_category_B: 1 }}};
const item2 = { id: 2, categories: { category_B: { sub_category_A: 1, sub_category_B: 2 }}};
const sorted = [item1, item2].sortBy(el => el.categories.category_B.sub_category_A);
console.log(sorted);

JavaScript - Insert & Remove a element in object

I have a object.
var dl_items;
After a loop for inputing data:
dl_items[code] = itemObject;
I have a array:
dl_items : {
"code_A" : { "index" : 1, "status" : 2, "name" : A_data},
"code_B" : { "index" : 2, "status" : 0, "name" : B_data},
"code_C" : { "index" : 3, "status" : 1, "name" : C_data},
"code_D" : { "index" : 4, "status" : 2, "name" : D_data},
"code_E" : { "index" : 5, "status" : 4, "name" : E_data}
}
Now I want to remove "dl_items[code_D]" and insert it into after "code_A" (index 2) for result like :
dl_items : {
"code_A" : { "index" : 1, "status" : 2, "name" : A_data},
"code_D" : { "index" : 4, "status" : 2, "name" : D_data},
"code_B" : { "index" : 2, "status" : 0, "name" : B_data},
"code_C" : { "index" : 3, "status" : 1, "name" : C_data},
"code_E" : { "index" : 5, "status" : 4, "name" : E_data}
}
I try to use "delete" after using a loop to find index of code_D:
delete dl_items[code_D];
and it successful removed but how can i insert code_D into his new index ?
Edit : Thanks all everyone to help me understand more about array.
Since object doesn't have an order, you need to convert your current implementation into array:
var dl_items = [];
When you need to add an item to the array:
dl_items.push({ code: code, item: itemObject });
Now, the similar data as array from your question is:
dl_items: [
{ code :"code_A", item: { index: 1, status: 2, name: "A_data" } },
{ code :"code_B", item: { index: 2, status: 0, name: "B_data" } },
{ code :"code_C", item: { index: 3, status: 1, name: "C_data" } },
{ code :"code_D", item: { index: 4, status: 2, name: "D_data" } },
{ code :"code_E", item: { index: 5, status: 3, name: "E_data" } },
]
In order to move the entry with code_D after the entry with code_A, use the following:
var codeDEntry = dl_items[3];
dl_items = dl_items
.filter(function(entry) {
return entry !== codeDEntry;
})
.splice(1, 0, codeDEntry);
Hope this helps!
You can make a temp var like this :
tempItem = dl_items.code_D;
dl_items.code_D = dl_items.code_B;
dl_items.code_B = tempItem;
What you have here is an object, not an array. Therefore, there is no concept of an index here.
You can map your object keys into an array as follows:
let array = Object.keys(dl_items);
You can then reorder their positions.

Categories