1

I've seen this question in several places but still can't figure this out. Using ramda, how can I filter the following object to return the records that are true for tomatoes?

[
    {
        "id": "a",
        "name": "fred",
        "food_prefs": {
            "tomatoes": true,
            "spinach": true,
            "pasta": false
        },
        "country": "singapore"
    },
    {
        "id": "b",
        "name": "alexandra",
        "food_prefs": {
            "tomatoes": false,
            "spinach": true,
            "pasta": true
        },
        "country": "france"
    },
    {
        "id": "c",
        "name": "george",
        "food_prefs": {
            "tomatoes": true,
            "spinach": false,
            "pasta": false
        },
        "country": "argentina"
    }
]

Storing this array as myData object, I thought that the following should work:

const R = require("ramda")

const lovesTomatoes = R.pipe ( // taken from: https://stackoverflow.com/a/61480617/6105259
    R.path (["food_prefs"]),
    R.filter (R.prop ("tomatoes"))
)

console.log(lovesTomatoes(myData))

But I end up with the error:

if (typeof obj[methodNames[idx]] === 'function') {

What am I doing wrong?


EDIT


The answers provided by @Ori Drori and @ThanosDi are both great, but I want to emphasize that a pipe-based solution would be ideal because I have follow-up steps I wish to carry on the filtered array. Consider for example the following array. It's similar the one above, but includes more data: year_born and year_record.

[
    {
        "id": "a",
        "name": "fred",
        "year_born": 1995,
        "year_record": 2010,
        "food_prefs": {
            "tomatoes": true,
            "spinach": true,
            "pasta": false
        },
        "country": "singapore"
    },
    {
        "id": "b",
        "name": "alexandra",
        "year_born": 2002,
        "year_record": 2015,
        "food_prefs": {
            "tomatoes": false,
            "spinach": true,
            "pasta": true
        },
        "country": "france"
    },
    {
        "id": "c",
        "name": "george",
        "year_born": 1980,
        "year_record": 2021,
        "food_prefs": {
            "tomatoes": true,
            "spinach": false,
            "pasta": false
        },
        "country": "argentina"
    }
]

So, for example, to answer a full question such as "for those who love tomatoes, what is the average age at the time of the record creation?"

we would need to:

  1. filter the records that love tomates;
  2. extract the elements year_born and year_record
  3. get the difference between values
  4. take the average of the differences

Therefore, using a pipe would be very beneficial.

Emman
  • 3,695
  • 2
  • 20
  • 44

3 Answers3

2

What went wrong?

You try to get the value of food_prefs out of the array. Since the array doesn't have this key - R.path (["food_prefs"]) is undefined, and then you try to filter this undefined value.

How to solve this problem?

Filter the array, and use R.path to get the tomatoes value.

const { filter, path, identity } = R

const lovesTomatoes = filter(path(['food_prefs', 'tomatoes']))

const data = [{"id":"a","name":"fred","food_prefs":{"tomatoes":true,"spinach":true,"pasta":false},"country":"singapore"},{"id":"b","name":"alexandra","food_prefs":{"tomatoes":false,"spinach":true,"pasta":true},"country":"france"},{"id":"c","name":"george","food_prefs":{"tomatoes":true,"spinach":false,"pasta":false},"country":"argentina"}]

const result = lovesTomatoes(data)

console.log(result)
<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>

Filtering using a pipe:

Using R.pipe. I wouldn't go this way for a simple filter by nested properties, but you can use a Schwartzian transform. The idea is to create a new array if pairs [value of tomatoes, original object], filter by the value of tomatoes, and then extract the original object:

const { pipe, map, applySpec, path, identity, filter, last, head } = R

const lovesTomatoes = pipe(
  map(applySpec([path(['food_prefs', 'tomatoes']), identity])), // create an array of [value of tomatoes, original object] 
  filter(head), // filter by the value of the tomatoes
  map(last) // extract the original object
)

const data = [{"id":"a","name":"fred","food_prefs":{"tomatoes":true,"spinach":true,"pasta":false},"country":"singapore"},{"id":"b","name":"alexandra","food_prefs":{"tomatoes":false,"spinach":true,"pasta":true},"country":"france"},{"id":"c","name":"george","food_prefs":{"tomatoes":true,"spinach":false,"pasta":false},"country":"argentina"}]

const result = lovesTomatoes(data)

console.log(result)
<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>

How to combine the 1st lovesTomatoes filtering function in a pipe:

However, if you just need the pipe to perform other operations on the filtered array, use the filter as one of the steps:

const { filter, path, identity, pipe, map, prop, uniq } = R

const lovesTomatoes = filter(path(['food_prefs', 'tomatoes']))

const countriesOfTomatoLovers = pipe(
  lovesTomatoes,
  map(prop('country')),
  uniq
)

const data = [{"id":"a","name":"fred","food_prefs":{"tomatoes":true,"spinach":true,"pasta":false},"country":"singapore"},{"id":"b","name":"alexandra","food_prefs":{"tomatoes":false,"spinach":true,"pasta":true},"country":"france"},{"id":"c","name":"george","food_prefs":{"tomatoes":true,"spinach":false,"pasta":false},"country":"argentina"}]

const result = countriesOfTomatoLovers(data)

console.log(result)
<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>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Wonderful, thank you. Is there a way to use `R.path()` to *navigate* to `food_prefs`? In other words, if we wanted to stick to the `R.pipe()` syntax, how should we do it? – Emman Feb 02 '22 at 15:03
  • Pipe is not relevant for this problem. I can add a solution that uses a pipe, but it would be much more complicated. And the question is why? – Ori Drori Feb 02 '22 at 15:05
  • Because (for me at least) it's easier to understand the functional steps: (1) go to location *x* in the nested hierarchy, then (2) filter property *y* according to whether it has value *z*. So the `pipe()` makes it an explicit set of steps... – Emman Feb 02 '22 at 15:10
  • Also consider that potentially I might want to do additional steps once I've filter the data. For example, once we get the records who love tomatoes, we then want to extract those people names to an array, then capitalize their names, and so on... so a `pipe()` will become very useful for long set of steps. Otherwise, we will end up with hard-to-read "onion" code (e.g., `doX(doY(doZ(doB(...)))))`) isn't that what the pipe is trying to solve? – Emman Feb 02 '22 at 15:22
  • I agree, but in this case the `filter(pathSatisfies)` is short enough to be part of the pipeline. I'm adding a more pipelined example to show the difference. – Ori Drori Feb 02 '22 at 15:54
  • Thanks. I edited the post to give better context. – Emman Feb 02 '22 at 16:01
  • See updated examples – Ori Drori Feb 02 '22 at 16:07
  • 2
    Jus wanted to point out that `filter(pathSatisfies(identity, ['food_prefs', 'tomatoes']))` is equivalent to `filter(R.path(['food_prefs', 'tomatoes']))`. pathSatisfies is great, but not needed here.. – Chad S. Feb 02 '22 at 17:39
  • @ChadS. I've missed the obvious. Fixed. – Ori Drori Feb 02 '22 at 17:45
  • 1
    @Emman: if it's not yet clear, the point is that `filter (path (['food_prefs', 'tomatoes'])` is a *step* in a pipeline. It takes an array, and returns a new array containing the appropriate subset of the original one. If you want to do more, simply wrap this inside your pipeline, between whatever steps you find it appropriate. – Scott Sauyet Feb 02 '22 at 18:29
2
const myData = [
    {
        "id": "a",
        "name": "fred",
        "food_prefs": {
            "tomatoes": true,
            "spinach": true,
            "pasta": false
        },
        "country": "singapore"
    },
    {
        "id": "b",
        "name": "alexandra",
        "food_prefs": {
            "tomatoes": false,
            "spinach": true,
            "pasta": true
        },
        "country": "france"
    },
    {
        "id": "c",
        "name": "george",
        "food_prefs": {
            "tomatoes": true,
            "spinach": false,
            "pasta": false
        },
        "country": "argentina"
    }
];

const lovesTomatoes = filter(pathOr(false, ['food_prefs','tomatoes']));

lovesTomatoes(myData);

Ramda REPL

ThanosDi
  • 339
  • 3
  • 8
  • Thank you. Would you mind advising why translating to the following pipe fails? `const lovesTomatoes_pipe = R.pipe(R.pathOr(false, ["food_prefs"]), R.filter(R.prop("tomatoes")))` I'm trying to adopt the syntax of [this answer](https://stackoverflow.com/a/61480617/6105259) but so far unsuccessfully. – Emman Feb 02 '22 at 15:44
  • The first function of pipe `R.pathOr(false, ["food_prefs"])` is not on a map so it will just return false and then the second function `R.filter(R.prop("tomatoes"))` will try to filter a boolean, not an array. – ThanosDi Feb 02 '22 at 15:52
1

Ramda comes with a whole suite of predicates built-in already, one of them that I'd use here is pathEq.

I'd suggest to adopt a map and reduce kind of approach, whereas the match function is separated from the actual aggregation...

  1. Collect your data point
  2. Reduce it to the information you need

const tomatoLovers = R.filter(
  R.pathEq(['food_prefs', 'tomatoes'], true),
);

const avgAge = R.pipe(R.pluck('age'), R.mean);

const data = [{
    "id": "a",
    age: 16,
    "name": "fred",
    "food_prefs": {
      "tomatoes": true,
      "spinach": true,
      "pasta": false
    },
    "country": "singapore"
  },
  {
    "id": "b",
    age: 66,
    "name": "alexandra",
    "food_prefs": {
      "tomatoes": false,
      "spinach": true,
      "pasta": true
    },
    "country": "france"
  },
  {
    "id": "c",
    age: 44,
    "name": "george",
    "food_prefs": {
      "tomatoes": true,
      "spinach": false,
      "pasta": false
    },
    "country": "argentina"
  }
]

console.log(
  'Average age of tomato lovers is:',
  R.pipe(tomatoLovers, avgAge) (data),
);

console.log(
  'They are the tomato lovers',
  R.pipe(tomatoLovers, R.pluck('name')) (data),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.js" integrity="sha512-ZZcBsXW4OcbCTfDlXbzGCamH1cANkg6EfZAN2ukOl7s5q8skbB+WndmAqFT8fuMzeuHkceqd5UbIDn7fcqJFgg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
  • Thank you! I wonder about `tomatoLovers()`. Isn't `filter(path(['food_prefs', 'tomatoes']))` more straightforward? Or does `R.pathEq()` have an advantage here? – Emman Feb 03 '22 at 12:16