src/utils/operator.js

// namespaces
var dwv = dwv || {};
dwv.utils = dwv.utils || {};

/**
 * Check if the input is a generic object, including arrays.
 *
 * @param {*} unknown The input to check.
 * @returns {boolean} True if the input is an object.
 * ref: https://github.com/jashkenas/underscore/blob/1.9.1/underscore.js#L1319-L1323
 */
dwv.utils.isObject = function (unknown) {
  var type = typeof unknown;
  return type === 'function' || type === 'object' && !!unknown;
};

/**
 * Check if the input is an array.
 *
 * @param {*} unknown The input to check.
 * @returns {boolean} True if the input is an array.
 * ref: https://github.com/jashkenas/underscore/blob/1.9.1/underscore.js#L1313-L1317
 */
dwv.utils.isArray = function (unknown) {
  return Array.isArray(unknown);
};

/**
 * Dump an object to an array.
 *
 * @param {object} obj The input object as: {key0: {}, key1: {}}
 * @returns {Array} The corresponding array:
 *   [{name: key0, {}}, {name: key1, {}}]
 */
dwv.utils.objectToArray = function (obj) {
  var array = [];
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; ++i) {
    var key = keys[i];
    var row = {name: key};
    var innerKeys = Object.keys(obj[key]);
    for (var j = 0; j < innerKeys.length; ++j) {
      var innerKey = innerKeys[j];
      var value = obj[key][innerKey];
      if (dwv.utils.isArray(value)) {
        var arrayValues = [];
        for (var k = 0; k < value.length; ++k) {
          if (dwv.utils.isObject(value[k])) {
            arrayValues.push(dwv.utils.objectToArray(value[k]));
          } else {
            arrayValues.push(value[k]);
          }
        }
        value = arrayValues;
      } else if (dwv.utils.isObject(value)) {
        value = dwv.utils.objectToArray(value);
      }
      row[innerKey] = value;
    }
    array.push(row);
  }
  return array;
};

/**
 * Merge two similar objects.
 * Objects need to be in the form of:
 * <code>
 * {
 *   idKey: {valueKey: 0},
 *   key0: {valueKey: "abc"},
 *   key1: {valueKey: 33}
 * }
 * </code>
 * Merged objects will be in the form of:
 * <code>
 * {
 *   idKey: {valueKey: [0,1,2], merged: true},
 *   key0: {valueKey: {
 *     0: {valueKey: "abc"},
 *     1: {valueKey: "def"},
 *     2: {valueKey: "ghi"}
 *   }},
 *   key1: {valueKey: {
 *     0: {valueKey: 33},
 *     1: {valueKey: 44},
 *     2: {valueKey: 55}
 *   }}
 * }
 * </code>
 *
 * @param {object} obj1 The first object, can be the result of a previous merge.
 * @param {object} obj2 The second object.
 * @param {string} idKey The key to use as index for duplicate values.
 * @param {string} valueKey The key to use to access object values.
 * @returns {object} The merged object.
 */
dwv.utils.mergeObjects = function (obj1, obj2, idKey, valueKey) {
  var res = {};
  // check id key
  if (!idKey) {
    throw new Error('Cannot merge object with an undefined id key: ' + idKey);
  } else {
    if (!Object.prototype.hasOwnProperty.call(obj1, idKey)) {
      throw new Error('Id key not found in first object while merging: ' +
                idKey + ', obj: ' + obj1);
    }
    if (!Object.prototype.hasOwnProperty.call(obj2, idKey)) {
      throw new Error('Id key not found in second object while merging: ' +
                idKey + ', obj: ' + obj2);
    }
  }
  // check value key
  if (!valueKey) {
    throw new Error('Cannot merge object with an undefined value key: ' +
      valueKey);
  }

  // check if merged object
  var mergedObj1 = false;
  if (Object.prototype.hasOwnProperty.call(obj1[idKey], 'merged') &&
    obj1[idKey].merged) {
    mergedObj1 = true;
  }
  // handle the id part
  if (!Object.prototype.hasOwnProperty.call(obj1[idKey], valueKey)) {
    throw new Error('Id value not found in first object while merging: ' +
            idKey + ', valueKey: ' + valueKey + ', ojb: ' + obj1);
  }
  if (!Object.prototype.hasOwnProperty.call(obj2[idKey], valueKey)) {
    throw new Error('Id value not found in second object while merging: ' +
            idKey + ', valueKey: ' + valueKey + ', ojb: ' + obj2);
  }
  var id1 = obj1[idKey][valueKey];
  var id2 = obj2[idKey][valueKey];
  // for merged object, id1 is an array
  if (mergedObj1) {
    // check if array does not include id2
    for (var k = 0; k < id1.length; ++k) {
      if (id1[k] === id2) {
        throw new Error('The first object already contains id2: ' +
                    id2 + ', id1: ' + id1);
      }
    }
    res[idKey] = obj1[idKey];
    res[idKey][valueKey].push(id2);
  } else {
    if (id1 === id2) {
      throw new Error('Cannot merge object with same ids: ' +
                id1 + ', id2: ' + id2);
    }
    // create merge object
    res[idKey] = {value: [id1, id2], merged: true};
  }

  // get keys
  var keys1 = Object.keys(obj1);
  // keys2 without duplicates of keys1
  var keys2 = Object.keys(obj2).filter(function (item) {
    return keys1.indexOf(item) < 0;
  });
  var keys = keys1.concat(keys2);

  // loop through keys
  for (var i = 0, leni = keys.length; i < leni; ++i) {
    var key = keys[i];
    if (key !== idKey) {
      // first
      var value1 = null;
      var subValue1 = null;
      if (Object.prototype.hasOwnProperty.call(obj1, key)) {
        value1 = obj1[key];
        if (Object.prototype.hasOwnProperty.call(value1, valueKey)) {
          subValue1 = value1[valueKey];
        }
      }
      // second
      var value2 = null;
      var subValue2 = null;
      if (Object.prototype.hasOwnProperty.call(obj2, key)) {
        value2 = obj2[key];
        if (Object.prototype.hasOwnProperty.call(value2, valueKey)) {
          subValue2 = value2[valueKey];
        }
      }
      // result value
      var value;
      // create merge object if different values
      if (subValue2 !== subValue1) {
        value = {};
        // add to merged object or create new
        if (mergedObj1) {
          if (dwv.utils.isObject(subValue1)) {
            value[valueKey] = subValue1;
          } else {
            // merged object with repeated value
            // copy it with the index list
            value[valueKey] = {};
            for (var j = 0; j < id1.length; j++) {
              value[valueKey][id1[j]] = value1;
            }
          }
          // add obj2 value
          value[valueKey][id2] = value2;
        } else {
          // create merge object
          var newValue = {};
          newValue[id1] = value1;
          newValue[id2] = value2;
          value[valueKey] = newValue;
        }
      } else {
        value = value1;
      }
      // store value in result object
      res[key] = value;
    }
  }
  return res;
};