I have an application where an object is used to display a tree view of files on a user's system. It's structured like so:
[{
text: 'C:/',
type: 'dir',
nodes: [
{
text: 'foo',
type: 'dir',
nodes: [] // And so on
},
{
text: 'bar',
type: 'file'
}
}]
In keeping with conventions, I'd like directories to be displayed first and files to be displayed second. Unfortunately, my data is retrieved in alphabetical order regardless of item type.
To remedy this I wrote a nice recursive function
var sort = function (subtree)
{
subtree = _.sortBy(subtree, function (item)
{
if (item.nodes)
{
sort(item.nodes)
}
return item.type
});
}
var tree = someTreeData;
sort(tree);
I'm using lodash to sort each of the nodes arrays alphabetically by file type. Unfortunately the subtree does not appear to reference the tree object as when I log its output it remains unsorted. How can I remedy this?
You can use JavaScript’s built-in Array.prototype.sort function, which does sort in-place. It accepts two arguments and performs a comparison. Note that sorting item.notes inside the sortBy key extractor is kind of inappropriate.
function isDirectory(node) {
return !!node.nodes;
}
function sortTree(subtree) {
subtree.sort(function (a, b) {
return a.type < b.type ? -1 :
a.type > b.type ? 1 : 0;
});
subtree
.filter(isDirectory)
.forEach(function (node) {
sortTree(node.nodes);
});
}
Related
My Problem:
I'm having a website where I can compare products stored inside an array (with objects). I want to add different filters from array inside of an object that get applied together.
For two filters I can easily do it (see my code below). I just compare two objects and use a filter depending on their content.
But what would be a good approach to use the filter if there are more than two objects. Can I loop through the object and compare if the arrays are empty?
With my current approach I would have to extend my code for every new filter and it would balloon.
What I'm trying to do:
I want to check which filter objects have any data in their "feature" array (that array gets filled after the user clicks a filter on the site) and if they have I want to use these arrays to filter the main filteredArray array.
My current Object:
features_collection: {
aspect_ratio_object: {
features: [],
value: "Aspect Ratio",
},
performance_rating_object: {
features: [],
value: "Performance Rating",
},
},
My Filter Function:
if (
features_collection.aspect_ratio_object.features.length &&
features_collection.performance_rating_object.features.length
) {
return filteredArray.filter(
(obj) =>
features_collection.aspect_ratio_object.features.includes(
obj[features_collection.aspect_ratio_object.value]
) &&
features_collection.performance_rating_object.features.includes(
obj[features_collection.performance_rating_object.value]
)
);
} else if (
features_collection.aspect_ratio_object.features.length ||
features_collection.performance_rating_object.features.length
) {
return filteredArray.filter(
(obj) =>
features_collection.aspect_ratio_object.features.includes(
obj[features_collection.aspect_ratio_object.value]
) ||
features_collection.performance_rating_object.features.includes(
obj[features_collection.performance_rating_object.value]
)
);
}
},
Further Notes:
I can also change my object. I could change it into an array of objects if that would make things easier?
Making your filters an array seems more practical. Here's an example on how to
filter a set of objects against your feature_collection.
function filter_by_features(targets, feature_collection) {
// Start right of to filter the `filteredArray`
return targets.filter(obj => {
// go through every feature and test it against the current object.
// every() returns either true or false and the targets array is filtered
// by that condition supplied within the callback of `every()`
return feature_collection.every(filter => {
// If for a given feature no filter is available, return true
// so the test for this filter passes.
if(filter.features.length === 0) {
return true
}
// there are features, check if any applies.
return filter.features.includes(obj[filter.value])
})
})
}
Usage
// feature collection (as array)
const feature_collection = [
{
features: [],
value: "Aspect Ratio",
},
{
features: [],
value: "Performance Rating",
}
]
// the objects you want to filter.
const objects_to_filter = [/* ... */]
const filtered = filter_by_features(objects_to_filter, feature_collection)
docs
every()
You obviously have too loop through your object.
Here is your loop code for features_collection:
features_collection.forEach(function (item, index) {
console.log(item, index);
});
I have an array that contains custom objects that look like this:
{
field: fieldName,
dataType: usuallyAString,
title: titleForLocalization,
environmentLabel: environmentName
}
There are a couple of other properties on the object, but the only ones that I actually care about are field and environmentLabel. I need to filter out any objects that have identical field and environmentLabel but don't care about any other properties. The array can have objects that share field or environmentLabel, just not both.
Ideally I'd like to use Array.filter but have yet to figure out how to do it based on two properties. Also, I am limited to es5.
Create another object that contains all the combinations of properties you want to test. Use filter() and test whether the pair already exists in the object. If not, add the properties to the other object and return true.
var seen = {};
newArray = array.filter(function(obj) {
if (seen[obj.field]) {
if (seen[obj.field].includes(obj.environmentLabel) {
return false;
} else {
seen[obj.field].push(obj.environmentLabel);
}
} else {
seen[obj.field] = [obj.environmentLabel];
}
return true;
});
const data = [{
field: 1,
dataType: "usuallyAString",
title: "titleForLocalization",
environmentLabel: 1
},
{
field: 1,
dataType: "usuallyAString",
title: "titleForLocalization",
environmentLabel: 1
},
{
field: 2,
dataType: "usuallyAString",
title: "titleForLocalization",
environmentLabel: 2
}]
var result = _.uniqWith(data, function(arrVal, othVal) {
return arrVal.field=== othVal.field && arrVal.environmentLabel=== othVal.environmentLabel;
});
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
If you are able to use lodash, you can do:
var result = _.uniqWith(data, function(arrVal, othVal) {
return arrVal.field=== othVal.field && arrVal.environmentLabel=== othVal.environmentLabel;
});
console.log(result)
I want to perform a query on this collection to determine which documents have any keys in things that match a certain value. Is this possible?
I have a collection of documents like:
{
"things": {
"thing1": "red",
"thing2": "blue",
"thing3": "green"
}
}
EDIT: for conciseness
If you don't know what the keys will be and you need it to be interactive, then you'll need to use the (notoriously performance challenged) $where operator like so (in the shell):
db.test.find({$where: function() {
for (var field in this.settings) {
if (this.settings[field] == "red") return true;
}
return false;
}})
If you have a large collection, this may be too slow for your purposes, but it's your only option if your set of keys is unknown.
MongoDB 3.6 Update
You can now do this without $where by using the $objectToArray aggregation operator:
db.test.aggregate([
// Project things as a key/value array, along with the original doc
{$project: {
array: {$objectToArray: '$things'},
doc: '$$ROOT'
}},
// Match the docs with a field value of 'red'
{$match: {'array.v': 'red'}},
// Re-project the original doc
{$replaceRoot: {newRoot: '$doc'}}
])
I'd suggest a schema change so that you can actually do reasonable queries in MongoDB.
From:
{
"userId": "12347",
"settings": {
"SettingA": "blue",
"SettingB": "blue",
"SettingC": "green"
}
}
to:
{
"userId": "12347",
"settings": [
{ name: "SettingA", value: "blue" },
{ name: "SettingB", value: "blue" },
{ name: "SettingC", value: "green" }
]
}
Then, you could index on "settings.value", and do a query like:
db.settings.ensureIndex({ "settings.value" : 1})
db.settings.find({ "settings.value" : "blue" })
The change really is simple ..., as it moves the setting name and setting value to fully indexable fields, and stores the list of settings as an array.
If you can't change the schema, you could try #JohnnyHK's solution, but be warned that it's basically worst case in terms of performance and it won't work effectively with indexes.
Sadly, none of the previous answers address the fact that mongo can contain nested values in arrays or nested objects.
THIS IS THE CORRECT QUERY:
{$where: function() {
var deepIterate = function (obj, value) {
for (var field in obj) {
if (obj[field] == value){
return true;
}
var found = false;
if ( typeof obj[field] === 'object') {
found = deepIterate(obj[field], value)
if (found) { return true; }
}
}
return false;
};
return deepIterate(this, "573c79aef4ef4b9a9523028f")
}}
Since calling typeof on array or nested object will return 'object' this means that the query will iterate on all nested elements and will iterate through all of them until the key with value will be found.
You can check previous answers with a nested value and the results will be far from desired.
Stringifying the whole object is a hit on performance since it has to iterate through all memory sectors one by one trying to match them. And creates a copy of the object as a string in ram memory (both inefficient since query uses more ram and slow since function context already has a loaded object).
The query itself can work with objectId, string, int and any basic javascript type you wish.
I have an object that looks like the following:
let responseData = [
{
"name": "name",
"other": "value",
"anotherField": "blue",
"appRoles": [
{
"code": "roleOne",
"shouldDisplay": true
},
{
"code": "roleTwo",
"shouldDisplay": false
}
]
}
I need to maintain the original structure all while keeping existing properties. I only want to remove/filter out any "appRoles" where "shouldDisplay" is false.
The following works, using a forEach and a filter operation to create a new object array, but is it possible to condense this even more?
let filteredApps;
responseData.forEach((team) => {
let indyTeam = team;
indyTeam.appRoles = team.appRoles.filter((role) => role.shouldDisplay === true);
filteredApps.push(indyTeam);
});
When I use the map operation, I only get an array of the filtered appRoles - missing extra properties on each object such as "name":
let enabledAppRolesOnly =
responseData.map((team) =>
team.appRoles.filter((role) => role.shouldDisplay === true));
array.map function calls a callback for each element of your array, and then push the return value of it to a new array.
from MDN doc:
map calls a provided callback function once for each element in an array, in order, and constructs a new array from the results. callback is invoked only for indexes of the array which have assigned values, including undefined. It is not called for missing elements of the array (that is, indexes that have never been set, which have been deleted or which have never been assigned a value).
So in your case, since you return team.appRoles.filter((role) => role.displayByDefault === true) which is your team array, you only get this.
What you could do would be this (in order to fully clone the object):
let responseData = [{
"name": "name",
"appRoles": [
{
"code": "roleOne",
"shouldDisplay": true
},
{
"code": "roleTwo",
"shouldDisplay": false
}
]
}]
let enabledAppRolesOnly = responseData.map(team => {
const appRoles = team.appRoles.filter(role => role.shouldDisplay === true)
return Object.assign({}, team, { appRoles })
});
console.log(enabledAppRolesOnly)
This will achieve your objective non-destructively. It will build a new array for you.
let responseData = [{
name: "name",
appRoles: [{
code: "roleOne",
shouldDisplay: true
}, {
code: "roleTwo",
shouldDisplay: false
}]
}];
let output = responseData.map(o => Object.assign({}, o, {
appRoles: o.appRoles.filter(r => r.shouldDisplay)
}));
console.log(responseData);
console.log(output);
Code explanation -
map
The map function iterates over the whole array and modifying the each item as specified this should be self evident.
Object.assign
This could be the tricky part -
o=>Object.assign({}, o, {appRoles: o.appRoles.filter(r=>r.shouldDisplay)})
From the docs Object.assign is used to copy values from the object.
The first argument {} causes a new object to be created.
The second argument o causes all props from the object o to be copied in the newly created object.
Now, note that we need to modify the appRoles property and keep only those roles which have shouldDisplay as true. That's exactly what the third argument does. It modifies the appRoles property and gives it the new value.
filter
Now the code -
o.appRoles.filter(r=>r.shouldDisplay)
should not be too difficult.
Here we keep only those roles which meet our criterion (namely shouldDisplay should be true)
If you look at the filter function, it expects the callback value to return a boolean value on whose basis it determines whether value has to be kept or not.
So the following code is not even required,
o.appRoles.filter(r=>r.shouldDisplay===true)
This is enough,
o.appRoles.filter(r=>r.shouldDisplay)
There's some missing information in the question. I'll assume the following:
filteredApps should only contain items where there's at least one appRole for display
it is OK if responseData and filteredApps contains references to the same team objects
there are no other references to team objects that need to keep the original data unaffected
As such, you can reduce your code down to this:
let filteredApps = responseData.filter(team =>
(team.appRoles = team.appRoles.filter(role => role.shouldDisplay)).length;
);
The result will be that each team will have only the .shouldDisplay members in its appRoles, and filteredApps will only have teams with at least one appRole with shouldDisplay being true.
You could build a new array with only part who are valid chinldren elements.
let responseData = [{ name: "name", appRoles: [{ code: "roleOne", shouldDisplay: true }, { code: "roleTwo", shouldDisplay: false }] }],
result = responseData.reduce((r, a) => {
var t = a.appRoles.filter(o => o.shouldDisplay);
if (t.length) {
r.push(Object.assign({}, a, { appRoles: t }));
}
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have an underscore filter which is returning the parent object which contains a child object I am looking for. But I want it to return just the child object. Since it is already doing the work of locating the child object in order to return the parent, I'm wondering how to simplify my code to return just the child. Here's the example:
var filterObj = _.filter(filtersPath, function(obj) {
return _.where(obj.filters, {id: prefilterCat}).length > 0;
});
So here, that nested object inside obj.filters, with the id of prefilterCat, is the object I want returned, not its parent. So currently I would have to do another find inside of filterObject to get what I need. Any ideas?
Underscore's filter method will return the "parent" object but will filter out the ones that don't match the conditional statement. That being the case, if there is only 1 result, then you can just access it similarly to how you would access an array. For instance:
var filterObj = _.filter(filtersPath, function(obj) {
return _.where(obj.filters, {id: prefilterCat}).length > 0;
})[0];
The above example would get the first child that is returned from the filter method.
From your question and code, I'm assuming a data structure like this:
var filtersPath = [
{
filters: [
{id: 0},
{id: 1}
]
},
{
filters: [
{id: 5},
{id: 42}
]
}
];
Now you can get an array of all "parent objects" (which you already have done) that have a filters array containing a object with matching ID:
_.filter(filtersPath, function(obj) {
return _.find(obj.filters, { id: 5 });
});
The advantage of doing it this way is that it will stop searching for a value once it's found one, and not always traverse the entire list.
If you want to actually get an array as result, it's a simple map operation:
_.chain(filtersPath)
.filter(function(obj) {
return _.find(obj.filters, { id: 5 });
})
.map(function(obj) {
return obj.filters;
})
.value();
If you only want to get the first matching object, you don't even need to use a filter or map:
_.find(filtersPath, function(obj) {
return _.find(obj.filters, { id: 5 });
})
.filters;
With lo-dash, this operation will be a little easier:
_.find(filtersPath, { filters: [{ id: 5 }] }).filters