Increment value of map based on array lookup index - javascript

I have an array:
const arr1 = [
"Strongly positive",
"Positive",
"Neutral",
"Negative",
"Strongly negative"
];
and responses object:
const responses = [
{ values: { QID16: 3 } },
{ values: { QID16: ["1", "2", "3"] } },
{ values: { QID16: 1 } }
];
The goal is to create a function that returns a map.
The map's keys are the elements of arr1 and the values are the count of those elements if their index + 1 appears in the responses object.
For example, responses[0].values.QID16 is 3 which is Neutral. (Neutral has index 2)
The problem is when the values in responses is an array like in responses[1]
I created the following function:
function getCounts(mainId, choices, responses) {
let choicesCounts = new Map();
choices.forEach((choice, i) => {
choicesCounts.set(choice, 0);
const id = i + 1;
responses.forEach((response) => {
if (response.values[mainId] && response.values[mainId] === id) {
choicesCounts.set(choice, choicesCounts.get(choice) + 1);
}
if (
response.values[mainId] &&
Array.isArray(response.values[mainId]) &&
response.values[mainId].includes(id.toString())
) {
// this is the part where I need help
response.values[mainId].forEach((n) => {
choicesCounts.set(
choices.at(parseInt(n, 10) - 1),
choicesCounts.get(choices.at(parseInt(n, 10) - 1)) + 1
);
});
}
});
});
return choicesCounts;
}
It would be called like this:
console.log(getCounts("QID16", arr1, responses));
Desired output:
// desired output is a map not an object
const desiredOutput = {
"Strongly positive": 2, // number 1 appears twice in responses
Positive: 1, // number 2 appers once in responses
Neutral: 2,
Negative: 0,
"Strongly negative": 0
};
It works in the case where the values are numbers but not when they're arrays.
What is wrong with this function? Any suggestions to make it simpler?

I think your problem lies in the way how you approached to solve this problem.
Currently you are iterating over your responses array once for each possible value from arr1.
When you encounter the array of values within a responses entry, you check whether it contains a value you are currently "looking for", and then increment ALL choices that were made. Since this will get looped over several times, you will also increment all of them several times and get the wrong counts.
Without giving you the exact code for changing this, just restructuring your function this way should already help coming to a cleaner solution:
Change the processing order. Instead of searching for individual values in your responses, try to do a "pre-processing" step where you create a map which represents the count of each unique key you encounter in the responses values.
The result will be almost what you were looking for, only the keys are not mapped to the values of arr1 yet. This has the advantage of only requiring a single iteration over your responses array, getting rid of the problem of counting some values multiple times.
In case you chose this approach because the responses values might contain values you are not interested in and you want to "skip" those, that behaviour can be replicated in the pre-processing phase by checking if the currently inspected value is contained in the arr1 array before writing it to the result map.
Does this cover the problem you were seeing, or was there an actual other error you encountered?

