skip to Main Content

I have two arrays of objects: publications and documentPrices
This is how they look:

const publications = [
  {
    id: "publication1",
    documents: [
      {
        id: "document1",
        priceId: "abc",
      },
      {
        id: "document2",
        priceId: "xyz",
      },
    ],
  },
  {
    id: "publication2",
    documents: [
      {
        id: "document3",
        priceId: "abc",
      },
      {
        id: "document4",
        priceId: "xyz",
      },
    ],
  },
]
const documentPrices = [
  { priceId: "abc", priceUsd: 100, priceEur: 120 },
  { priceId: "xyz", priceUsd: 10, priceEur: 12 },
]

I want to achieve the following output

const publicationsFinal = [
  {
    id: "publication1",
    documents: [
      {
        id: "document 1",
        priceId: "abc",
        priceUsd: 100,
        priceEur: 120,
      },
      {
        id: "document 2", 
        priceId: "xyz",
        priceUsd: 10,
        priceEur: 12,
      },
    ],
  },
  {
    id: "publication2",
    documents: [
      {
        id: "document 3",
        priceId: "abc",
        priceUsd: 100,
        priceEur: 120,
      },
      {
        id: "document 4",
        priceId: "xyz",
        priceUsd: 10,
        priceEur: 12,
      },
    ],
  },
]

At the moment, I have implemented this with 3 nested loops, which is highly inefficient. Also, I have checked other solutions on SO but haven’t found one that is relevant to the issue I am facing. Is there an efficient way to achieve the desired output?

3

