I'm trying to leverage the technique shown here for replacing values in an object with ramda.js. Unlike the linked reference, my object has many more nesting layers, and so it fails.
In the following example, we have an object that details attractions in cities. First it specifies the cities, the we dive in into nyc, then to zoos, then StatenIslandZoo, and finally we get to zooInfo that holds two records for two animals. In each one, we have the aniaml's name in the value associated with the animal key. I want to correct the value's string by replacing it with another string and return a new copy of the entire cityAttractions object.
const cityAttractions = {
"cities": {
"nyc": {
"towers": ["One World Trade Center", "Central Park Tower", "Empire State Building"],
"zoos": {
"CentralParkZoo": {},
"BronxZoo": {},
"StatenIslandZoo": {
"zooInfo": [
{
"animal": "zebra_typo", // <- replace with "zebra"
"weight": 100
},
{
"animal": "wrongstring_lion", // <- replace with "lion"
"weight": 1005
}
]
}
}
},
"sf": {},
"dc": {}
}
}
So I defined a function very similar to this one:
const R = require("ramda")
const myAlter = (myPath, whereValueEquals, replaceWith, obj) => R.map(
R.when(R.pathEq(myPath, whereValueEquals), R.assocPath(myPath, replaceWith)),
obj
)
And then called myAlter() and stored the ouput into altered:
const altered = myAlter(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", "animal"], "zebra_typo", "zebra", cityAttractions)
But when checking, I realize no replacement had happend:
console.log(altered.cities.nyc.zoos.StatenIslandZoo.zooInfo)
// [
// { animal: 'zebra_typo', weight: 100 },
// { animal: 'wrongstring_lion', weight: 1005 }
// ]
Some troubleshooting
If I we go back and examine the original cityAttractions object, then we can first extract just the level of cityAttractions.cities.nyc.zoos.StatenIslandZoo.zooInfo, then acting on that with myAlter() does work.
const ZooinfoExtraction = R.path(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"])(cityAttractions)
console.log(ZooinfoExtraction)
// [
// { animal: 'zebra_typo', weight: 100 },
// { animal: 'wrongstring_lion', weight: 1005 }
// ]
console.log(myAlter(["animal"], "zebra_typo", "zebra", ZooinfoExtraction))
// here it works!
// [
// { animal: 'zebra', weight: 100 },
// { animal: 'wrongstring_lion', weight: 1005 }
// ]
So for some reason, myAlter() works on the extracted ZooinfoExtraction but not on the original cityAttractions. Which is a problem because I need the entire original structure (just replacing the specified values).
EDIT - troubleshooting 2
I guess the problem relies in the fact that
R.path(["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", "animal"], cityAttractions)
returns undefined.
There main problem is that the animal property is part of an array item. Since array index should be a number, the path for Zebra is actually:
["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo", 0, "animal"]
However, this will force you to know the actual index.
In addition, mapping an array returns a clone of the array (with the changes), and not the entire structure.
To solve this problem, you can use a lens (R.lensPath in this case) with R.over to return an updated clone of the entire structure.
Example:
const { curry, over, lensPath, map, when, pathEq, assoc } = R
const alterAnimal = curry((path, subPath, whereValueEquals, replaceWith, obj) =>
over(
lensPath(path),
map(when(pathEq(subPath, whereValueEquals), assoc(subPath, replaceWith))),
obj
))
const cityAttractions = {"cities":{"nyc":{"towers":["One World Trade Center","Central Park Tower","Empire State Building"],"zoos":{"CentralParkZoo":{},"BronxZoo":{},"StatenIslandZoo":{"zooInfo":[{"animal":"zebra_typo","weight":100},{"animal":"wrongstring_lion","weight":1005}]}}},"sf":{},"dc":{}}}
const altered = alterAnimal(
["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"],
["animal"],
"zebra_typo",
"zebra",
cityAttractions
)
console.log(altered)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Since you transform an object's property value, you can also use R.evolve, and supply an update function that can cover all cases. For example:
const { curry, over, lensPath, map, evolve, flip, prop, __ } = R
const alterObj = curry((updateFn, prop, path, obj) =>
over(
lensPath(path),
map(evolve({
[prop]: updateFn
})),
obj
))
const replacements = {
'zebra_typo': 'zebra',
'wrongstring_lion': 'lion',
}
const alterAnimals = alterObj(prop(__, replacements))
const cityAttractions = {"cities":{"nyc":{"towers":["One World Trade Center","Central Park Tower","Empire State Building"],"zoos":{"CentralParkZoo":{},"BronxZoo":{},"StatenIslandZoo":{"zooInfo":[{"animal":"zebra_typo","weight":100},{"animal":"wrongstring_lion","weight":1005}]}}},"sf":{},"dc":{}}}
const altered = alterAnimals(
"animal",
["cities", "nyc", "zoos", "StatenIslandZoo", "zooInfo"],
cityAttractions
)
console.log(altered)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Using Lenses
Another lens-based approach is to write a new lens function. Ramda only supplies lensProp, lensIndex and lensPath. But we could write one that matches the first array element where the animal matches. I can reuse a lensMatch function I've used in other answers and then configure it with const animalLens = lensMatch ('animal'). Then we can compose that with other lenses to get to the property we want to change. It might look like this:
const lensMatch = (propName) => (key) => lens (
find (propEq (propName, key)),
(val, arr, idx = findIndex (propEq (propName, key), arr)) =>
update (idx > -1 ? idx : length (arr), val, arr)
)
const animalLens = lensMatch ('animal')
const updateAnimalName = (oldName, newName, attractions) => set (compose (
lensPath (['cities', 'nyc', 'zoos', 'StatenIslandZoo', 'zooInfo']),
animalLens (oldName),
lensProp ('animal')
), newName, attractions)
const cityAttractions = {cities: {nyc: {towers: ["One World Trade Center", "Central Park Tower", "Empire State Building"], zoos: {CentralParkZoo: {}, BronxZoo: {}, StatenIslandZoo: {zooInfo: [{animal: "zebra_typo", weight: 100}, {animal: "wrongstring_lion", weight: 1005}]}}}, sf: {}, dc: {}}}
console .log (
updateAnimalName ('zebra_typo', 'zebra', cityAttractions)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {lens, find, propEq, findIndex, update, length, set, lensPath, compose, lensProp} = R </script>
Obviously we could then fold this over multiple animal names (say zebra and lion) if you wanted.
A generic replacement function
Another entirely different approach would be -- if the typo values are unlikely to appear elsewhere in your data structure -- to simply walk the whole tree, replacing all "zebra_typo" with "zebra". That would be a simple recursion:
const replaceVal = (oldVal, newVal) => (o) =>
o == oldVal
? newVal
: Array .isArray (o)
? o .map (replaceVal (oldVal, newVal))
: Object (o) === o
? Object .fromEntries (Object .entries (o) .map (([k, v]) => [k, replaceVal (oldVal, newVal) (v)]))
: o
const cityAttractions = {cities: {nyc: {towers: ["One World Trade Center", "Central Park Tower", "Empire State Building"], zoos: {CentralParkZoo: {}, BronxZoo: {}, StatenIslandZoo: {zooInfo: [{animal: "zebra_typo", weight: 100}, {animal: "wrongstring_lion", weight: 1005}]}}}, sf: {}, dc: {}}}
console .log (
replaceVal ('zebra_typo', 'zebra') (cityAttractions)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
This approach is quite generic, but is more targeted at replacing all "foo" values with "bar" ones, regardless of level. But it might work for your case.
How can i make a search engine out of an object?
Example:
how can i use
{
onE:"two",
three:"four",
wOn:"too"
},
do something with the phrase "ON",
and somehow return ["two","too"]?
Edit:
if i may alter this question slightly, how can i make the most alike keys' values go first? I'm also using spaces in the keys.
new Example:
in:
{
"one one zero":"0"
"zero":"1"
"one two":"2"
"one one":"3"
}
out:
["3","0","2"]
var object = {
onE: "two",
three: "four",
wOn: "too"
};
const result = Object.entries(object).reduce((accum, current) => {
const [key, value] = current;
if (key.match(/On/i)) {
return [...accum, value]
}
return [...accum]
}, [])
console.log(result);
An alternative solution using entries and reduce, avoiding push which avoid side effects
This might help.
var object = {
onE: "two",
three: "four",
wOn: "too"
};
var array = [];
Object.keys(object).filter((key) => key.match(/On/i)).forEach((key) => {
array.push(object[key]);
});
console.log(array);
.as-console-wrapper {
max-height: 100% !important;
}
Basically, get the keys from the object, and filter them on the basis of regex to match ON in the key. Once we have the filtered keys, we forEach loop them to push their values into the empty array.
1) You can simply get keys of an object using Object.keys and push the value of an object whose key contains on in it using regex
/on/i
var object = {
onE: "two",
three: "four",
wOn: "too",
};
const result = [];
for (let key of Object.keys(object)) {
if (key.match(/on/i)) result.push(object[key]);
}
console.log(result);
2) You can also do with Object.keys, filter, and map
var object = {
onE: "two",
three: "four",
wOn: "too",
};
const result = Object.keys(object)
.filter((key) => key.match(/On/i))
.map((k) => object[k]);
console.log(result);
My task is straight forward. I have an array of strings:
let a=["a","b","c"];
And i want to convert that array to (can alter the original array, doesn't matter) what i would like to call as "recursive object" just so:
//in json format just to demonstrate
"object": {
"a": {
"b":{
"c":{
}
}
}
}
I've tried the following logic but couldn't get it to work due to reference problem and i couldn't build recursion.
let tempObj;
let obj2;
array.slice().reverse().forEach(function(item,index){
obj2=tempObj;
tempObj[item]="";
});
Just to make sure we are on the same page, another example:
let arr=["alpha","beta","gamma"];
let magicObj=someMagicFunction(arr);
//magicObj["alpha"]["beta"]["gamma"] contains the value ""
Thanks
Start aggregating your object from the array's right most side via reduceRight and provide e.g. an empty object / {} or an empty string / "" as this method's initial value(s) ...
console.log(
'object :',
["a","b","c"]
.reduceRight((obj, key) =>
({ [key]: obj }), {}
)
);
console.log(
'object :',
["alpha","beta","gamma"]
.reduceRight((obj, key) =>
({ [key]: obj }), ""
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
... and since code code-reuse always should be a goal the above examples change to ...
function createObjectWithParentKeyAndChildValue(value, key) {
return { [key]: value };
}
console.log(
'object :',
['a', 'b', 'c']
.reduceRight(createObjectWithParentKeyAndChildValue, {})
);
console.log(
'object :',
['alpha', 'beta', 'gamma']
.reduceRight(createObjectWithParentKeyAndChildValue, '')
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Here is a simple solution:
const arr = ["a","b","c"];
const result = arr.reverse().reduce((obj, key) => ({[key]: obj}), {})
console.log(result)
Here a little explaination:
o is the result of the last iteration and v is the current element in the array. {[v]: o} creates a new object and sets the property v to o and returns that.
The reduce / reduceRight answers are great. But this can also be done with a fairly trivial recursive version:
const buildDeep = ([p, ...ps], val) =>
p == undefined ? val : {[p]: buildDeep (ps, val)}
console .log (buildDeep (['a', 'b', 'c'], {}))
console .log (buildDeep (['alpha', 'beta', 'gamma'], ''))
To my mind, this is even simpler than reduce. It feels related to the various path-setting functions you see around, but is less complex since it doesn't have to work with an existing object.
there is my pure recursive answer:
let a=["a","b","c"];
const b = (arr = [], obj = null) => {
if (arr.length > 0) {
const { length, [length - 1]: last, ...r } = arr;
const rest = Object.values(r);
const nextObj = obj ? { [last]: obj } : { [last]: {} };
return b(rest, nextObj);
}
return obj;
};
console.log(b(a));
let magicObj = arr.reverse().reduce((obj, prop) => ({ [prop]: obj }), {})
My object looks like this
const obj = Immutable.fromJS({
{
1: {foo: 'a'}, 2: {foo: 'b'}, 3: {foo: 'ac'}, ...
}
});
I want to search the object, and return the result like this
const results = {
1: {foo: 'a'}, 3: {foo: 'ac'}
};
That is what I've got so far. However, this only works if object is an Immutable Object.
const search = 'a';
const results = obj.filter(elem => {
return search.split(' ').some(word => {
return (
elem.get('foo').indexOf(word) > -1
)
});
})
How to filter if obj is a Vanilla JavaScript object?
I don't think Immutable Object is a vanilla javascript type. Are you using some sort of third-party library?
How to filter a javascript object
Regular javascript objects don't have a filter method defined. One way of solving the issue is to convert an object to an array of its entries first, filter the entries, and then convert back to an object:
const filterObject = ( filterFn, obj ) => {
const validEntries = Object
.entries(obj)
.filter(filterFn);
return Object.fromEntries(validEntries);
};
console.log(
filterObject(
([ key, value ]) => value !== 2,
{ "a": 1, "b": 2, "c": 3 }
)
);
Note that Object.fromEntries is not supported by all browsers yet.
You might notice that my filter function is a bit different then one you'd pass to an Array#filter call. Object.entries returns an array of key-value pairs. Therefore, the filterFn should take an array of key and value, which I destructure in the arguments.
Implementing your requested filter logic
const filterObject = ( filterFn, obj ) => {
const validEntries = Object
.entries(obj)
.filter(filterFn);
return Object.fromEntries(validEntries);
};
const search = "a";
const myFilter = ([_, elem]) => { // elem corresponds to our entry's value
return search
.split(' ')
.some(word =>
elem.foo.indexOf(word) > -1 // Use the regular . syntax instead of .get
);
}
console.log(
filterObject(
myFilter,
{ 1: {foo: 'a'}, 2: {foo: 'b'}, 3: {foo: 'ac'} }
)
);
You can do it in the following way:
const keys = [1, 3];
const obj = {1: {test: 1}, 2: {test: 2}, 3: {test: 3}};
let yourObject = Object.keys(obj)
.filter(key => keys.includes(parseInt(key)))
.map(key => {
return {[key]: obj[key]}
})
.reduce((a, b) => Object.assign({}, a,b));
console.log(yourObject);
I have a function that returns an object like (key is string and value is array of strings):
{"myType1": ["123"]}
I want to merge all the results its returning for example - if I have:
{"myType1": ["123"]}
{"myType2": ["456"]}
{"myType1": ["789"]}
I'd like to get
{"myType1": ["123", "789], "myType2": ["456"]}
I've seen Object.assign however that doesn't merge the values it just replaces with the most recent.
Has anyone got an examples of how I can achieve this?
Any help appreciated.
Thanks.
You can the keys of an object by using Object.keys. Use forEach to loop thru the keys and modify the result object.
var result = {};
function mergeObj(o) {
Object.keys(o).forEach(k=>{
result[k] = ( result[k] || [] ).concat(o[k]);
})
}
mergeObj({"myType1": ["123"]});
mergeObj({"myType2": ["456"]});
mergeObj({"myType1": ["789"]});
console.log(result);
Make a function that adds each property to a result object, and call it with each object you get.
let res = {};
const addObj = obj => {
Object.entries(obj).forEach(([k, v]) => (res[k] = res[k] || []).push(...v));
};
addObj({"myType1": ["123"]});
addObj({"myType2": ["456"]});
addObj({"myType1": ["789"]});
console.log(res);
.as-console-wrapper { max-height: 100% !important; top: auto; }
Here is a one liner with the desired process:
Array.from(new Set([{"myType1": ["123"]}, {"myType2": ["456"]}, {"myType1": ["789"]}].map(item => Object.keys(item)[0]).sort())).map(e => { const entry = {}; entry[e] = items.filter(item => Object.keys(item)[0] === e).map(item => item[e][0]); return entry })
Result:
[ { myType1: [ '123', '789' ] }, { myType2: [ '456' ] } ]
Regards.