Search code examples
javascriptobjectdata-structuresmergecomparison

Comparing 2 nested data-structures,target+source,what are appropriate merge-strategies for missing target values compared to their source counterpart?


What is a better way of doing this. I'am assigning either of two property values (from two different objects), depending on their existence, to a third data-structure.

In case the args object's value is nullish a non nullish value gets accessed from the default object and assigned to the final structure.

return {
  first: {
    visible: args.first?.visible ?? defaulttest.first?.visible,
    emoji: args.first?.emoji ?? defaulttest.first?.emoji,
    style: args.first?.style ?? defaulttest.first?.style,
  },
  back: {
    visible: args.back?.visible ?? defaulttest.back?.visible,
    emoji: args.back?.emoji ?? defaulttest.back?.emoji,
    style: args.back?.style ?? defaulttest.back?.style,
  },
  page: {
    visible: args.page?.visible ?? defaulttest.page?.visible,
    emoji: args.page?.emoji ?? defaulttest.page?.emoji,
    style: args.page?.style ?? defaulttest.page?.style,
  },
  forward: {
    visible: args.forward?.visible ?? defaulttest.forward?.visible,
    emoji: args.forward?.emoji ?? defaulttest.forward?.emoji,
    style: args.forward?.style ?? defaulttest.forward?.style,
  },

  last: {
    visible: args.last?.visible ?? defaulttest.last?.visible,
    emoji: args.last?.emoji ?? defaulttest.last?.emoji,
    style: args.last?.style ?? defaulttest.last?.style,
  },
  Mdelete: {
    visible: args.Mdelete?.visible ?? defaulttest.Mdelete?.visible,
    emoji: args.Mdelete?.emoji ?? defaulttest.Mdelete?.emoji,
    style: args.Mdelete?.style ?? defaulttest.Mdelete?.style,
  },
  removeBtn: {
    visible: args.removeBtn?.visible ?? defaulttest.removeBtn?.visible,
    emoji: args.removeBtn?.emoji ?? defaulttest.removeBtn?.emoji,
    style: args.removeBtn?.style ?? defaulttest.removeBtn?.style,
  },
};