Answers


  1. There always will be 3 loops. It can’t be done without them, but I wouldn’t think about performance if you have less than a million records.

    In-place version:

    const publications = [{
        id: "publication1",
        documents: [{
            id: "document1",
            priceId: "abc",
          },
          {
            id: "document2",
            priceId: "xyz",
          },
        ],
      },
      {
        id: "publication2",
        documents: [{
            id: "document3",
            priceId: "abc",
          },
          {
            id: "document4",
            priceId: "xyz",
          },
        ],
      },
    ]
    
    
    const documentPrices = [{
        priceId: "abc",
        priceUsd: 100,
        priceEur: 120
      },
      {
        priceId: "xyz",
        priceUsd: 10,
        priceEur: 12
      },
    ]
    
    for (const pub of publications) {
      for (const doc of pub.documents) {
        const { priceUsd, priceEur } = documentPrices.find(p => p.priceId === doc.priceId)
        doc.priceUsd = priceUsd
        doc.priceEur = priceEur
      }
    }
    
    
    console.log(publications)

    Creating a new object:

    const publications = [{
        id: "publication1",
        documents: [{
            id: "document1",
            priceId: "abc",
          },
          {
            id: "document2",
            priceId: "xyz",
          },
        ],
      },
      {
        id: "publication2",
        documents: [{
            id: "document3",
            priceId: "abc",
          },
          {
            id: "document4",
            priceId: "xyz",
          },
        ],
      },
    ]
    
    
    const documentPrices = [{
        priceId: "abc",
        priceUsd: 100,
        priceEur: 120
      },
      {
        priceId: "xyz",
        priceUsd: 10,
        priceEur: 12
      },
    ]
    
    const result = publications.map(pub => ({
      ...pub,
      documents: pub.documents.map(doc => ({
        ...doc,
        ...documentPrices.find(p => p.priceId === doc.priceId)
      }))
    }))
    
    console.log(result)
    Login or Signup to reply.
  2. It depends on whether you want to modify the objects in the publications array or create a new array with new objects. But unless you’re seeing a performance problem, I wouldn’t worry about it too much.

    Either way, you’re going to have two nested loops (publications and documents), there’s no getting around it. But you can avoid a third nested loop by building a Map of the price information up-front:

    const priceMap = new Map(documentPrices.map((entry) => [entry.priceId, entry]));
    

    Looking up the entries in the map is required by the JavaScript specification to be faster, in general, than looking up the entries in the array would have been. That is: sublinear.

    Once you have the map, if you just want to update the existing objects, you have your nested loops:

    for (const { documents } of publications) {
        for (const doc of documents) {
            const entry = priceMap.get(doc.priceId);
            if (entry) {
                doc.priceEur = entry.priceEur;
                doc.priceUsd = entry.priceUsd;
            }
        }
    }
    

    (I’ve used for-of loops there. These days, JavaScript engines are very nearly as fast at for-of as they are at for [when using standard arrays], but if you like you could use just a for loop instead.)

    Live example:

    const publications = [
        {
            id: "publication1",
            documents: [
                {
                    id: "document1",
                    priceId: "abc",
                },
                {
                    id: "document2",
                    priceId: "xyz",
                },
            ],
        },
        {
            id: "publication2",
            documents: [
                {
                    id: "document3",
                    priceId: "abc",
                },
                {
                    id: "document4",
                    priceId: "xyz",
                },
            ],
        },
    ];
    const documentPrices = [
        { priceId: "abc", priceUsd: 100, priceEur: 120 },
        { priceId: "xyz", priceUsd: 10, priceEur: 12 },
    ];
    
    const priceMap = new Map(documentPrices.map((entry) => [entry.priceId, entry]));
    
    for (const { documents } of publications) {
        for (const doc of documents) {
            const entry = priceMap.get(doc.priceId);
            if (entry) {
                doc.priceEur = entry.priceEur;
                doc.priceUsd = entry.priceUsd;
            }
        }
    }
    
    console.log(JSON.stringify(publications, null, 4));
    .as-console-wrapper {
        max-height: 100% !important;
    }

    If you want a new array with new objects, you’d have nested map calls (which are loops):

    const result = publications.map((publication) => {
        publication = {
            ...publication,
            documents: publication.documents.map((doc) => {
                const entry = priceMap.get(doc.priceId);
                if (entry) {
                    doc = {
                        ...doc,
                        priceEur: entry.priceEur,
                        priceUsd: entry.priceUsd,
                    };
                }
                return doc;
            }),
        };
        return publication;
    });
    

    Live example:

    const publications = [
        {
            id: "publication1",
            documents: [
                {
                    id: "document1",
                    priceId: "abc",
                },
                {
                    id: "document2",
                    priceId: "xyz",
                },
            ],
        },
        {
            id: "publication2",
            documents: [
                {
                    id: "document3",
                    priceId: "abc",
                },
                {
                    id: "document4",
                    priceId: "xyz",
                },
            ],
        },
    ];
    const documentPrices = [
        { priceId: "abc", priceUsd: 100, priceEur: 120 },
        { priceId: "xyz", priceUsd: 10, priceEur: 12 },
    ];
    
    const priceMap = new Map(documentPrices.map((entry) => [entry.priceId, entry]));
    
    const result = publications.map((publication) => {
        publication = {
            ...publication,
            documents: publication.documents.map((doc) => {
                const entry = priceMap.get(doc.priceId);
                if (entry) {
                    doc = {
                        ...doc,
                        priceEur: entry.priceEur,
                        priceUsd: entry.priceUsd,
                    };
                }
                return doc;
            }),
        };
        return publication;
    });
    
    console.log(JSON.stringify(result, null, 4));
    .as-console-wrapper {
        max-height: 100% !important;
    }

    If you don’t mind creating a new object even if the price information isn’t found, it can be a bit more concise:

    const result = publications.map((publication) => ({
        ...publication,
        documents: publication.documents.map((doc) => ({
            ...doc,
            // `get` returns `undefined` if not found, which is safe to use with property spread
            ...priceMap.get(doc.priceId),
        })),
    }));
    

    Live example:

    const publications = [
        {
            id: "publication1",
            documents: [
                {
                    id: "document1",
                    priceId: "abc",
                },
                {
                    id: "document2",
                    priceId: "xyz",
                },
            ],
        },
        {
            id: "publication2",
            documents: [
                {
                    id: "document3",
                    priceId: "abc",
                },
                {
                    id: "document4",
                    priceId: "xyz",
                },
            ],
        },
    ];
    const documentPrices = [
        { priceId: "abc", priceUsd: 100, priceEur: 120 },
        { priceId: "xyz", priceUsd: 10, priceEur: 12 },
    ];
    
    const priceMap = new Map(documentPrices.map((entry) => [entry.priceId, entry]));
    
    const result = publications.map((publication) => ({
        ...publication,
        documents: publication.documents.map((doc) => ({
            ...doc,
            // `get` returns `undefined` if not found, which is safe to use with property spread
            ...priceMap.get(doc.priceId),
        })),
    }));
    
    console.log(JSON.stringify(result, null, 4));
    .as-console-wrapper {
        max-height: 100% !important;
    }
    Login or Signup to reply.
  3. Turn documentPrices into a map:

    const documentPrices = [...]
    
    const documentPricesMap = documentPrices.reduce((acc,p) => {
        acc[p.priceId] = p
        return acc
    },{})
    

    and then merge:

    publications.forEach(p => {
        p.documents.forEach((d) => Object.assign(d,documentPricesMap[d.priceId]))
    })
    

    The above mutates, you can also do it by not mutating:

    const newPublications = publications.map(p => ({
          ...p,
          documents: p.documents.map(d => ({
            ...d,
            ...documentPricesMap[d.priceId]
          }))
       }
    }))
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search