Related
I am trying to write a recursive function that essentially drills down an object looking for a specific key. Once that key is found I would like to rename that object key and keep the original contents and continue to iterate through that found key's value (array of objects) looking for the same key if present.
const data = {
"name": "lorem",
"folders": [
{
"name": "ipsum",
"folders": [
{
"name": "inner ipsum",
"folders": [
{
"name": "first nested"
},
{
"name": "second nested"
}
]
}
]
},
{
"name": "dolor",
"folders": [
{
"name": "first inner dolor"
},
{
"name": "second inner dolor",
"folders": [
{
"name": "test"
}
]
}
]
}
]
}
// recursive function that looks for key === "folders" within obj and renames to "items"
// continues to search within "items" value for the same "folders" key to repeat the process of // renaming the obj
const recursivelyGetAllFolders = (obj) => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === "folders") {
// keep contents of folders obj, but rename key to "items"
obj["items"] = obj["folders"];
// call function recursively to iterate through again?
if (obj["items"] && typeof obj["items"] === "object") {
const result = recursivelyGetAllFolders(folder["items"]);
if (result) {
return result;
}
} else {
return obj;
}
}
}
}
}
recursivelyGetAllFolders(data);
You don't have to loop through the keys of object. You just need to
check if the object has the folders key and rename it to items.
And then recursively call the function on each object in items (You are recursively calling on the items array itself instead of each object in the array)
function recursivelyUpdateFolders(obj) {
if (obj.hasOwnProperty("folders")) {
obj.items = obj.folders
delete obj.folders
obj.items?.forEach(recursivelyUpdateFolders)
}
return obj
}
Here's a snippet:
const data={name:"lorem",folders:[{name:"ipsum",folders:[{name:"inner ipsum",folders:[{name:"first nested"},{name:"second nested"}]}]},{name:"dolor",folders:[{name:"first inner dolor"},{name:"second inner dolor",folders:[{name:"test"}]}]}]};
function recursivelyUpdateFolders(obj) {
if (obj.hasOwnProperty("folders")) {
obj.items = obj.folders
delete obj.folders
obj.items?.forEach(recursivelyUpdateFolders)
}
return obj
}
console.log(
recursivelyUpdateFolders(data)
)
We can take one step back and make "folders" and "items" parameters to a function. It won't make our code any more difficult, but will make it more flexible.
This version does not mutate your original (we're not barbarians, right?), but returns a copy with the changes applied.
const renameNestedKey = (source, target) => (o) =>
Array .isArray (o)
? o .map (renameNestedKey (source, target))
: Object (o) === o
? Object .fromEntries (Object .entries (o) .map (
([k, v]) => [k == source ? target : k, renameNestedKey (source, target) (v)]
))
: o
const data = {name: "lorem", folders: [{name: "ipsum", folders: [{name: "inner ipsum", folders: [{name: "first nested"}, {name: "second nested"}]}]}, {name: "dolor", folders: [{name: "first inner dolor"}, {name: "second inner dolor", folders: [{name: "test"}]}]}]}
console .log (renameNestedKey ('folders', 'items') (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
Our function takes source and target key names and returns a function that takes an input object. When the input is an array, it maps itself over that array. When the input is an object, we tear it apart into key-value pairs, then map over those, converting the key when necessary, and recurring on the value, then puts the resulting set of key-value pairs back into an object. When the input is neither an array nor an object, we simply return it.
Let's consider I have the following plain object:
var numberedList = {
"1": "Text of 1",
"1.1": ["array of 1.1"],
"1.2": {
key: "object of 1.2"
},
"1.1.1": 999,
"1.2.1": true,
"1.2.2": function () {
return "function of 1.2.2";
},
"1.3": null,
"2": 2
}
I want to accomplish the following multidimensional array:
To easy explain myself:
[
["1",
[
[
[
"1.1",
[
"1.1.1"
]
]
],
[
[
"1.2",
[
"1.2.1",
"1.2.2"
]
],
],
"1.3"
]
],
"2"
]
Final array:
[
[
"Text of 1",
[
[
[
["array of 1.1"],
[
999
]
]
],
[
[
{
key: "object of 1.2"
},
[
true,
function()
{
return "function of 1.2.2";
}
]
],
],
null
]
],
2
]
Note that the deepest elements are not wrapped into an array.
How can I do a generic recursion function to accomplish this in pure vanilla js?
The main objective is to perform a tree of console.group() calls such as the console.groupEnd()would be also called on the end of each tree branch. It would be a bonus to help me to accomplish this also – given the object on the top execute these console calls.
Here is my solution,
var numberedList = { "1": "Text of 1", "1.1": ["array of 1.1"], "1.2": { key: "object of 1.2" }, "1.1.1": 999, "1.2.1": true, "1.2.2": function () { return "function of 1.2.2"; }, "1.3": null, "2": 2 }
function gen(numberedList) {
let result = Object.keys(numberedList).reduce((res, key) => {
let indexs = key.split(".");
let lastindex = indexs.pop()
indexs.reduce((res, i) => res[i], res)[lastindex] = [key]
return res
}, []);
result.shift(); // Removing first item, Becouse index count from 0;
return result
}
console.log(gen(numberedList))
You need to create a trimer function, to trim this result, for removing brackets ([])
First of all, in the future, please include your attempted code, or at least one relevant version of it, in the question. It's not a problem to refer to JSFiddle or the like for additional context, but the question should be complete enough without it.
Here I use a generic helpers, setPath, which takes a path such as ['foo', 0, 'bar', 'baz'], a new result, say, 'new result', and creates an updated copy of a nested object/array with the value at that path set to the new value:
setPath
(['foo', 1, 'bar', 'baz'])
("new result")
({foo: [{bar: {qux: 3}}, {bar: {qux: 5}}], corge: 8})
//=> {foo: [{bar: {qux: 3}}, {bar: {qux: 5, baz: "new result"}}], corge: 8}
// ^^^^^^^^^^^^^^^^^
(Here integers represent array indices and strings represent object keys.)
Then it's a simple matter of converting your input structure into an array of path-value objects, and reduce-ing that by calling setPath with each of them on a target array.
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const nest = (o) =>
Object .entries (o) .map (
([k, v]) => [k .split ('.') .map (Number), v]
) .reduce ((a, [p, v]) => setPath (p) ([[v]]) (a), []) .slice (1)
const numberedList = {"1": "Text of 1", "1.1": ["array of 1.1"], "1.2": {key: "object of 1.2"}, "1.1.1": 999, "1.2.1": true, "1.2.2": function () {return "function of 1.2.2";}, "1.3": null, "2": 2}
console .log (nest (numberedList))
.as-console-wrapper {max-height: 100% !important; top: 0}
This isn't precisely the output you requested. But I don't make much sense out of that output. You might have to tweak a bit to get it exactly how you want.
Let's say I have nested objects, like:
var obj = {
"items":[
{
"name":"Item 1",
"value": "500",
"options": [{...},{...}]
},
{
"name":"Item 2",
"value": "300",
"options": [{...},{...}]
}
],
"name": "Category",
"options": [{...},{...}]
};
I want to remove the options property from any level deep from all the objects. Objects can be nested within objects, and arrays as well.
We're currently using Lodash in the project, but I'm curious about any solutions.
There is no straight forward way to achieve this, however you can use this below function to remove a key from JSON.
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
} else if (i == key) {
delete obj[key];
}
}
return obj;
}
and use it like
var newObject = filterObject(old_json, "option");
Modifying the above solution, To delete "dataID" which appears multiple times in my JSON . mentioned below code works fine.
var candidate = {
"__dataID__": "Y2FuZGlkYXRlOjkuOTI3NDE5MDExMDU0Mjc2",
"identity": {
"__dataID__": "aWRlbnRpdHk6NjRmcDR2cnhneGE3NGNoZA==",
"name": "Sumanth Suvarnas"
},
};
candidate = removeProp(candidate, "__dataID__")
console.log(JSON.stringify(candidate, undefined, 2));
function removeProp(obj, propToDelete) {
for (var property in obj) {
if (typeof obj[property] == "object") {
delete obj.property
let newJsonData= this.removeProp(obj[property], propToDelete);
obj[property]= newJsonData
} else {
if (property === propToDelete) {
delete obj[property];
}
}
}
return obj
}
A little modification of void's answer that allows for deletion of propertise which are also objects
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (i == key) {
delete obj[key];
} else if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
}
}
return obj;
}
We now use object-scan for data processing tasks like this. It's very powerful once you wrap your head around it. Here is how you'd answer your questions
// const objectScan = require('object-scan');
const prune = (input) => objectScan(['**.options'], {
rtn: 'count',
filterFn: ({ parent, property }) => {
delete parent[property];
}
})(input);
const obj = { items: [{ name: 'Item 1', value: '500', options: [{}, {}] }, { name: 'Item 2', value: '300', options: [{}, {}] }], name: 'Category', options: [{}, {}] };
console.log(prune(obj));
// => 3
console.log(obj);
// => { items: [ { name: 'Item 1', value: '500' }, { name: 'Item 2', value: '300' } ], name: 'Category' }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
I had similar issues. So, I developed the following library. Please see the source code of the library on GitHub, and you can download it using npm.
You can use the function removePropertiesDeeply together with isInitialized as below to remove all non-initialized properties (such as an empty object, empty array, empty string, or non-finite number).
const {removePropertiesDeeply, isInitialized} = require("#thedolphinos/utility4js");
const object = {
a: null,
b: "",
x: {
a: null,
b: ""
},
y: [],
z: [
null,
"",
{a: null, b: ""},
[[{a: {b: null, c: ""}}]],
"abc"
]
};
removePropertiesDeeply(object, (x) => !isInitialized(x));
console.log(JSON.stringify(object)); // See that the object becomes {"z":["abc"]}.
I had a similar issue and I got it resolved. I hope my solution might be helpful to someone.
I use Es6 ... spread operator to do a shallow copy of an object and made null to property I was not interested.
const newObject = {
...obj.items,
...obj.name,
options: null // option property will be null.
}
function omit(source) {
return isArray(source)
? source.map(omit)
: isObject(source)
? (({ options, ...rst }) => mapValues(rst, omit))(source)
: source;
}
as with lodash, that's an easy thing, also you can specify the key via an param like this
function omit(source, omitKey) {
return isArray(source)
? source.map(partialRight(omit,omitKey)))
: isObject(source)
? (({[omitKey]: _, ...rst }) => mapValues(rst, partialRight(omit,omitKey)))(source)
: source;
}
You can remove properties given a condition using following function:
// Warning: this function mutates original object
const removeProperties = (obj, condition = (key, value) => false) => {
for (var key in obj) {
const value = obj[key]
if (!obj.hasOwnProperty(key)) continue
if (typeof obj[key] === "object") {
removeProperties(obj[key], condition)
} else if (condition(key, value)) {
delete obj[key]
}
}
return obj
}
Examples:
// Remove all properties where key is equal to 'options'
removeProperties(someObject, (key, value) => key === 'options'))
// Remove all properties where key starts with 'ignore_'
removeProperties(someObject, (key, value) => key.startsWith('ignore_'))
// Remove all properties where value is null
removeProperties(someObject, (key, value) => value === null))
Is there a better way other than looping to find data in JSON? It's for edit and delete.
for(var k in objJsonResp) {
if (objJsonResp[k].txtId == id) {
if (action == 'delete') {
objJsonResp.splice(k,1);
} else {
objJsonResp[k] = newVal;
}
break;
}
}
The data is arranged as list of maps.
Like:
[
{id:value, pId:value, cId:value,...},
{id:value, pId:value, cId:value,...},
...
]
(You're not searching through "JSON", you're searching through an array -- the JSON string has already been deserialized into an object graph, in this case an array.)
Some options:
Use an Object Instead of an Array
If you're in control of the generation of this thing, does it have to be an array? Because if not, there's a much simpler way.
Say this is your original data:
[
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
Could you do the following instead?
{
"one": {"pId": "foo1", "cId": "bar1"},
"two": {"pId": "foo2", "cId": "bar2"},
"three": {"pId": "foo3", "cId": "bar3"}
}
Then finding the relevant entry by ID is trivial:
id = "one"; // Or whatever
var entry = objJsonResp[id];
...as is updating it:
objJsonResp[id] = /* New value */;
...and removing it:
delete objJsonResp[id];
This takes advantage of the fact that in JavaScript, you can index into an object using a property name as a string -- and that string can be a literal, or it can come from a variable as with id above.
Putting in an ID-to-Index Map
(Dumb idea, predates the above. Kept for historical reasons.)
It looks like you need this to be an array, in which case there isn't really a better way than searching through the array unless you want to put a map on it, which you could do if you have control of the generation of the object. E.g., say you have this originally:
[
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
The generating code could provide an id-to-index map:
{
"index": {
"one": 0, "two": 1, "three": 2
},
"data": [
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
}
Then getting an entry for the id in the variable id is trivial:
var index = objJsonResp.index[id];
var obj = objJsonResp.data[index];
This takes advantage of the fact you can index into objects using property names.
Of course, if you do that, you have to update the map when you modify the array, which could become a maintenance problem.
But if you're not in control of the generation of the object, or updating the map of ids-to-indexes is too much code and/ora maintenance issue, then you'll have to do a brute force search.
Brute Force Search (corrected)
Somewhat OT (although you did ask if there was a better way :-) ), but your code for looping through an array is incorrect. Details here, but you can't use for..in to loop through array indexes (or rather, if you do, you have to take special pains to do so); for..in loops through the properties of an object, not the indexes of an array. Your best bet with a non-sparse array (and yours is non-sparse) is a standard old-fashioned loop:
var k;
for (k = 0; k < someArray.length; ++k) { /* ... */ }
or
var k;
for (k = someArray.length - 1; k >= 0; --k) { /* ... */ }
Whichever you prefer (the latter is not always faster in all implementations, which is counter-intuitive to me, but there we are). (With a sparse array, you might use for..in but again taking special pains to avoid pitfalls; more in the article linked above.)
Using for..in on an array seems to work in simple cases because arrays have properties for each of their indexes, and their only other default properties (length and their methods) are marked as non-enumerable. But it breaks as soon as you set (or a framework sets) any other properties on the array object (which is perfectly valid; arrays are just objects with a bit of special handling around the length property).
I had come across this issue for a complex model with several nested objects. A good example of what I was looking at doing would be this: Lets say you have a polaroid of yourself. And that picture is then put into a trunk of a car. The car is inside of a large crate. The crate is in the hold of a large ship with many other crates. I had to search the hold, look in the crates, check the trunk, and then look for an existing picture of me.
I could not find any good solutions online to use, and using .filter() only works on arrays. Most solutions suggested just checking to see if model["yourpicture"] existed. This was very undesirable because, from the example, that would only search the hold of the ship and I needed a way to get them from farther down the rabbit hole.
This is the recursive solution I made. In comments, I confirmed from T.J. Crowder that a recursive version would be required. I thought I would share it in case anyone came across a similar complex situation.
function ContainsKeyValue( obj, key, value ){
if( obj[key] === value ) return true;
for( all in obj )
{
if( obj[all] != null && obj[all][key] === value ){
return true;
}
if( typeof obj[all] == "object" && obj[all]!= null ){
var found = ContainsKeyValue( obj[all], key, value );
if( found == true ) return true;
}
}
return false;
}
This will start from a given object inside of the graph, and recurse down any objects found. I use it like this:
var liveData = [];
for( var items in viewmodel.Crates )
{
if( ContainsKeyValue( viewmodel.Crates[items], "PictureId", 6 ) === true )
{
liveData.push( viewmodel.Crates[items] );
}
}
Which will produce an array of the Crates which contained my picture.
Zapping - you can use this javascript lib; DefiantJS. There is no need to restructure JSON data into objects to ease searching. Instead, you can search the JSON structure with an XPath expression like this:
var data = [
{
"id": "one",
"pId": "foo1",
"cId": "bar1"
},
{
"id": "two",
"pId": "foo2",
"cId": "bar2"
},
{
"id": "three",
"pId": "foo3",
"cId": "bar3"
}
],
res = JSON.search( data, '//*[id="one"]' );
console.log( res[0].cId );
// 'bar1'
DefiantJS extends the global object JSON with a new method; "search" which returns array with the matches (empty array if none were found). You can try it out yourself by pasting your JSON data and testing different XPath queries here:
http://www.defiantjs.com/#xpath_evaluator
XPath is, as you know, a standardised query language.
If the JSON data in your array is sorted in some way, there are a variety of searches you could implement. However, if you're not dealing with a lot of data then you're probably going to be fine with an O(n) operation here (as you have). Anything else would probably be overkill.
Ok. So, I know this is an old post, but perhaps this can help someone else. This is not backwards compatible, but that's almost irrelevant since Internet Explorer is being made redundant.
Easiest way to do exactly what is wanted:
function findInJson(objJsonResp, key, value, aType){
if(aType=="edit"){
return objJsonResp.find(x=> x[key] == value);
}else{//delete
var a =objJsonResp.find(x=> x[key] == value);
objJsonResp.splice(objJsonResp.indexOf(a),1);
}
}
It will return the item you want to edit if you supply 'edit' as the type. Supply anything else, or nothing, and it assumes delete. You can flip the conditionals if you'd prefer.
General Solution
We use object-scan for a lot of data processing. It has some nice properties, especially traversing in delete safe order. Here is how one could implement find, delete and replace for your question.
// const objectScan = require('object-scan');
const tool = (() => {
const scanner = objectScan(['[*]'], {
abort: true,
rtn: 'bool',
filterFn: ({
value, parent, property, context
}) => {
if (value.id === context.id) {
context.fn({ value, parent, property });
return true;
}
return false;
}
});
return {
add: (data, id, obj) => scanner(data, { id, fn: ({ parent, property }) => parent.splice(property + 1, 0, obj) }),
del: (data, id) => scanner(data, { id, fn: ({ parent, property }) => parent.splice(property, 1) }),
mod: (data, id, prop, v = undefined) => scanner(data, {
id,
fn: ({ value }) => {
if (value !== undefined) {
value[prop] = v;
} else {
delete value[prop];
}
}
})
};
})();
// -------------------------------
const data = [ { id: 'one', pId: 'foo1', cId: 'bar1' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ];
const toAdd = { id: 'two', pId: 'foo2', cId: 'bar2' };
const exec = (fn) => {
console.log('---------------');
console.log(fn.toString());
console.log(fn());
console.log(data);
};
exec(() => tool.add(data, 'one', toAdd));
exec(() => tool.mod(data, 'one', 'pId', 'zzz'));
exec(() => tool.mod(data, 'one', 'other', 'test'));
exec(() => tool.mod(data, 'one', 'gone', 'delete me'));
exec(() => tool.mod(data, 'one', 'gone'));
exec(() => tool.del(data, 'three'));
// => ---------------
// => () => tool.add(data, 'one', toAdd)
// => true
// => [ { id: 'one', pId: 'foo1', cId: 'bar1' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'pId', 'zzz')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'other', 'test')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'gone', 'delete me')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: 'delete me' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'gone')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: undefined }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.del(data, 'three')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: undefined }, { id: 'two', pId: 'foo2', cId: 'bar2' } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan
If you are doing this in more than one place in your application it would make sense to use a client-side JSON database because creating custom search functions is messy and less maintainable than the alternative.
Check out ForerunnerDB which provides you with a very powerful client-side JSON database system and includes a very simple query language to help you do exactly what you are looking for:
// Create a new instance of ForerunnerDB and then ask for a database
var fdb = new ForerunnerDB(),
db = fdb.db('myTestDatabase'),
coll;
// Create our new collection (like a MySQL table) and change the default
// primary key from "_id" to "id"
coll = db.collection('myCollection', {primaryKey: 'id'});
// Insert our records into the collection
coll.insert([
{"name":"my Name","id":12,"type":"car owner"},
{"name":"my Name2","id":13,"type":"car owner2"},
{"name":"my Name4","id":14,"type":"car owner3"},
{"name":"my Name4","id":15,"type":"car owner5"}
]);
// Search the collection for the string "my nam" as a case insensitive
// regular expression - this search will match all records because every
// name field has the text "my Nam" in it
var searchResultArray = coll.find({
name: /my nam/i
});
console.log(searchResultArray);
/* Outputs
[
{"name":"my Name","id":12,"type":"car owner"},
{"name":"my Name2","id":13,"type":"car owner2"},
{"name":"my Name4","id":14,"type":"car owner3"},
{"name":"my Name4","id":15,"type":"car owner5"}
]
*/
Disclaimer: I am the developer of ForerunnerDB.
Is there a better way other than looping to find data in JSON? It's for edit and delete.
for(var k in objJsonResp) {
if (objJsonResp[k].txtId == id) {
if (action == 'delete') {
objJsonResp.splice(k,1);
} else {
objJsonResp[k] = newVal;
}
break;
}
}
The data is arranged as list of maps.
Like:
[
{id:value, pId:value, cId:value,...},
{id:value, pId:value, cId:value,...},
...
]
(You're not searching through "JSON", you're searching through an array -- the JSON string has already been deserialized into an object graph, in this case an array.)
Some options:
Use an Object Instead of an Array
If you're in control of the generation of this thing, does it have to be an array? Because if not, there's a much simpler way.
Say this is your original data:
[
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
Could you do the following instead?
{
"one": {"pId": "foo1", "cId": "bar1"},
"two": {"pId": "foo2", "cId": "bar2"},
"three": {"pId": "foo3", "cId": "bar3"}
}
Then finding the relevant entry by ID is trivial:
id = "one"; // Or whatever
var entry = objJsonResp[id];
...as is updating it:
objJsonResp[id] = /* New value */;
...and removing it:
delete objJsonResp[id];
This takes advantage of the fact that in JavaScript, you can index into an object using a property name as a string -- and that string can be a literal, or it can come from a variable as with id above.
Putting in an ID-to-Index Map
(Dumb idea, predates the above. Kept for historical reasons.)
It looks like you need this to be an array, in which case there isn't really a better way than searching through the array unless you want to put a map on it, which you could do if you have control of the generation of the object. E.g., say you have this originally:
[
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
The generating code could provide an id-to-index map:
{
"index": {
"one": 0, "two": 1, "three": 2
},
"data": [
{"id": "one", "pId": "foo1", "cId": "bar1"},
{"id": "two", "pId": "foo2", "cId": "bar2"},
{"id": "three", "pId": "foo3", "cId": "bar3"}
]
}
Then getting an entry for the id in the variable id is trivial:
var index = objJsonResp.index[id];
var obj = objJsonResp.data[index];
This takes advantage of the fact you can index into objects using property names.
Of course, if you do that, you have to update the map when you modify the array, which could become a maintenance problem.
But if you're not in control of the generation of the object, or updating the map of ids-to-indexes is too much code and/ora maintenance issue, then you'll have to do a brute force search.
Brute Force Search (corrected)
Somewhat OT (although you did ask if there was a better way :-) ), but your code for looping through an array is incorrect. Details here, but you can't use for..in to loop through array indexes (or rather, if you do, you have to take special pains to do so); for..in loops through the properties of an object, not the indexes of an array. Your best bet with a non-sparse array (and yours is non-sparse) is a standard old-fashioned loop:
var k;
for (k = 0; k < someArray.length; ++k) { /* ... */ }
or
var k;
for (k = someArray.length - 1; k >= 0; --k) { /* ... */ }
Whichever you prefer (the latter is not always faster in all implementations, which is counter-intuitive to me, but there we are). (With a sparse array, you might use for..in but again taking special pains to avoid pitfalls; more in the article linked above.)
Using for..in on an array seems to work in simple cases because arrays have properties for each of their indexes, and their only other default properties (length and their methods) are marked as non-enumerable. But it breaks as soon as you set (or a framework sets) any other properties on the array object (which is perfectly valid; arrays are just objects with a bit of special handling around the length property).
I had come across this issue for a complex model with several nested objects. A good example of what I was looking at doing would be this: Lets say you have a polaroid of yourself. And that picture is then put into a trunk of a car. The car is inside of a large crate. The crate is in the hold of a large ship with many other crates. I had to search the hold, look in the crates, check the trunk, and then look for an existing picture of me.
I could not find any good solutions online to use, and using .filter() only works on arrays. Most solutions suggested just checking to see if model["yourpicture"] existed. This was very undesirable because, from the example, that would only search the hold of the ship and I needed a way to get them from farther down the rabbit hole.
This is the recursive solution I made. In comments, I confirmed from T.J. Crowder that a recursive version would be required. I thought I would share it in case anyone came across a similar complex situation.
function ContainsKeyValue( obj, key, value ){
if( obj[key] === value ) return true;
for( all in obj )
{
if( obj[all] != null && obj[all][key] === value ){
return true;
}
if( typeof obj[all] == "object" && obj[all]!= null ){
var found = ContainsKeyValue( obj[all], key, value );
if( found == true ) return true;
}
}
return false;
}
This will start from a given object inside of the graph, and recurse down any objects found. I use it like this:
var liveData = [];
for( var items in viewmodel.Crates )
{
if( ContainsKeyValue( viewmodel.Crates[items], "PictureId", 6 ) === true )
{
liveData.push( viewmodel.Crates[items] );
}
}
Which will produce an array of the Crates which contained my picture.
Zapping - you can use this javascript lib; DefiantJS. There is no need to restructure JSON data into objects to ease searching. Instead, you can search the JSON structure with an XPath expression like this:
var data = [
{
"id": "one",
"pId": "foo1",
"cId": "bar1"
},
{
"id": "two",
"pId": "foo2",
"cId": "bar2"
},
{
"id": "three",
"pId": "foo3",
"cId": "bar3"
}
],
res = JSON.search( data, '//*[id="one"]' );
console.log( res[0].cId );
// 'bar1'
DefiantJS extends the global object JSON with a new method; "search" which returns array with the matches (empty array if none were found). You can try it out yourself by pasting your JSON data and testing different XPath queries here:
http://www.defiantjs.com/#xpath_evaluator
XPath is, as you know, a standardised query language.
If the JSON data in your array is sorted in some way, there are a variety of searches you could implement. However, if you're not dealing with a lot of data then you're probably going to be fine with an O(n) operation here (as you have). Anything else would probably be overkill.
Ok. So, I know this is an old post, but perhaps this can help someone else. This is not backwards compatible, but that's almost irrelevant since Internet Explorer is being made redundant.
Easiest way to do exactly what is wanted:
function findInJson(objJsonResp, key, value, aType){
if(aType=="edit"){
return objJsonResp.find(x=> x[key] == value);
}else{//delete
var a =objJsonResp.find(x=> x[key] == value);
objJsonResp.splice(objJsonResp.indexOf(a),1);
}
}
It will return the item you want to edit if you supply 'edit' as the type. Supply anything else, or nothing, and it assumes delete. You can flip the conditionals if you'd prefer.
General Solution
We use object-scan for a lot of data processing. It has some nice properties, especially traversing in delete safe order. Here is how one could implement find, delete and replace for your question.
// const objectScan = require('object-scan');
const tool = (() => {
const scanner = objectScan(['[*]'], {
abort: true,
rtn: 'bool',
filterFn: ({
value, parent, property, context
}) => {
if (value.id === context.id) {
context.fn({ value, parent, property });
return true;
}
return false;
}
});
return {
add: (data, id, obj) => scanner(data, { id, fn: ({ parent, property }) => parent.splice(property + 1, 0, obj) }),
del: (data, id) => scanner(data, { id, fn: ({ parent, property }) => parent.splice(property, 1) }),
mod: (data, id, prop, v = undefined) => scanner(data, {
id,
fn: ({ value }) => {
if (value !== undefined) {
value[prop] = v;
} else {
delete value[prop];
}
}
})
};
})();
// -------------------------------
const data = [ { id: 'one', pId: 'foo1', cId: 'bar1' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ];
const toAdd = { id: 'two', pId: 'foo2', cId: 'bar2' };
const exec = (fn) => {
console.log('---------------');
console.log(fn.toString());
console.log(fn());
console.log(data);
};
exec(() => tool.add(data, 'one', toAdd));
exec(() => tool.mod(data, 'one', 'pId', 'zzz'));
exec(() => tool.mod(data, 'one', 'other', 'test'));
exec(() => tool.mod(data, 'one', 'gone', 'delete me'));
exec(() => tool.mod(data, 'one', 'gone'));
exec(() => tool.del(data, 'three'));
// => ---------------
// => () => tool.add(data, 'one', toAdd)
// => true
// => [ { id: 'one', pId: 'foo1', cId: 'bar1' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'pId', 'zzz')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'other', 'test')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'gone', 'delete me')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: 'delete me' }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.mod(data, 'one', 'gone')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: undefined }, { id: 'two', pId: 'foo2', cId: 'bar2' }, { id: 'three', pId: 'foo3', cId: 'bar3' } ]
// => ---------------
// => () => tool.del(data, 'three')
// => true
// => [ { id: 'one', pId: 'zzz', cId: 'bar1', other: 'test', gone: undefined }, { id: 'two', pId: 'foo2', cId: 'bar2' } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan
If you are doing this in more than one place in your application it would make sense to use a client-side JSON database because creating custom search functions is messy and less maintainable than the alternative.
Check out ForerunnerDB which provides you with a very powerful client-side JSON database system and includes a very simple query language to help you do exactly what you are looking for:
// Create a new instance of ForerunnerDB and then ask for a database
var fdb = new ForerunnerDB(),
db = fdb.db('myTestDatabase'),
coll;
// Create our new collection (like a MySQL table) and change the default
// primary key from "_id" to "id"
coll = db.collection('myCollection', {primaryKey: 'id'});
// Insert our records into the collection
coll.insert([
{"name":"my Name","id":12,"type":"car owner"},
{"name":"my Name2","id":13,"type":"car owner2"},
{"name":"my Name4","id":14,"type":"car owner3"},
{"name":"my Name4","id":15,"type":"car owner5"}
]);
// Search the collection for the string "my nam" as a case insensitive
// regular expression - this search will match all records because every
// name field has the text "my Nam" in it
var searchResultArray = coll.find({
name: /my nam/i
});
console.log(searchResultArray);
/* Outputs
[
{"name":"my Name","id":12,"type":"car owner"},
{"name":"my Name2","id":13,"type":"car owner2"},
{"name":"my Name4","id":14,"type":"car owner3"},
{"name":"my Name4","id":15,"type":"car owner5"}
]
*/
Disclaimer: I am the developer of ForerunnerDB.