Solution

  • From my above comments ...

    1/2 ... The OP actually is not really comparing. For a certain set of properties the OP looks up each property at a target object, and only in case it features a nullish value there will be an assignment from a source object's counterpart to the missing property. Thus an approach I would choose was ...

    2/2 ... implementing a generic function which merges two objects in a way that a source property can only be written/assigned in case the target structure does not already provide a non nullish value. This function then has to be invoked twice once for args and defaulttest and a second time for the to be returned entirely empty data structure and args.

    The above statement was a bit ambitious for there are at least 2 strategies of how one could achieve such kind of mergers.

    Thus the below provided example code implements two approaches

    • one, called refit, which follows a pushing/patching agenda due to forcing the assignement of every non nullish property in a source-object to its non nullish counterpart of a target-object.

    • a 2nd one, called revive, which resembles a pulling approach for it just reassigns the nullish target-object properties with their non nullish source-object counterparts.

    The difference in the results they produce for one and the same preset is going to be demonstrated herby ...

    // "refit" ... a pushing/patching approach.
    // - force the assignement of every non nullish property in source
    //   to its non nullish counterpart in target ... hence a *refit*.
    function refitNullishValuesRecursively(target, source) {
      if (
        // are both values array-types?
        Array.isArray(source) &&
        Array.isArray(target)
      ) {
        source
          // for patching always iterate the source items ...
          .forEach((sourceItem, idx) => {
            // ... and look whether a target counterpart exists.
            if (target[idx] == null) {
    
              // either assign an existing structured clone ...
              if (sourceItem != null) {
                target[idx] = cloneDataStructure(sourceItem);
              }
            } else {
              // ... or proceed recursively.
              refitNullishValuesRecursively(target[idx], sourceItem);
            }
          });
      } else if (
        // are both values object-types?
        source && target &&
        'object' === typeof source &&
        'object' === typeof target
      ) {
        Object
          // for patching ...
          .entries(source)
          // ... always iterate the source entries (key value pairs) ...
          .forEach(([key, sourceValue], idx) => {
            // ... and look whether a target counterpart exists.
            if (target[key] == null) {
    
              // either assign an existing structured clone ...
              if (sourceValue != null) {
                target[key] = cloneDataStructure(sourceValue);
              }
            } else {
              // ... or proceed recursively.
              refitNullishValuesRecursively(target[key], sourceValue);
            }
          });
      }
      return target;
    }
    // "revive" ... a pulling approach.
    // - just reassign the nullish target properties with their
    //   non nullish source counterparts ... hence a *revive*.
    function reviveNullishValuesRecursively(target, source) {
      if (
        // are both values array-types?
        Array.isArray(target) &&
        Array.isArray(source)
      ) {
        target
          // for fixing always iterate the target items.
          .forEach((targetItem, idx) => {
            if (targetItem == null) {
    
              // either assign an existing structured clone ...
              target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
            } else {
              // ... or proceed recursively.
              reviveNullishValuesRecursively(targetItem, source[idx]);
            }
          });
      } else if (
        // are both values object-types?
        target && source &&
        'object' === typeof target &&
        'object' === typeof source
      ) {
        Object
          // for fixing ...
          .entries(target)
          // ... always iterate the target entries (key value pairs).
          .forEach(([key, targetValue], idx) => {
            if (targetValue == null) {
    
              // either assign an existing structured clone ...
              target[key] = cloneDataStructure(source[key]) ?? targetValue;
            } else {
              // ... or proceed recursively.
              reviveNullishValuesRecursively(targetValue, source[key]);
            }
          });
      }
      return target;
    }
    const cloneDataStructure =
      ('function' === typeof structuredClone)
      && structuredClone
      || (value => JSON.parse(JSON.stringify(value)));
    
    const targetBlueprint = {
      x: { xFoo: 'foo', xBar: 'bar', xBaz: { xBiz: null } },
      y: { yFoo: 'foo', yBar: null },
    };
    const patch = {
      x: { xFoo: null, xBar: null, xBaz: { xBiz: 'biz' } },
      y: { yFoo: null, yBar: 'bar', yBaz: { yBiz: 'biz' } },
    };
    let target = cloneDataStructure(targetBlueprint);
    
    console.log('"refit" ... a pushing/patching approach.');
    console.log('before refit ...', { target, patch });
    refitNullishValuesRecursively(target, patch);
    console.log('after refit ...', { target, patch });
    
    target = cloneDataStructure(targetBlueprint);
    
    console.log('"revive" ... a pulling approach.');
    console.log('before revive ...', { target, patch });
    reviveNullishValuesRecursively(target, patch);
    console.log('after revive ...', { target, patch });
    .as-console-wrapper { min-height: 100%!important; top: 0; }

    As for the OP's example which targets the creation of kind of a config-object, one could fully patch/refit a clone of the temporary or current args-config, whereas within the last step one has control over the config-object's final structure by providing the most basic empty config-base which just gets revived by the before created full patch/refit-config.

    function refitNullishValuesRecursively(target, source) {
      if (Array.isArray(source) && Array.isArray(target)) {
        source
          .forEach((sourceItem, idx) => {
            if (target[idx] == null) {
    
              if (sourceItem != null) {
                target[idx] = cloneDataStructure(sourceItem);
              }
            } else {
              refitNullishValuesRecursively(target[idx], sourceItem);
            }
          });
      } else if (
        source && target &&
        'object' === typeof source &&
        'object' === typeof target
      ) {
        Object
          .entries(source)
          .forEach(([key, sourceValue], idx) => {
            if (target[key] == null) {
    
              if (sourceValue != null) {
                target[key] = cloneDataStructure(sourceValue);
              }
            } else {
              refitNullishValuesRecursively(target[key], sourceValue);
            }
          });
      }
      return target;
    }
    function reviveNullishValuesRecursively(target, source) {
      if (Array.isArray(target) && Array.isArray(source)) {
        target
          .forEach((targetItem, idx) => {
            if (targetItem == null) {
    
              target[idx] = cloneDataStructure(source[idx]) ?? targetItem;
            } else {
              reviveNullishValuesRecursively(targetItem, source[idx]);
            }
          });
      } else if (
        target && source &&
        'object' === typeof target &&
        'object' === typeof source
      ) {
        Object
          .entries(target)
          .forEach(([key, targetValue], idx) => {
            if (targetValue == null) {
    
              target[key] = cloneDataStructure(source[key]) ?? targetValue;
            } else {
              reviveNullishValuesRecursively(targetValue, source[key]);
            }
          });
      }
      return target;
    }
    const cloneDataStructure =
      ('function' === typeof structuredClone)
      && structuredClone
      || (value => JSON.parse(JSON.stringify(value)));
    
    const defaultConfig = {
      first: {
        visible: 'default.first.visible',
        emoji: 'default.first.emoji',
        style: 'default.first.style',
      },
      forward: {
        visible: 'default.forward.visible',
        emoji: 'default.forward.emoji',
        style: 'default.forward.style',
      },
      removeBtn: {
        visible: 'default.removeBtn.visible',
        emoji: 'default.removeBtn.emoji',
        style: 'default.removeBtn.style',
      },
    };
    const currentConfig = {
      first: {
        visible: 'current.first.visible',
        emoji: 'current.first.emoji',
        style: 'current.first.style',
      },
      forward: {
        visible: 'current.forward.visible',
        emoji: null,
      },
      FOO: {
        visible: 'current.FOO.visible',
        emoji: 'current.FOO.emoji',
        style: 'current.FOO.style',
      }
    };
    
    function getConfiguration(baseConfig) {
      return reviveNullishValuesRecursively(
        cloneDataStructure(baseConfig),
        refitNullishValuesRecursively(
          cloneDataStructure(currentConfig),
          defaultConfig,
        ),
      );
    }
    console.log(
      getConfiguration({
        first: null,
        forward: null,
        removeBtn: null,
      })
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }