Traverse a javascript object recursively and replace a value by key - javascript

Since JSON can not perfom functions i need to eval a JSON string by a key flag in the JSON object. I want to mutate the JSON data when it's in Object form.
I can't find a function/method online that can give me the full path to key based on a known key pattern.
Given this data:
{
"js:bindto": "#chart-1", // note the key here 'js:'
"point": {
"r": 5
},
"data": {
"x": "x",
"xFormat": "%Y",
"columns": [
...
],
"colors": {
"company": "#ed1b34",
"trendline": "#ffffff"
}
},
"legend": {
"show": false
},
"axis": {
"x": {
"padding": {
"left": 0
},
"type": "timeseries",
"tick": {
"format": "%Y",
"outer": false
}
},
"y": {
"tick": {
"outer": false,
"js:format": "d3.format(\"$\")" // note the key here 'js:'
}
}
},
"grid": {
"lines": {
"front": false
},
"y": {
"lines": [...]
}
}
}
The flags are keys beginning with js:.
If I look up js:format, I'd expect it's path to be something like: /js:bindto and /axis/y/tick/js:format. Open to suggestions.
In context:
mutateGraphData<T>(data:T):T {
// data here is a parsed JSON string. ( an object as shown above )
let jsonKeys = this.findKeysInJSON(JSON.stringify(data), "js:");
// jsonKeys = ["js:bindto","js:format"]
jsonKeys.map((key:string) => {
// find the key in data, then modify the value. (stuck here)
// i need the full path to the key so i can change the data property that has the key in question
});
});
return data;
}
findKeysInJSON<T>(jsonString:string, key:string):Array<T> {
let keys = [];
if (Boolean(~(jsonString.indexOf(`"${key}`)))) {
let regEx = new RegExp(key + "(\\w|\\-)+", "g");
keys = jsonString.match(regEx);
}
return keys;
}
I have been around a few npm packages:
object search no wild card lookaround
dotty looks good, but fails with search: "*.js:format"
https://github.com/capaj/object-resolve-path - needs full path to key. I don't know that in advance.
I have looked at
JavaScript recursive search in JSON object
Find property by name in a deep object
DefiantJS ( no npm module )
https://gist.github.com/iwek/3924925
Nothing have I seen can return the full path to the key in question so I can modify it, nor work directly on the object itself so I can change its properties.

You could go with Ramda. It has built in functions that will allow you to map over an object and modify parts of the object in a completely immutable way.
Ramda offers R.lensPath that will allow you to dig into the object, and modify it as needed. It doesn't follow the pattern you want, but we can quickly patch it with the lensify function.
It is using the R.set function to set the node to the passed in value, and creating a pipeline that will run all the operations on the passed in object.
You can do some really cool stuff with ramda and lenses. Checkout evilsoft on livecoding.tv for a really good overview.
const obj={"js:bindto":"#chart-1",point:{r:5},data:{x:"x",xFormat:"%Y",columns:[],colors:{company:"#ed1b34",trendline:"#ffffff"}},legend:{show:!1},axis:{x:{padding:{left:0},type:"timeseries",tick:{format:"%Y",outer:!1}},y:{tick:{outer:!1,"js:format":'d3.format("$")'}}},grid:{lines:{front:!1},y:{lines:[]}}}
const lensify = path => R.lensPath(path.split('/'))
// create the property accessors split by /
const bindToLens = lensify('js:bindto')
const formatLens = lensify('axis/y/tick/js:format')
const modifyObj = R.pipe(
R.set(bindToLens, 'dis be bind'),
R.set(formatLens, 'I been here')
)
console.log(modifyObj(obj))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>

After doing a look around I modified the answer from Javascript/JSON get path to given subnode? #adam-rackis
function search(obj, target, path = "") {
for (var k in obj) {
if (obj.hasOwnProperty(k))
if (k === target)
return path + "." + k;
else if (typeof obj[k] === "object") {
var result = search( obj[k], target, path + "." + k );
if (result){
return result;
}
}
}
return false;
}
search(data,"js:format").slice(1); // returns: axis.y.tick.js:format
search(data,"js:bindto").slice(1); // returns: js:bindto
Now I can use dotty or object-resolve-path or plain ole split('.') to resolve the path to the object.

With object-scan this should be a one liner. We've been using it a lot for data processing and it's powerful once you wrap your head around it. Here is how you could use it
// const objectScan = require('object-scan');
const find = (data) => objectScan(['**.js:*'], { joined: true })(data);
const data = { 'js:bindto': '#chart-1', point: { r: 5 }, data: { x: 'x', xFormat: '%Y', columns: [], colors: { company: '#ed1b34', trendline: '#ffffff' } }, legend: { show: false }, axis: { x: { padding: { left: 0 }, type: 'timeseries', tick: { format: '%Y', outer: false } }, y: { tick: { outer: false, 'js:format': 'd3.format("$")' } } }, grid: { lines: { front: false }, y: { lines: [] } } };
console.log(find(data));
// => [ 'axis.y.tick.js:format', 'js:bindto' ]
.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

Related

How to rename object key based on a string of both properties and indexes generated by JSONPath

Sorry for the potentially confusing question. What I mean is this:
I've been tasked to rename a specific set of object keys on a massively nested JSON object I've converted from XML. These keys are scattered throughout the nested object, buried under both arrays and objects. Let me show you what I mean:
const obj = {
"Zone": [
{
ExtWall: [
{ Win: "10" }
],
HVACDist: {
HVACCool: [
{ Win: "10" }
]
}
}
]
};
In this case, I need to rename the key Win to ResWin. However, this tag Win is scattered across a nested object far deeper than this.
I'm currently using JSONPath to find the path of these keys. For example:
var WinPath = jp.paths(object, '$..Win');
// Returns - [
// [ '$', 'Zone', 0, 'ExtWall', 0, 'Win' ],
// [ '$', 'Zone', 0, 'HVACDist', 'HVACCool', 0, 'Win' ]
// ]
JSONPath also has a stringify function which generates a path based on the paths array it produces. Let's focus on one element for now:
const pathStringified = jp.stringify(WinPath[0])
// Returns - $['Zone'][0]['ExtWall][0]['Win']
Somehow, I need to rename this Win key to ResWin.
Right now my best guess is to replace the $ with an empty string so I can access the objec for key replacement.
My goal:
obj['Zone'][0]['ExtWall][0]['ResWin'] = obj['Zone'][0]['ExtWall][0]['Win'];
delete obj['Zone'][0]['ExtWall][0]['Win'];
Any feedback or suggestions would be much appreciated.
Also, let me know how to restructure my question because I can understand how it would be difficult to understand.
Sounds like you're ok using a library, so here is a solution using object-scan.
.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan#18.3.11/lib/index.min.js';
const obj = { Zone: [{ ExtWall: [{ Win: '10' }], HVACDist: { HVACCool: [{ Win: '10' }] } }] };
const modify = objectScan(['**.Win'], {
filterFn: ({ parent, property, value }) => {
delete parent[property];
parent.ResWin = value;
},
rtn: 'count'
});
const r = modify(obj);
console.log(r); // number of matches
// => 2
console.log(obj);
// => { Zone: [ { ExtWall: [ { ResWin: '10' } ], HVACDist: { HVACCool: [ { ResWin: '10' } ] } } ] }
</script>
Disclaimer: I'm the author of object-scan
The syntax is very similar to the JSONPath sytax, but it provides callbacks. The code could easily be generified to allow renaming of multiple keys in a single iteration.
Note that obj is modified in place. You could do a copy before modification, if that is not desired (e.g. using lodash cloneDeep)
If you want to replace all keys with same name you can iterate the entire tree.
If you want to rename by path, basically loop the path drilling down until you are at the requested key.
const obj = {
"Zone": [{
ExtWall: [{
Win: "10"
}],
HVACDist: {
HVACCool: [{
Win: "10"
}]
}
}]
};
var WinPath = [
['$', 'Zone', 0, 'ExtWall', 0, 'Win'],
['$', 'Zone', 0, 'HVACDist', 'HVACCool', 0, 'Win']
];
function rename_by_path(obj, arr_path, new_name) {
var pointer = obj;
var dollar = arr_path.shift();
var key = arr_path.shift();
while (arr_path.length) {
pointer = pointer[key]
key = arr_path.shift();
}
pointer[new_name] = pointer[key]
delete pointer[key]
}
function rename_all(obj, key_name, new_name) {
const iterate = (obj) => {
if (!obj) {
return;
}
Object.keys(obj).forEach(key => {
var value = obj[key]
if (typeof value === "object" && value !== null) {
iterate(value)
}
if (key == key_name) {
obj[new_name] = value;
delete obj[key];
}
})
}
iterate(obj)
}
function rename_by_paths(obj, arr_paths, new_name) {
arr_paths.forEach(arr_path => rename_by_path(obj, arr_path, new_name))
}
rename_by_paths(obj, WinPath, "ResWin")
console.log(obj)
rename_all(obj, "ResWin", "ResWin2");
console.log(obj)
.as-console-wrapper {
max-height: 100% !important;
}

Rename nested key in array of objects JS

Using JS i am trying to rename canBook -> quantity, variationsEN -> variations and nested keys valueEN -> value
var prod = [{
price: 10,
canBook: 1
}, {
price: 11,
canBook: 2,
variationsEN: [{
valueEN: 1
}, {
valueEN: 2
}]
}]
I was able to rename keys, but i dont have a clue how to rename the nested ones: valueEN
prod.map(p => ({
quantity: p.canBook, variations:p.variationsEN
}))
Just apply the same trick again. Replace:
variations:p.variationsEN
with:
variations:(p.variationsEN || []).map(q => ({ value: q.valueEN }))
The additional || [] is to deal with cases where the property does not exist in your source object. In that case an empty array is produced for it.
You could take a recursiv approach with an object for the renamed properties and build new object or arrays.
function rename(value) {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(rename);
return Object.fromEntries(Object
.entries(value)
.map(([k, v]) => [keys[k] || k, rename(v)])
);
}
var keys = { canBook: 'quantity', variationsEN: 'variations', valueEN: 'value' },
prod = [{ price: 10, canBook: 1 }, { price: 11, canBook: 2, variationsEN: [{ valueEN: 1 }, { valueEN: 2 }] }],
result = rename(prod);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
If it's indeed a JSON (as your tag seems to indicate), e.g. you get it as string from a server, you can use a reviver function, such as:
var prod = JSON.parse(prodJSON, function (k, v) {
if (k === "canBook") {
this["quantity"] = v
} else {
return v
}
});
(Of course you could always stringify in case you start from a JS Object and not from a JSON string, but in that case it would be an overkill)
variations:p.variationsEN.map(q => { value: q.valueEN })

Turning 2 level JSON to multiple 1 level JSON in Node.js

I have problems with writing a universal function in node that would parse JSON like this:
{
"parserId": 1,
"filters": [
{
"filterName": "replaceTitle",
"regex": "..."
},
{
"filterName": "replaceRegion",
"regex": "..."
}
]}
into multiple JSON like this:
{ "parserId": 1, "filterName": "replaceTitle","regex": "..." },{ "parserId": 1, "filterName": "replaceRegion", "regex": "..."}
It would be great if this function would be universal so doesn't matter what are the names of the fields in JSON, as long as it's build the same way.
Is there any node package already doing it? Thank you for your help!
You could map every item of the array and append an object with parserId with the item of the array.
var object = { parserId: 1, filters: [{ filterName: "replaceTitle", regex: "..." }, { filterName: "replaceRegion", regex: "..." }] },
array = object.filters.map(o => Object.assign({ parserId: object.parserId }, o));
console.log(array);
.as-console-wrapper { max-height: 100% !important; top: 0; }
A more general approach could be the check if a property is an array and loop that array later or add the property to a common object, which keeps the same values for all new generated objects.
Later iterate the arrays and add the content to the objects as well.
This works without specifying the properties who are arrays or just values.
function unnormalize(object) {
var common = {};
return Object
.keys(object)
.filter(k => Array.isArray(object[k]) || (common[k] = object[k], false))
.reduce((r, k) => (object[k].forEach((o, i) => Object.assign(r[i] = r[i] || Object.assign({}, common), o)), r), []);
}
var object = { parserId: 1, filters: [{ filterName: "replaceTitle", regex: "..." }, { filterName: "replaceRegion", regex: "..." }] };
console.log(unnormalize(object));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use square bracket syntax to provide the name of the array. Then you can use map to transform the array, and Object.assign to merge the objects
let data = {
"parserId": 1,
"filters": [
{
"filterName": "replaceTitle",
"regex": "..."
},
{
"filterName": "replaceRegion",
"regex": "..."
}
]};
function format(data) {
let array = data.filters;
// Remove the array so that it's not duplicated
delete data.filters;
return array.map((item) => Object.assign(item, data));
}
console.log(format(data));

Check if obj exist and push to array

I want to iterate through a JSON response, check to see if key,value exists and if not, add it to the array.
$scope.InitProdToArray = function (data) {
angular.forEach($scope.data.obj.Product, function(value, index) {
if (value.Product_type != 'T' ) {
$scope.data.obj.Product.push({Product_type: 'T'});
}
if (value.Product_type != '16364NB' ) {
$scope.data.obj.Product.push({Product_type: '16364NB'});
}
if (value.Product_type != '39087NB' ) {
$scope.data.obj.Product.push({Product_type: '39087NB'});
}
if (value.Product_type != 'C' ) {
$scope.data.obj.Product.push({Product_type: 'C'});
}
if (value.Product_type != '4NB' ) {
$scope.data.obj.Product.push({Product_type: '4NB'});
}
});
JSON: $scope.data.obj.Product =
[{
"Count": 28,
"Product_type": "T"
}, {
"Count": 88,
"Product_type": "4NB"
}, {
"Count": 20,
"Product_type": "C"
}, {
"Count": 3,
"Product_type": "39087NB"
}
]
This doesn't seem to work because I'm pushing the key,value pair every time it iterates through each node. I end up getting back a JSON that has multiple product_type that is the same.
Is there a way to stop the code from adding additional key,value if it already exists?
You seem to have written your type check upside down.
instead of if (value.Product_type != 'T' ) {... I would have imagine something like if (value.Product_type == 'T' ) {... this way you would push to Product array only when the type is matching.
apart from that, you can also check before pushing if you Product array already contains a key of that type : if($scope.data.obj.Product.indexOf(value.Product_type)!==undefined)
From your code it seems like you are doing search and insert rather than things to do with key/value.
If you are supporting new browsers only there is a handy function called find for this purpose.
var productTypes = ['T', '16364NB', '39087NB', 'C', '4NB'];
angular.forEach(productTypes, function(value) {
var exists = $scope.data.obj.Product.find(function(item) {
return (item.Product_type == value);
});
if (!exists) {
$scope.data.obj.Product.push({
Product_type: value,
count: 0
});
}
});
If you need to support older browsers, you'll need to add polyfill for find as well.
var TYPES = { T: 'T', NB4: '4NB', C: 'C', NB39087: '39087NB' };
var products = [{ "Count": 28, "Product_type": "T" }, { "Count": 88, "Product_type": "4NB" }];
/* check if the element key is not the specified value....*/
function isKeyInElement(keyToCheck, element) {
return element.Product_type === keyToCheck;
}
/* checks if there are NO elements in array with the specified key ...*/
function isKeyInArray(keyToCheck, array) {
return !!array.filter(function (element) { return isKeyInElement(keyToCheck, element); }).length;
}
/* another way to check if there are NO elements in array with the specified key ...*/
function isKeyInArray2(keyToCheck, array) {
return array.map(function (element) { return element.Product_type; }).indexOf(keyToCheck) > -1;
}
function initProdToArray(source) {
if (!isKeyInArray(TYPES.T, source)) { source.push({ Product_type: TYPES.T }); }
if (!isKeyInArray(TYPES.NB4, source)) { source.push({ Product_type: TYPES.NB4 }); }
if (!isKeyInArray(TYPES.C, source)) { source.push({ Product_type: TYPES.C }); }
if (!isKeyInArray(TYPES.NB39087, source)) { source.push({ Product_type: TYPES.NB39087 }); }
}
initProdToArray(products);
console.log(products);

Get the object in an array of object with the key

So I have an array of objects and I want to get the object with the key "Z".
Obviously I can just loop through the array and check each key one by one and grab the one which matches, but I was thinking that there is probably a better way than my current approach:
for (var i = 0; i < data.length; i++) {
if (Object.keys(data[i]).toString() == "z") {
return data[i].z;
break;
}
}
My data:
"data": [
{ "X": { "foo": "bar1" } },
{ "Y": { "foo": "bar2" } },
{ "Z": { "foo": "bar3" } }
]
Desired Output:
{
"foo": "bar3"
}
Instead of an array of objects, you could replace it with an object:
"data": {
"X": { "foo": "bar1" },
"Y": { "foo": "bar2" },
"Z": { "foo": "bar3" }
}
And then access your object like so:
data['Z']
as you can see, much neater.
I'm guessing you used an array originally for easy appending and so on, but it's just as easy with an object:
data['A'] = { "foo": "bar4" };
will create key "A" in data, and you can still loop through your object using for (... in ...) loops, i.e:
for (key in data) {
console.log(data[key].foo);
}
should print
bar1
bar2
bar3
bar4
Using lodash, you could do something like:
var collection = [
{ X: { foo: 'bar1' } },
{ Y: { foo: 'bar2' } },
{ Z: { foo: 'bar3' } }
];
_(collection)
.chain()
.find(_.ary(_.partialRight(_.has, 'Z'), 1))
.values()
.first()
.value()
// → { foo: 'bar3' }
An outline of what this chain is doing:
chain(): Enables explicit chaining. Otherwise, find() will return an unwrapped object, and we still have actions to perform.
find(): We pass in a callback that checks for the existence of the Z key. The callback itself is constructed using higher-order function utilities:
ary(): Restricts the number of arguments passed to the function. We do this here because find() passes arguments that we don't care about to our callback.
partialRight(): Provides the 'Z' argument to the has() function. This is the rightmost argument, meaning that each item in the collection is passed as the first argument.
has(): Returns true if the Z key exists on an object.
values(): We don't care about the object key, only it's values.
first(): The values() function returns a collection, but we only want the first item in it. There should only ever be one item in the collection.

Categories

Resources