简体   繁体   中英

Using ramda.js, how to replace a value in a nested structure?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM