How to filter and map nested array items while checking whether the to be filtered items meet certain criteria?

Viewed 473

I need to filter and map both, array items and each item's child array items while checking whether the to be filtered items have certain properties which meet certain criteria.

What are good array method based approaches for filtering and mapping nested array items?

Required Properties

"productName", "productCategory", "content", "id"

AND also if status inside productImages is not Linked or Existing

Sample Data

[
  {
    "productName": null,
    "productCategory": null,
    "productImages": [
      {
        "content": "1",
        "id": null,
        "data": null,
        "file": null,
        "url": null,
        "status": "Existing"
      },
      {
        "content": "",
        "id": "234",
        "data": "test data",
        "file": "test file",
        "url": null,
        "status": "Existing"
      }
    ]
  },
  {
    "productName": null,
    "productCategory": "hello category",
    "productImages": [
      {
        "content": "1",
        "id": null,
        "data": null,
        "file": null,
        "url": null,
          "status": "Existing"
      },
      {
        "content": "",
        "id": "234",
        "data": "test data",
        "file": "test file",
        "url": null,
          "status": "Existing"
      }
    ]
  },
  {
    "productName": "test product",
    "productCategory": "test category",
    "productImages": [
      {
        "content": "1",
        "id": "123",
        "data": "test data",
        "file": "test file",
        "url": null,
        "status": "Linked"
      },
      {
        "content": "2",
        "id": "234",
        "data": "test data",
        "file": "test file",
        "url": "test",
        "status": "Linked"
      }
    ]
  },
  {
    "productName": "new product",
    "productCategory": "new category",
    "productImages": [
      {
        "content": "2",
        "id": "32332",
        "data": "test data",
        "file": "test file",
        "url": "test",
        "status": "new"
      }
    ]
  }
]

Expected Output

[
  {
    "productIndex": 3,
    "name": "new product",
    "category": "new category",
    "filteredImages": [
      {
        "newcontent": "2",
        "id": "32332",
      },
    ]
  },
]

Code

const filteredProducts = products.filter((element) => element.productName && element.productCategory);
3 Answers

You can do it with one for-of loop:

const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
  if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
    new_data.push({
      productIndex: index,
      name: item.productName,
      category: item.productCategory,
      filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id,  }))
    })
  } else {
    incomplete_items.push(index);
  }
}

Working example

const data = [
  {
    "productName": null,
    "productCategory": null,
    "productImages": [
      { "content": "1",  "id": null, "data": null, "file": null, "url": null, "status": "Existing" },
      { "content": "","id": "234","data": "test data", "file": "test file", "url": null, "status": "Existing" }
    ]
  },
  {
    "productName": null,
    "productCategory": "hello category",
    "productImages": [
      { "content": "1", "id": null,"data": null, "file": null, "url": null, "status": "Existing"},
      { "content": "", "id": "234","data": "test data","file": "test file", "url": null, "status": "Existing" }
    ]
  },
  {
    "productName": "test product",
    "productCategory": "test category",
    "productImages": [ 
      { "content": "1", "id": "123", "data": "test data", "file": "test file", "url": null, "status": "Linked" },
      { "content": "2", "id": "234", "data": "test data", "file": "test file", "url": "test","status": "Linked" }
    ]
  },
  {
    "productName": "new product",
    "productCategory": "new category",
    "productImages": [
      { "content": "2","id": "32332", "data": "test data", "file": "test file", "url": "test", "status": "new" }
    ]
  }
]

const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
  if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
    new_data.push({
      productIndex: index,
      name: item.productName,
      category: item.productCategory,
      filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id,  }))
    })
  } else {
    incomplete_items.push(index);
  }
}

if(incomplete_items.length) console.log(`Items ${incomplete_items} were missing required properties.`);
console.log(new_data);

A straightforward approach would be based on just two reducer functions, each being properly named according to its implemented purpose, where the outer reduce task iterates the products and the inner task iterates each product's related images.

Thus one achieves both within each reduce task, the filtering and the property mapping for the expected result items.

const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];

function collectNonMatchingImageItem(result, image) {
  const { id = null, content = null, status = null } = image;
  if (
    // loose truthyness comparison ... 1st stage filter (valid image) ...
    String(id ?? '').trim() &&
    String(content ?? '').trim() &&
    String(status ?? '').trim() &&

    // ... 2nd stage filter (non matching valid image item) ...
    !(/^linked|existing$/i).test(status)
  ) {
    // ... and mapping at once.
    result.push({
      id,
      newcontent: content,
    });
  }
  return result;
}
function collectValidProductWithNonMatchingImages(result, product, idx) {
  const {
    productName = null,
    productCategory = null,
    productImages = [],
  } = product;

  // loose truthyness comparison ... 1st stage filter (valid product) ...
  if (
    String(productName ?? '').trim() &&
    String(productCategory ?? '').trim()
  ) {
    const nonMatchingItems = productImages
      .reduce(collectNonMatchingImageItem, []);

    // 2nd stage filter (valid product with non matching valid images) ...
    if (nonMatchingItems.length >= 1) {

      // ... and mapping at once.
      result.push({
        productIndex: idx,
        name: productName,
        category: productCategory,
        filteredImages: nonMatchingItems,
      });
    }
  }
  return result;
}

const result = input
  .reduce(collectValidProductWithNonMatchingImages, []);

console.log({ result });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit

"... How do you compare the old and new array. I wanted to output a warning if it isn't complete" – Joseph

The above approach shows its flexibility if it comes to the integration of the new requirement ...

const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];

function collectNonMatchingImageItem(collector, image, idx) {
  const { id = null, content = null, status = null } = image;

  // loose truthyness comparison ... 1st stage filter (valid image) ...
  if (
    String(id ?? '').trim() &&
    String(content ?? '').trim() &&
    String(status ?? '').trim()
  ) {
    // ... 2nd stage filter (non matching valid image item) ...
    if (
      !(/^linked|existing$/i).test(status)
    ) {
      // ... and mapping at once.
      collector.result.push({
        id,
        newcontent: content,
      });
      collector.nonMatchingAt.push(idx);
    }
  } else {
    collector.invalidAt.push(idx);
  }
  return collector;
}
function collectValidProductWithNonMatchingImages(collector, product, idx) {
  const {
    productName = null,
    productCategory = null,
    productImages = [],
  } = product;

  // loose truthyness comparison ... 1st stage filter (valid product) ...
  if (
    String(productName ?? '').trim() &&
    String(productCategory ?? '').trim()
  ) {
    const {
      result: nonMatchingItems,
      nonMatchingAt,
      invalidAt,
    } = productImages.reduce(
      collectNonMatchingImageItem,
      { result: [], nonMatchingAt: [], invalidAt: [] },
    );
    // report related variables.
    let isPlural, itemPhrase, indexPhrase, imageReport;

    // 2nd stage filter (valid product with non matching valid images) ...
    if (nonMatchingItems.length >= 1) {

      // ... and mapping at once.
      collector.result.push({
        productIndex: idx,
        name: productName,
        category: productCategory,
        filteredImages: nonMatchingItems,
      });
      // create and collect report item.
      isPlural = (nonMatchingAt.length >= 2);

      itemPhrase = `item${ isPlural ? 's' : '' }`;
      indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
      imageReport = `with non matching valid image ${ itemPhrase } at ${ indexPhrase } ${ nonMatchingAt }`;

      collector.report.push(
        `Valid product item at index ${ idx } ${ imageReport }.`
      );
    }
    if (invalidAt.length >= 1) {
      // create and collect report item.
      isPlural = (invalidAt.length >= 2);

      itemPhrase = `item${ isPlural ? 's' : '' }`;
      indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
      imageReport = `with invalid image ${ itemPhrase } at ${ indexPhrase } ${ invalidAt }`;

      collector.report.push(
        `Valid product item at index ${ idx } ${ imageReport }.`
      );
    }
  } else {
    // create and collect report item.
    collector.report.push(
      `Invalid product item at index ${ idx }.`
    );
  }
  return collector;
}

const {
  result,
  report,
} = input.reduce(
  collectValidProductWithNonMatchingImages,
  { result: [], report: [] },
);
console.log({ result, report });
.as-console-wrapper { min-height: 100%!important; top: 0; }

You can use reduce for this.

Thank you Peter Seliger for pointing me to the double reduce. In terms of readability, do you think it's preferred to use 2 reducers instead of a filter().map() inside the outer reducer?

const sampleData = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];

const expectedData = sampleData.reduce((acc, val, i) => {

  // check if productName and productCategory are available
  if (val.productName && val.productCategory) {

    // get images that match the criteria and return object in desired format
    const filteredImages = val.productImages.reduce(
      (matchingImages, { content, id, status }) => {

        if (content && id && status !== "Existing" && status !== "Linked") {
          matchingImages.push({ newcontent: content, id });
        }
        return matchingImages;
      },
      []
    );

    // if images are available, all conditions are met
    if (filteredImages.length > 0) {
      acc.push({
        productIndex: i,
        name: val.productName,
        category: val.productCategory,
        filteredImages,
      });
    }
  }
  return acc;
}, []);

console.log(expectedData);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related