patrickrs
patrickrs

Reputation: 77

JavaScript mapping and filtering on nested arrays

I'm guessing this is a basic question for anyone with experience and a logical brain, but this has stumped me for two days.

I'm trying to filter an array by states, then map just one property of the resulting array(s).

Here is my states array, which contains a subset of U.S. states:

const states = [{ state: 'AL' }, { state: 'OH' }, { state: 'PA' }]

Here is the array I want to first filter by state, and then map into a new array of just the values I need.

refData = [
  {
    state: 'AL',
    details: [
      {
        code: '1A',
        description: 'AL Description 1'
      },
      {
        code: '1B',
        description: 'AL Description 2'
      },
      {
        code: '1C',
        description: 'AL Description 3'
      }
    ]
  },
  {
    state: 'PA',
    details: [
      {
        code: '1A',
        description: 'PA Description 1'
      },
      {
        code: '1B',
        description: 'PA Description 2'
      }
    ]
  }
]

Here is my only working attempt to filter and then map, but it doesn't give me what I need:

const filteredRefData = refData
  .filter((item) => (states.some(stateName => item.state === stateName.state)))
  .map((item) => item.details)

What this gets me is an array of ALL the details, both code and description. What I need is JUST the description value, but no matter what I try, I can't arrive at that end result.

What I get from this map:

[
  0: [
    0: {code: "1A", description: "AL Description 1"}
    1: {code: "1B", description: "AL Description 2"}
    2: {code: "1C", description: "AL Description 3"}
  ],
  1: [
    0: {code: "1A", description: "PA Description 1"}
    1: {code: "1B", description: "PA Description 2"}
  ]
]

What I need from this map:

[
    0: [
      0: "AL Description 1"
      1: "AL Description 2"
      2: "AL Description 3"
    ],
    1: [
      0: "PA Description 1"
      1: "PA Description 2"
    ]
]

I tried using dynamic indexes, but that failed because any index I passed always related to the top level array, and not the nested array. I also tried the reduce method, but I found it difficult to understand how reduce works with just an object's key/value pairs.

Thank you for any help you can provide!

Upvotes: 0

Views: 1466

Answers (4)

Everett
Everett

Reputation: 9568

You are right that this is a job for reduce: a map operation will yield a 1:1 translation of the input array (e.g. refData.map(state => state.state) would return an array of the same length containing only the state attribute), whereas reduce can return an array with more or fewer results.

In your case, you start with x number of states, but you will return y descriptions, so map is not an option, you must use reduce (and this is a good example of how to use a functional approach).

For example, if you wanted to strip out just the state abbreviation using reduce instead of map, you could do that by accumulating the state abbreviation:

data.reduce(function(acc, x) {
    return acc.concat(x.state);
}, []);

Here we set the initial value of the accumulator to an empty array [] (2nd arg) and we use concat to append single values to the end of the acc array -- the result is the same as using map.

In your case, you want to concatenate an array of descriptions (or some other variable attribute), so here's how you could reduce the descriptions:

var refData = [
  {
    state: 'AL',
    details: [
      {
        code: '1A',
        description: 'AL Description 1'
      },
      {
        code: '1B',
        description: 'AL Description 2'
      },
      {
        code: '1C',
        description: 'AL Description 3'
      }
    ]
  },
  {
    state: 'PA',
    details: [
      {
        code: '1A',
        description: 'PA Description 1'
      },
      {
        code: '1B',
        description: 'PA Description 2'
      }
    ]
  }
];

refData.reduce(function(acc, x) {
    var descriptions = x.details.map(function(detail) { 
        return detail.description; 
    });
    return acc.concat(descriptions);
}, []);

or more tersely as

refData.reduce(function(acc, x) {
    return acc.concat(x.details.map(detail => detail.description));
}, []);

Upvotes: 2

ahwayakchih
ahwayakchih

Reputation: 2371

If you insist on using only filter and map, try this:

const filteredRefData = refData
    .filter((item) => (states.some(stateName => item.state === stateName.state)))
    .map((item) => item.details.map(d => d.description))

But that's not really optimal solution. Maybe try using reduce instead and compare results.

Upvotes: 1

Robin Zigmond
Robin Zigmond

Reputation: 18249

You were almost there. The only change needed was, in the final .map, not to just return item.details but the result of map-ing that array to extract just the description:

const states = [{ state: 'AL' }, { state: 'OH' }, { state: 'PA' }];

const refData = [
  {
    state: 'AL',
    details: [
      {
        code: '1A',
        description: 'AL Description 1'
      },
      {
        code: '1B',
        description: 'AL Description 2'
      },
      {
        code: '1C',
        description: 'AL Description 3'
      }
    ]
  },
  {
    state: 'PA',
    details: [
      {
        code: '1A',
        description: 'PA Description 1'
      },
      {
        code: '1B',
        description: 'PA Description 2'
      }
    ]
  }
];

const filteredRefData = refData
  .filter((item) => (states.some(stateName => item.state === stateName.state)))
  .map((item) => item.details.map(({ description }) => description));

console.log(filteredRefData);

Upvotes: 3

Charles
Charles

Reputation: 255

I guess consuming the 'description' field would solve your problem. Try this:

const filteredRefData = refData
  .filter((item) => (states.some(stateName => item.state === stateName.state)))
  .map((item) => item.details.description)

Upvotes: 0

Related Questions