The OP should think about choosing an approach which breaks the entire task apart into smaller ones.
From the ratings-array (the one which literally names the ratings) create a rating-value (the ones that come with the response item's values) based map for looking up rating-keys (the rating-names which are the keys to the to be achieved rating-counts map).
const ratingValuesToKeys = new Map([
"Strongly positive",
"Positive",
"Neutral",
"Negative",
"Strongly negative",
].map((key, idx) => [idx + 1, key]));
console.log(Object.fromEntries([...ratingValuesToKeys]));
Also from the very same ratings-array create a rating-name based map for counting/summing up the occurrences of related rating-values.
const ratingCounts = new Map([
"Strongly positive",
"Positive",
"Neutral",
"Negative",
"Strongly negative",
].map(key => [key, 0]));
console.log(Object.fromEntries([...ratingCounts]));
Sanitize and collect all key specific rating-values for they are occurring as different types like number and/or string values as well as arrays.
const ratingValues = [
{ values: { QID16: 3 } },
{ values: { QID16: ["1", "2", "3"] } },
{ values: { QID16: 1 } },
]
.reduce((result, { values }) => {
if (values.hasOwnProperty('QID16')) {
const rating = values['QID16'];
if (Array.isArray(rating)) {
result
.push(
...rating
.map(value =>
parseInt(value, 10)
)
);
} else {
result
.push(parseInt(rating, 10));
}
}
return result;
}, []);
console.log({ ratingValues });
Based on all sanitized key specific rating-values do update each rating's occurrence by incrementing the related rating-key's count-value.
The final combined implementation and example code then might look similar to this ...
const ratings = [
"Strongly positive",
"Positive",
"Neutral",
"Negative",
"Strongly negative",
];
const responses = [
{ values: { QID16: 3 } },
{ values: { QID16: ["1", "2", "3"] } },
{ values: { QID16: 1 } },
];
function getRatingCounts(responseValueKey, ratings, responses) {
// create a rating-value based map for looking up rating-keys.
const ratingValuesToKeys = new Map(
ratings
.map((key, idx) => [idx + 1, key])
);
// create a rating-key based map for counting/summing up ratings.
const ratingCounts = new Map(
ratings
.map(key => [key, 0])
);
// sanitize and collect all key specific rating-values
// for they are occurring as different types ... like
// number and/or string values as well as arrays.
const ratingValues = responses
.reduce((result, { values }) => {
if (values.hasOwnProperty(responseValueKey)) {
const rating = values[responseValueKey];
if (Array.isArray(rating)) {
result
.push(
...rating
.map(value =>
parseInt(value, 10)
)
);
} else {
result
.push(parseInt(rating, 10));
}
}
return result;
}, []);
// based on all sanitized key specific rating-values
// do update each rating's occurrence by incrementing
// the related rating-key's count-value.
ratingValues
.forEach(value => {
const ratingKey = ratingValuesToKeys.get(value);
const ratingCount = ratingCounts.get(ratingKey);
ratingCounts.set(ratingKey, ratingCount + 1);
});
return ratingCounts;
}
const ratingCounts = getRatingCounts('QID16', ratings, responses);
console.log({
ratingCounts,
'counts as entry list': [...ratingCounts],
'counts as object': Object.fromEntries([...ratingCounts]),
});
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related

How can I push objects to arrays with similar names?

I have a form that I can create duplicate sections. Right now when the form is submitted the form spits out one giant object. I'm working on a filter function that can filter out the duplicate sections and organize the duplicates in array so it works with my API.
e.g.
// base object
{
question_10: ""
question_10a: ""
question_11_checkbox: false
question_11_checkbox_copy_0: false
question_11_checkbox_copy_1: true
question_11_text: "110 Monroe St"
question_11_text_copy_0: "186 Aspen Road"
question_12_checkbox: false
question_12_checkbox_copy_0: false
question_12_text: "New York"
question_12_text_copy_0: "South Orange"
...
}
// what I want is
{
question_10: ""
question_10a: ""
question_11_checkbox: false
question_11_checkbox_copies: [
{ question_11_checkbox_copy_0: false }
{ question_11_checkbox_copy_1: true }
]
question_11_text: "101 Monroe St"
question_11_text_copies: [
{ question_11_text_copy_0: "186 Aspen Road"}
]
question_12_checkbox: false
question_12_checkbox_copies: [
{ question_12_checkbox_copy_0: false}
]
question_12_text: "New York"
question_12_text_copies: [
{ question_12_text_copy_0: "South Orange"}
]
...
}
So far I've been able to filter out the copies from the original object and create the arrays for the copies
// filter out copy keys
const copiesKey = Object.keys(data).filter(key => key.includes('copy'));
const copy = {};
// create arrays for copies
copiesKey.map(copiedQuestion => {
if (!(`${copiedQuestion.slice(0, copiedQuestion.length - 7)}_copies` in copy)) {
copy[`${copiedQuestion.slice(0, copiedQuestion.length - 7)}_copies`] = [];
}
});
Where I'm stuck is it's not clear how to match the object to the appropriate array and push it.
e.g.
question_11_text_copies: [
{ question_11_text_copy_0: "186 Aspen Road" }
]
So far I've tried to slice the last three keys of the copy_0 object and match the key to the array name by using array.filter, but that didn't work as expected.
How can I match the 'copy_n' objects to the appropriate array and push those objects to the array?
This might be a bit old fashioned, but using a for...in loop in combination with regex pattern matching against the key might be the clearest solution.
const data = {
question_10: "",
question_10a: "",
question_11_checkbox: false,
question_11_checkbox_copy_0: false,
question_11_checkbox_copy_1: true,
question_11_text: "110 Monroe St",
question_11_text_copy_0: "186 Aspen Road",
question_12_checkbox: false,
question_12_checkbox_copy_0: false,
question_12_text: "New York",
question_12_text_copy_0: "South Orange",
};
const copy_n = /^(.*)_copy_(\d+)$/;
const result = {};
for (const key in data) {
const value = data[key];
const match = key.match(copy_n);
if (match) {
const copies_key = `${match[1]}_copies`;
const index = parseInt(match[2], 10);
result[copies_key] ||= [];
result[copies_key][index] = { [key]: value };
} else {
result[key] = value;
}
}
console.log(result);
The pattern /^(.*)_copy_(\d+)$/ matches anything that ends with _copy_ followed by 1 or more decimals. Everything before _copy_ is placed in capture group 1, the decimals behind _copy_ are placed in capture group 2.
If there is no match (else scenario) we simply assign the value to the same key.
If there is a match (if scenario) we first determine the collection key (copies_key) and the index to use. We then check if result[copies_key] is already set (we check if it's a truthy value to be precise). If it's not, we assign it to an empty array. After that we use the index to assign an object to the correct index in the array.
You can reduce to object's entries to a new object. For each key find if it has a base key (the one before "copy") using a RegExp with lookahead. If it doesn't has a base key, add it directly to the accumulator. If it does, add it to the appropriate "copies" array (initialize it if needed using Logical nullish assignment ??=) (TS playground).
const fn = obj => Object.entries(obj)
.reduce((acc, [k, v]) => {
const [baseKey] = k.match(/.*(?=_copy_)/) ?? [] // get the key from an item with copy
if(!baseKey) acc[k] = v // if no baseKey add the item to the accumulator
else {
const copiesKey = `${baseKey}_copies`
acc[copiesKey] ??= [] // if no copies key add a new one to the accumulator
acc[copiesKey].push({ [k]: v }) // create an object and push to the current copies key
}
return acc
}, {})
const obj = {"question_10":"","question_10a":"","question_11_checkbox":false,"question_11_checkbox_copy_0":false,"question_11_checkbox_copy_1":true,"question_11_text":"110 Monroe St","question_11_text_copy_0":"186 Aspen Road","question_12_checkbox":false,"question_12_checkbox_copy_0":false,"question_12_text":"New York","question_12_text_copy_0":"South Orange"}
const result = fn(obj)
console.log(result)

How to reduce objects in an array based off an existing key value

So I have an existing array with three objects:
[ { date: 3/12/2022, oService: 10}
{ date: 3/13/2022, oService: 12, aService: 1}
{ date: 3/13/2022, oService: 1 }]
Based on the date, I would like to get something that looks like the following:
[ {date: 3/12/2022, oService: 10}
{date: 3/13/2022, oService: 13, aService: 1}]
additionally, I could have up to 10 services, so I cant reduce by prev.oService. I'll have an array of services that looks something like:
const Services = [aService, bService, cService, oService, yService]
I think it's best to think of the problem as 2 different steps:
First, get all things that belong to the same date together. This is a groupBy that you should be able to look up on StackOverflow or borrow from an existing library.
Then, you merge each of the date groups in to a single object.
The merge operation is a bit more custom than the groupBy operation. You can implement it in many ways. I would propose to:
Implement merge to deal with just 2 objects
Inside merge, loop over all unique keys in those objects and sum them (excluding the date key)
To apply merge to a date group, use reduce. Note that it's safe to not provide a seed argument here, because you are guaranteed to not have empty groups.
// Merge two date-service entries in to one
const merge = (a, b) => {
const merged = Object.assign({}, a);
Object.keys(b).forEach(k => {
if (k !== "date")
merged[k] = (merged[k] || 0) + b[k];
});
return merged;
};
const input = [ { date: "3/12/2022", oService: 10},
{ date: "3/13/2022", oService: 12, aService: 1},
{ date: "3/13/2022", oService: 1 }];
// Create groups that have a matching date
const byDate = groupByProp("date", input);
// Reduce each group to a single item by merging
const all = Object.values(byDate).map(xs => xs.reduce(merge));
console.log(all);
// A basic `groupBy` implementation
function groupByProp(prop, xs) {
return xs.reduce(
(groups, x) => {
const k = x[prop];
if (!groups[k]) groups[k] = [x];
else groups[k].push(x);
return groups;
},
{}
);
}

Process multiple request queries

I have an API that serves JSON data. Currently if you do api/weapons for example it gives you all the weapons available, api/weapons/weaponName gives information about that specific weapon. What I want to do is be able to api/weapons?type=sword&rarity=5 for example. I managed to pull of api/weapons?type=sword and api/weapons?rarity=5 on their own but not together.
Here's what I'm currently doing:
let filtered = [];
if (query.type) {
filtered = filtered.concat((await weapons).filter(w => formatName(w.weaponType) === formatName(query.type)));
}
if (query.rarity) {
filtered = filtered.concat((await weapons).filter(w => w.rarity == query.rarity));
}
if (!filtered.length) filtered = [await weapons]
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
formatName is just a function that makes the string all lowercase and trims it and removes all spaces.
If we take api/weapons?type=sword&rarity=5
I think what's happening right now is:
It is getting all the weapons with the type "sword"
It is getting all the weapons with the rarity "5"
It is joining all the results together, so all the weapons with the type sword (regardless of rarity) and al the weapons with the rarity 5 (regardless of type).
I want it to filter weapons with ONLY that rarity AND ONLY that type. So only 5 rarity swords for example. What is the most beneficial way of handling this
I'd suggest retrieving "weapons" once and then running any filters on them without concatenating the results:
let filtered = [ ...(await weapons) ];
if (query.type) {
filtered = filtered.filter(w => w => formatName(w.weaponType) === formatName(query.type));
}
if (query.rarity) {
filtered = filtered.filter(w => w.rarity == query.rarity);
}
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
Your current logic is testing whether one constraint OR another matches, what you actually need to do is to do an AND, which means you must perform the test in a single pass of filter.
I would slightly modify your code so that you compare all constraints that you're sending...you could further modify the logic below to accept a logical operator to test whether the rarity is >= or <= to a certain number for example.
const weapons = [{
type: 'sword',
name: 'swift blade of zek',
rarity: 5
},
{
type: 'mace',
name: 'hammer of kromzek kings',
rarity: 1
},
{
type: 'sword',
name: 'split blade of thunder',
rarity: 2
},
{
type: 'sword',
name: 'blade of carnage',
rarity: 5
},
]
const getWeapons = (query = {}) => {
let filtered = [];
let constraints = [];
// We could build this object dynamically but I just wanted
// to demonstrate it using your current approach
if (query.hasOwnProperty('type')) {
constraints.push({
name: 'type',
value: query.type
})
}
if (query.hasOwnProperty('rarity')) {
constraints.push({
name: 'rarity',
value: query.rarity
})
}
// Compare all of the conditions and only return weapons
// that match all of the conditions passed.
filtered = weapons.filter(w => {
let matches = 0
constraints.forEach(c => {
if (w[c.name] === c.value) {
matches += 1
}
})
// ensures we only return complete matches
return matches === constraints.length
});
return filtered
}
console.log(getWeapons({
type: 'sword',
rarity: 5
}))
Create an object which has the same property keys as the filters you want to use. Assign a function to each property where the evaluation for that specific filter is specified.
const filters = {
type: (weapon, type) => formatName(weapon.weaponType) === formatName(type),
rarity: (weapon, rarity) => weapon.rarity === rarity,
};
Then loop over the weapons with filter. Inside the filter loop, loop over the keys of the query variable with the every method. This method will return true or false based on if every evaluation is true or not.
In the every loop, use the keys of the query to select the filter from the filters list. Pass the weapon and the values of the query object to these filter functions and return result.
By doing this you can use one, two or no filters at all. And any new filters can be added in the filters object.
const filteredWeapons = weapons.filter((weapon) =>
Object.keys(query).every((filterKey) => {
if (!(filterKey in filters)) {
return false;
}
const filter = filters[filterKey]
const value = query[filterKey]
return filter(weapon, value);
})
);
res.status(HttpStatusCodes.ACCEPTED).send(filteredWeapons);

Filter array of objects based on the input passed: Javascript

I have an array of objects with the following structure
arr = [ { name: "abc" , items: ["itemA","itemB","itemC"], days :138} ,
{ name: "def" , items: ["itemA1","itemB2","itemC1"], days :157} ,
{ name: "hfg" , items: ["itemAN","itemB7","itemC7"], days :189} ]
This array needs to be filtered based on the search input passed. I was able to achieve the same for the name , where days is not getting filtered.
Also can someone help how to search across items array too so it filters the rows based on input passed
This is what I have tried
handleSearch = (arr, searchInput) => {
let filteredData= arr.filter(value => {
return (
value.name.toLowerCase().includes(searchInput.toLowerCase()) ||
value.days.toString().includes(searchInput.toString())
);
});
console.log(filteredData);
//this.setState({ list: filteredData });
}
You can use Array#some and then perform the same kind of match that you've already done :
The some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value.
handleSearch = (arr, searchInput) => {
const filteredData = arr.filter(value => {
const searchStr = searchInput.toLowerCase();
const nameMatches = value.name.toLowerCase().includes(searchStr);
const daysMatches = value.days.toString().includes(searchStr);
const oneItemMatches = value.items.some(item => item.toLowerCase().includes(searchStr));
return nameMatches || daysMatches || oneItemMatches;
});
console.log(filteredData);
//this.setState({ list: filteredData });
}
As your search value can apply to all fields in your data array, you can combine the values together in one array (row by row) and perform the search in one place.
To do that, I've provided a snippet below that will filter the original array checking each object's values after the transformations. These involve using Object.values() to get the values of the object in an array, since this array is nested, we can make use of Array.flat() to flatten it into just the strings and numbers, finally call Array.some() to check if one of the values partially includes the search value (after they've both been lowercase-d).
const arr = [
{ name: "abc" , items: ["itemA","itemB","itemC"], days: 138 },
{ name: "def" , items: ["itemA1","itemB2","itemC1"], days: 157 },
{ name: "hfg" , items: ["itemAN","itemB7","itemC7"], days: 189 }
];
const handleSearch = (arr, searchInput) => (
arr.filter((obj) => (
Object.values(obj)
.flat()
.some((v) => (
`${v}`.toLowerCase().includes(`${searchInput}`.toLowerCase())
))
))
);
console.log('"A1" =>', JSON.stringify(handleSearch(arr, 'A1')));
console.log('189 =>', JSON.stringify(handleSearch(arr, 189)));
console.log('"nope" =>', JSON.stringify(handleSearch(arr, 'nope')));
NOTE: This approach has one obvious flaw, it will seach through numbers as strings, meaning that providing 89 as the search value will still return the second element.

How do I turn the values from an object into an array?

I have a function that is polling for temperature data:
{"a":"43",
"b":"43",
"c":"42",
"d":"43",
"e":"40",
"f":"41",
"g":"100",
"h":"42.6"}
I want to be able to graph that data over time, but I can't figure out the best way to map the above data, to something like the below data:
temps: [{
name: "a",
data: ["43","42","43"]
},
name: "b",
data: ["43","42","43"]
},
etc...
]
I have tried the code below, and tried to figure out the javascript map function, but I keep running into scoping problems where "this" isn't the same thing as it was in the parent:
this.temp_names.forEach(function(e){
if(typeof this.temps[e] == "undefined") {
this.temps[e] = []
}
this.temps.e.unshift(this.sys_telemetry.S.temps)
if (this.temps.e.length > 10) {
this.temps.e.pop()
}
})
where "temp_names" was an array of the keys.
I'm doing this in VueJS, so the "this" is accessing the data in my component.
Using Array#from, Object#entries, Array#map and destructuring you could do something like this.
const data={"a":"43","b":"43","c":"42","d":"43","e":"40","f":"41","g":"100","h":"42.6"}
const res = Object.entries(data)
.map(([name, data])=>({name, data:[data]}));
console.log(res);
Alternative using Array#reduce, Map,
const data={"a":"43","b":"43","c":"42","d":"43","e":"40","f":"41","g":"100","h":"42.6"}
const res = Array.from(Object
.entries(data)
.reduce((a,[k,v])=>{
if(!a.has(k)) a.set(k, []);
a.get(k).push(v);
return a;
}, new Map()))
.map(([name, data])=>({name, data}));
console.log(res);
graph that data over time
Because you want to do this over time, it would make sense to create an array and then using Object.entries, & Array.find, update the results.
Here is an example.
const values1 =
{"a":"43", "b":"43", "c":"42", "d":"43", "e":"40", "f":"41",
"g":"100", "h":"42.6"};
const values2 =
{"c":"44", "e":"39"};
const results = [];
function addData(data) {
Object.entries(data).forEach(([k, v]) => {
let find = results.find(f => f.name === k);
if (!find) {
find = {
name: k,
data: []
}
results.push(find);
}
find.data.push(v);
});
}
addData(values1); //data packet one arrives
addData(values2); //data packet two arrives
console.log(results); //results contains both data packet one & two.
You might be able to get away with a simpler data structure like, eg. { a: [43, 42, 43], b: [1, 2, 3] }
ie. instead of having separate name and data keys, you could use name as the key, and the data array as the value.
If this would work to represent the timeline for each key, and your initial data is structured like, eg. [{ a: 43, b: 1, c: 3 }, { a: 42, b: 2, c: 3 }], then something like this might be suitable to transform the latter into the former:
const output = {};
temp_data.forEach(x => {
for (const key in x) {
const y = x[key];
if (typeof output[key] === 'undefined') {
output[key] = [];
}
output[key].push(y);
}
});
This produces an object whose keys match the keys in your data points (eg. "a", "b", "c", etc), and whose values are an array of all the values for each of these keys, which might be suitable for plotting a timeline.
(Incidentally, if you want to plot these as values on a graph, it might be better to treat the values as numbers - eg. 1, 2, 3 - rather than strings - eg. "1", "2", "3").
There are probably more elegant, functional-style ways of doing this, but this might do the job!
It seems to me that you want to be able to add multiple datasets to the data object. One approach is to have a data object with methods that know how to do things like add data to themselves, maybe something like the following. You might want to keep the index property private, and maybe sort it so it's always in a particular order regardless of the order the values are added.
var data0 = {"a":"43",
"b":"43",
"c":"42",
"d":"43"};
var data1 = {"a":"53",
"b":"53",
"c":"52",
"d":"53",
"e":"65"
};
class DataObject {
constructor (data) {
this.index = [];
this.data = [];
if (data) {
this.addData(data);
}
}
addData (data) {
Object.keys(data).forEach(key => {
let idx = this.index.indexOf(key);
if (idx == -1) {
idx = this.index.push(key) - 1;
this.data.push({name:key, data:[]});
}
this.data[idx].data.push(data[key]);
});
}
}
// Initialise object with some data
let myData = new DataObject(data0);
console.log(JSON.stringify(myData.data));
// Add more data
myData.addData(data1);
console.log(JSON.stringify(myData.data));

Categories

Resources