Aggregating data after building multi-level array in javascript - javascript

I have a reduce function that is building multiple levels and is working perfectly for me except for one issue
Currently, it's building data based on employee first, then by date, area, and job. I'm getting all of the data at the proper level but I'm now trying to aggregate certain data for a totals section at the date level and it's just listing values rather than aggregating.
Basically, in the line I've notated below, I'd like to create a value called total_scans that simply adds up ALL scans for any orders on that date. In other words, for the record for Miranda on 8/12 I would expect the total_scans at the date level to have 49 as the value. Am I on the right track?
const nest = (rows) =>
rows.reduce(
(a, row) => {
const employee = a[row.employee] || (a[row.employee] = { dates: {} })
const date = employee.dates[row.job_date] || (employee.dates[row.job_date] = { areas: {} })
const order = date.areas[row.area_number] || (date.areas[row.area_number] = { jobs: {} })
const job = order.jobs[row.job] || (order.jobs[row.job] = { hours: '', scans: '', job_date: '' })
job.hours += row.hours
job.scans += row.scans
job.job_date = row.job_date
//this line is my issue
date.total_scans += job.scans
return a
},
{}
);
new Vue({
el: "#app",
props: {
},
data: {
rows: [
{
employee: "Miranda",
job: "123",
hours: "10",
job_date: "08/12/2021",
scans: 37,
area_number: "1234567",
},
{
employee: "Miranda",
job: "167",
hours: "15",
scans: 12,
job_date: "08/12/2021",
area_number: "1234568",
},
{
employee: "Miranda",
job: "184",
hours: "18",
scans: 24,
job_date: "08/13/2021",
area_number: "1234569",
}
],
},
computed: {
numbersByEmployee() {
return nest(this.rows)
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{numbersByEmployee}}
</div>

Your usage of reduce is a little irregular. The idea of reduce is to take an iterable (array) and return a single value, usually something like a String or Number.
Also, you're causing all sorts of side effects in your reducer, by modifying the object and arrays. Since Javascript is pass-by-reference for arrays and objects, those changes you're causing will be reflected in the original object, which is not how Vue prescribes things are done. If you want to modify data, it should be done in a watch, not a computed.
Finally, I believe you're overcomplicating your reduce function. Instead of a long reduce like that, you could simply do the below. Note the initialValue of 0.
const nest = (rows) =>
rows.reduce(
(sum, row) => {
return sum + row['scans'];
},
0
);
Obviously this will count all scans. If you want to only count scans by date, how about save yourself running the reducer across the array, and instead run filter first? Something like
const nest = (rows) =>
rows.filter(({job_date}) => job_date === SomeDate).reduce(...)
The {job_date} is a destructuring assignment. You could also split out a date filtered array into its own computed.

Related

Access and extract values from doubly nested array of objects in JavaScript

I have an array of objects and within those objects is another object which contains a particular property which I want to get the value from and store in a separate array.
How do I access and store the value from the name property from the data structure below:
pokemon:Object
abilities:Array[2]
0:Object
ability:Object
name:"blaze"
1:Object
ability:Object
name:"solar-power"
How would I return and display the values in the name property as a nice string like
blaze, solar-power ?
I tried doing something like this but I still get an array and I don't want to do a 3rd loop since that is not performant.
let pokemonAbilities = [];
let test = pokemon.abilities.map((poke) =>
Object.fromEntries(
Object.entries(poke).map(([a, b]) => [a, Object.values(b)[0]])
)
);
test.map((t) => pokemonAbilities.push(t.ability));
Sample Data:
"pokemon": {
"abilities": [
{
"ability": {
"name": "friend-guard",
"url": "https://pokeapi.co/api/v2/ability/132/"
},
"ability": {
"name": "Solar-flare",
"url": "https://pokeapi.co/api/v2/ability/132/"
}
}
]
}
Then I am doing a join on the returned array from above to get a formatted string.
It just seems like the multiple map() loops can be optimized but I am unsure how to make it more efficient.
Thank you.
There is no need for a loop within loop. Try this:
const pokemon = {
abilities: [{
ability: {
name: 'friend-guard',
url: 'https://pokeapi.co/api/v2/ability/132/'
},
}, {
ability: {
name: 'Solar-flare',
url: 'https://pokeapi.co/api/v2/ability/132/'
}
}]
};
const pokemonAbilities = pokemon.abilities.map(item => item.ability.name).join(', ');
console.log(pokemonAbilities);

Optimal way of transforming flat map to nested data structure using React.js?

I've been pondering the best way to handle grouping in my app. It's a video editing app and I am introducing the ability to group layers. If you're familiar with Figma or any design/video editing program then there is usually the ability to group layers.
To keep this simple in the app the video data is a map
const map = {
"123": {
uid: "123",
top: 25,
type: "text"
},
"345": {
uid: "345",
top: 5,
type: "image"
},
"567": {
uid: "567",
top: 25,
type: "group"
children: ["345", "123"]
}
}
Then I am grouping them inside a render function (this feels expensive)
const SomeComponent = () => {
const objects = useMemo(() => makeTrackObjects(map), [map]);
return (
<div>
{objects.map(object => {
return <div>Some layer that will change the data causing re-renders</div>
})}
</div>
)
}
Here is the function that does the grouping
const makeTrackObjects = (map) => {
// converts map to array
const objects = Object.keys(map).map((key: string) => ({ ...map[key] }));
// flat array of all objects to be grouped by their key/id
const objectsInGroup = objects
.filter((object) => object.type === "group")
.map((object) => object.children)
.flat();
// filter out objects that are nested/grouped
const filtered = objects.filter((object) => !objectsInGroup.includes(object.uid))
// insert objects as children during render
const grouped = filtered.map((object) => {
const children = object.children
? {
children: object.children
.map((o, i) => {
return {
...map[o]
};
})
.flat()
}
: {};
return {
...object,
...children
};
});
// the core data is flat but now nested for the UI. Is this inefficient?
return grouped
}
Ideally I would like to keep the data flat, I have a lot of code that I would have to update to go deep in the data. It feels nice to have it flat and transformers in certain areas where needed.
The main question is does this make sense, is it efficient, and if not then why?
If you are running into performance issues, one area you may want to investigate is how you are chaining array functions (map, filter, flat, etc). Each call to one of these functions creates an intermediate collection based on the array it receives. (For instance, if we chained 2 map functions, this is looping through the full array twice). You could increase performance by creating one loop and adding items into a collection. (Here's an article that touches on this being a motivation for transducers.)
I haven't encountered a performance issue with this before, but you may also want to remove spread (...) when unnecessary.
Here is my take on those adjustments on makeTrackObjects.
Update
I also noticed that you are using includes while iterating through an array. This is effectively O(n^2) time complexity because each item will be scanned against the full array. One way to mitigate is to instead use a Set to check if that content already exists, turning this into O(n) time complexity.
const map = {
"123": {
uid: "123",
top: 25,
type: "text"
},
"345": {
uid: "345",
top: 5,
type: "image"
},
"567": {
uid: "567",
top: 25,
type: "group",
children: ["345", "123"]
}
};
const makeTrackObjects = (map) => {
// converts map to array
const objects = Object.keys(map).map((key) => map[key]);
// set of all objects to be grouped by their key/id
const objectsInGroup = new Set();
objects.forEach(object => {
if (object.type === "group") {
object.children.forEach(child => objectsInGroup.add(child));
}
});
// filter out objects that are nested/grouped
const filtered = objects.filter((object) => !objectsInGroup.has(object.uid))
// insert objects as children during render
const grouped = filtered.map((object) => {
const children = {};
if (object.children) {
children.children = object.children.map(child => map[child]);
}
return {
...object,
...children
};
});
// the core data is flat but now nested for the UI. Is this inefficient?
return grouped
}
console.log(makeTrackObjects(map));

How can I filter through an array of objects based on a key in a nested array of objects?

I am having a hard time filtering through an array of objects based on a value in a nested array of objects. I have a chat application where a component renders a list of chats that a user has. I want to be able to filter through the chats by name when a user types into an input element.
Here is an example of the array or initial state :
const chats= [
{
id: "1",
isGroupChat: true,
users: [
{
id: "123",
name: "Billy Bob",
verified: false
},
{
id: "456",
name: "Superman",
verified: true
}
]
},
{
id: "2",
isGroupChat: true,
users: [
{
id: "193",
name: "Johhny Dang",
verified: false
},
{
id: "496",
name: "Batman",
verified: true
}
]
}
];
I want to be able to search by the Users names, and if the name exists in one of the objects (chats) have the whole object returned.
Here is what I have tried with no results
const handleSearch = (e) => {
const filtered = chats.map((chat) =>
chat.users.filter((user) => user.name.includes(e.target.value))
);
console.log(filtered);
// prints an empty array on every key press
};
const handleSearch = (e) => {
const filtered = chats.filter((chat) =>
chat.users.filter((user) => user.name.includes(e.target.value))
);
console.log(filtered);
// prints both objects (chats) on every keypress
};
Expected Results
If the input value is "bat" I would expect the chat with Id of 2 to be returned
[{
id: "2",
isGroupChat: true,
users: [
{
id: "193",
name: "Johhny Dang",
verified: false
},
{
id: "496",
name: "Batman",
verified: true
}
]
}]
The second approach seems a little closer to what you're trying to accomplish. There's two problems you may still need to tackle:
Is the search within the name case insensitive? If not, you're not handling that.
The function being used by a filter call needs to return a boolean value. Your outer filter is returning all results due to the inner filter returning the array itself and not a boolean expression. Javascript is converting it to a "truthy" result.
The following code should correct both of those issues:
const filtered = chats.filter((chat) => {
const searchValue = e.target.value.toLowerCase();
return chat.users.filter((user) => user.name.toLowerCase().includes(searchValue)).length > 0;
});
The toLowerCase() calls can be removed if you want case sensitivity. The .length > 0 verifies that the inner filter found at least one user with the substring and therefore returns the entire chat objects in the outer filter call.
If you want to get object id 2 when entering bat you should transform to lowercase
const handleSearch = (e) =>
chats.filter(chat =>
chat.users.filter(user => user.name.toLowerCase().includes(e.target.value)).length
);
try this it should work
const handleSearch2 = (e) => {
const filtered = chats.filter((chat) =>
chat.users.some((user) => user.name.includes(e))
);
console.log(filtered);
};
filter needs a predicate as argument, or, in other words, a function that returns a boolean; here some returns a boolean.
Using map as first iteration is wrong because map creates an array with the same number of elements of the array that's been applied to.
Going the easy route, you can do this.
It will loop first over all the chats and then in every chat it will check to see if the one of the users' username contains the username passed to the function. If so, the chat will be added to the filtered list.
Note, I am using toLowerCase() in order to make the search non case sensitive, you can remove it to make it case sensitive.
const handleSearch = (username) => {
var filtered = [];
chats.forEach((chat) => {
chat.users.forEach((user) => {
if (user.name.toLowerCase().includes(username.toLowerCase())) {
filtered.push(chat);
}
});
});
console.log(filtered);
return filtered;
}
handleSearch('bat');

Mapping thru state object and testing multiple nested arrays without multiple return values?

I'm (obviously) very new to React and Javascript, so apologies in advance if this is a stupid question. Basically I have an array of objects in this.state, each with its own nested array, like so:
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
}, ...
I've already written a function that maps through the state objects and runs .includes() on each object.recipe to see if it contains a string.
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
The main issue is that .map() returns a value for each item in the array, and I don't want that. I need to have a function that checks each object.recipe, returns a match if it finds one (like above), but also returns a "No match found" message if NONE of the nested arrays contain the value it's searching for. Right now this function returns "{this.state.findMe} not found in {thing.type}" for each object in the array.
I do know .map() is supposed to return a value. I have tried using forEach() and .filter() instead, but I could not make the syntax work. (Also I can't figure out how to make this function a stateless functional component -- I can only make it work if I put it in the render() method -- but that's not my real issue here. )
class App extends React.Component {
state = {
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
},
{
type: "chili",
recipe: ["beans", "beef", "tomato"]
},
{
type: "padthai",
recipe: ["noodles", "peanuts", "chicken"]
},
],
findMe: "bread",
}
render() {
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
return (
<div>
<div>
<h3>Results:</h3>
{newArray}
</div>
</div>
)
}
};
You could use Array.reduce for this:
const result = foods.reduce((acc,curr)=> curr.recipe.includes(findMe) ? `${curr.type} contains ${findMe}`:acc ,'nothing found')
console.log(result)
Though if the ingredient is found in more than one recipe it will only return the last one.
Alternatively, you could use a map and a filter:
const result = foods.map((thing, i) => {
if (thing.recipe.includes(findMe)) {
return `${thing.type} contains ${findMe}`;
}}).filter(val=>!!val)
console.log(result.length?result[0]:'nothing found')

Am I updating the React state correctly?

In my React state, I have the state:
this.state = {
users: [{
id: 1,
name: "john",
age: 27
}, {
id: 2,
name: "ed",
age: 18
}, {
id: 3,
name: "mel",
age: 20
}]
}
I am rendering the name correctly. When you click on the name, it should remove the name, which will need an onClick that takes in a function and that returns a removeUser function.
It is my understanding that you do not want to mutate the state in React, but return a new state. So, in my removeUser function, I did:
removeUser(index) {
// Making a new copy of the array
const users = [...this.state.people].splice(index, 1);
this.setState({ users });
}
I bind my method with this.removeUser = this.removeUser.bind(this).
When I tested out my code, I am removing the users as expected. However, when I run my code against a test that my friend wrote, I got a failed test that said: Expected 1 to be 0
That message tells me that I must be mutating the state somehow, but I am not sure how. Am I returning a new array and updating the state correctly? Can someone explain to me how I should update my state correctly in this case?
Here is the full code:
class Group extends Component {
constructor(props) {
super(props);
this.state = {
users: [
{id: 1, name: "john", age: 27},
{id: 2, name: "ed", age: 18},
{id: 3, name: "mel", age: 20}
]
}
this.removeUser = this.removeUser.bind(this);
}
removeUser(index) {
const users = [...this.state.users].splice(index, 1);
this.setState({ users });
}
render() {
const list = this.state.users.map((user, i) => {
return (
<div onClick={() => this.removeUser(i)} key={i}>{user.name}</div>
);
});
return (
<div>
{list}
</div>
);
}
}
you could also change your onClick and pass it the user.id instead of the index. that would allow you to filter the results instead of needing to splice the array.
onClick={() => this.removeUser(user.id)}
removeUser(id) {
const users = this.state.users.filter((user) => user.id !== id);
this.setState({ users });
}
const users = [...this.state.users].splice(index, 1)
looks like it should be removing an item from a collection, which I suppose it technically is. The problem is that users doesn't contain the list of users you want to keep.
Instead, splice has modified your new array in-place and then returned the removed items:
splice docs
Return value
An array containing the deleted elements. If only one element is removed, an array of one element is returned. If no elements are removed, an empty array is returned.
Instead, if you'd like to use splice, create a new array:
const users = [...this.state.users]
and then splice the new array:
users.splice(index, 1)
You'll run into a similar issue if you need to sort an array.
The issue of modifying data in-place is generally frowned upon for react, in favor of immutable references. This is because React uses a lot of direct comparison of objects as a heuristic approach to speed things up. If your function modifies an existing object instance, React will assume the objects haven't changed.
The act of copying to a new object and then operating on the data comes with some tradeoffs. For removing a single item, it's negligible. For removing many items, you may be better served by an alternative method, such as multiple slice calls:
const users = [
...this.state.users.slice(0, index)
...this.state.users.slice(index + 1)
]
However this too is quite verbose.
Another approach is to use an immutable variant of splice:
// this quick example doesn't handle negative start indices
const splice = (start, deleteCount, ...items) => arr => {
const output = []
let i
for (i = 0; i < start && i < arr.length; i++) {
output.push(arr[i])
}
output.push(...items)
for (i += deleteCount; i < arr.length; i++) {
output.push(arr[i])
}
return output
}
const users = splice(index, 1)(this.state.users)
You should use slice() instead of splice() because splice() mutates the original array, but slice() returns a new array. Slice() is a pure function. Pure is better!!
removeUser(index) {
const users = [
...this.state.users.slice(0, index),
...this.state.users.slice(index+1)
];
this.setState({ users });
}
Here is JS Fiddle

Categories

Resources