Search code examples
javascriptarraysalgorithmobject

How do I conditionally deep merge objects in JavaScript with multiple keys


The project I am working on is a trilingual dictionary. I have a database of words, and each word contains (among other things) an array of spelling variations and an array of objects representing definitions in all three languages, as seen below:

// Word object
{
  variants: ["chuyuchik", "chuyuchiikk"],
  definitions: [
    {
      en: {
        translation: "English definition",
        example: "English example",
      },
      es: {
        translation: "Spanish definition",
        example: "Spanish example",
      },
      tz: {
        example: "Tz'utujil example",
      },
    },
}

I'm trying to implement a feature where new words can be added to the database but if a word is a duplicate (based on what the variants array contains) they are merged. My problem occurs when trying to merge the definitions arrays in the two words. The flow I'm trying to achieve is as follows:

  • Properties that are already in the original word take precedence and should not ever be overwritten.
  • If a translation in the new word is a duplicate of an existing translation in the original word (in the same language), but the new word has an example attached to it then that example should be put into the merged word in its respective place.
  • If there is a completely new translation, that translation and its example (if it exists) should be appended to the definitions array.
  • There should never be duplicate values in any field of the same language throughout the word (e.g., the first object's English example should never be the same as the second object's English example). In the case of duplicate values, the value that is from the old word should be preserved and the other value should become an empty string.

I've tried a bunch of different ways to achieve this - maps, loops, flattening out the objects - I can't seem to wrap my mind around it. Any suggestions?

Here's an example of what the before and after should be:

// Original definitions in word
const oldDefs = [{
  en: {
    translation: "EN 1",
    example: "ENGLISH EX 1",
  },
  es: {
    translation: "ES 1",
    example: "SPANISH EX 1",
  },
  tz: {
    example: "TZ EXAMPLE 1",
  },
}, {
  en: {
    translation: "EN 2",
    example: "",
  },
  es: {
    translation: "",
    example: "",
  },
  tz: {
    example: "",
  },
}]

// The definitions to merge into the original
const newDefs = [{
  en: {
    translation: "EN 2",
    example: "ENGLISH EX 2",
  },
  es: {
    translation: "ES 2",
    example: "SPANISH EX 2",
  },
  tz: {
    example: "TZ EXAMPLE 1",
  },
}]

// What the output should be
const output = [{
  en: {
    translation: "EN 1",
    example: "ENGLISH EX 1",
  },
  es: {
    translation: "ES 1",
    example: "SPANISH EX 1",
  },
  tz: {
    example: "TZ EXAMPLE 1",
  },
}, {
  en: {
    translation: "EN 2",
    example: "ENGLISH EX 2",
  },
  es: {
    translation: "ES 2",
    example: "SPANISH EX 2",
  },
  tz: {
    example: "",
  },
}]


Solution

    • Create a copy of the old definitions array (i.e., res).

    • Iterate over each new definition (b) in newDefs.

    • For each new definition, check if it matches any existing definition (a) in res.

      • If a lang in the new definition does not exist in the old definition, initialize it.
      • If the translation in the new definition matches the translation in the old definition and the example in the new definition is non-empty and the old definition’s example is empty, update the old definition’s example.
    • If the translation in the new definition does not exist in the old one, add the new translation and example to the old one.

    • If no match is found for a new definition after checking all existing definitions, add the new one to res.

    • Use a Set() to keep track of visited examples for each language.

    • If an example is already visited, set the duplicate example to an empty string.

    function _merge(A, B) {
      const res = [...A];
    
      B.forEach((b) => {
        let flag = false;
    
        res.forEach((a) => {
          Object.keys(b).forEach((lang) => {
            if (!a[lang]) {
              a[lang] = {
                translation: "",
                example: "",
              };
            }
    
            if (
              b[lang].translation &&
              b[lang].translation === a[lang].translation
            ) {
              if (b[lang].example && !a[lang].example) {
                a[lang].example = b[lang].example;
              }
              flag = true;
            } else if (b[lang].translation && !a[lang].translation) {
              a[lang].translation = b[lang].translation;
              a[lang].example = b[lang].example || "";
              flag = true;
            }
          });
        });
    
        if (!flag) {
          res.push({ ...b });
        }
      });
    
      res.forEach((def) => {
        Object.keys(def).forEach((lang) => {
          const vis = new Set();
          res.forEach((d) => {
            if (d[lang] && d[lang].example) {
              if (vis.has(d[lang].example)) {
                d[lang].example = "";
              } else {
                vis.add(d[lang].example);
              }
            }
          });
        });
      });
    
      return res;
    }
    
    const oldDefs = [
      {
        en: {
          translation: "EN 1",
          example: "ENGLISH EX 1",
        },
        es: {
          translation: "ES 1",
          example: "SPANISH EX 1",
        },
        tz: {
          example: "TZ EXAMPLE 1",
        },
      },
      {
        en: {
          translation: "EN 2",
          example: "",
        },
        es: {
          translation: "",
          example: "",
        },
        tz: {
          example: "",
        },
      },
    ];
    
    const newDefs = [
      {
        en: {
          translation: "EN 2",
          example: "ENGLISH EX 2",
        },
        es: {
          translation: "ES 2",
          example: "SPANISH EX 2",
        },
        tz: {
          example: "TZ EXAMPLE 1",
        },
      },
    ];
    
    console.log(_merge(oldDefs, newDefs));