Saturday, January 27, 2018

Fastest way to flatten / un-flatten nested JSON objects

Fastest way to flatten / un-flatten nested JSON objects

Solution 1:

// Reference:
// https://github.com/henrytseng/dataobject-parser

/**
 * Dependencies
 */

// True if Object
function _isObject(value) {
  return typeof(value) === 'object' && value !== null;
}

// True if Array
function _isArray(value) {
  return Array.isArray(value);
}

/// True if type is string
function _isString(value) {
  return typeof(value) === 'string';
}

// True if undefined
function _isUndefined(value) {
  return typeof(value) === 'undefined';
}

// True if Number
function _isNumber(value) {
  return typeof(value) === 'number';
}

// True if Boolean
function _isBoolean(value) {
  return typeof(value) === 'boolean';
}

// True if Date object
function _isDate(value) {
  return value instanceof Date;
}

function DataObjectParser($data){
  this._data = $data || {};
}

/**
 * Given a dot deliminated string set will create an object
 * based on the structure of the string with the desired value
 *
 * @param {[String} $path  path indicating where value should be placed
 * @param {Mixed} $value   the value desired to be set at the location determined by path
 */
DataObjectParser.prototype.set = function($path, $value) {
  if(!$path || $path==='') return void 0;

  var _self = this;
  var re = /[\$\w-|]+|\[\]|([^\[[\w]\]]|["'](.*?)['"])/g;
  // parse $path on dots, and brackets
  var pathList = $path.match(re);
  var parent = this._data;
  var parentKey;
  var grandParent = null;
  var grandParentKey = null;

  var addObj = function($obj, $key, $data) {
    if($key === '[]') {
      $obj.push($data);
    } else {
      $obj[$key] = $data;
    }
  };

  while(pathList.length > 0) {
    parentKey = pathList.shift().replace(/["']/g, '');

    // Number, treat it as an array
    if (!isNaN(+parentKey) || parentKey === "[]") {
      if(!_isArray(parent)  /* prevent overwritting */ ) {
        parent = [];
        addObj(grandParent, grandParentKey, parent);
      }

    // String, treat it as a key
    } else if (_isString(parentKey)) {
      if(!_isObject(parent)) {
        parent = {};
        addObj(grandParent, grandParentKey, parent);
      }
    }
    // Next
    grandParent = parent;
    grandParentKey = parentKey;
    parent = parent[parentKey];
  }

  addObj(grandParent, grandParentKey, $value);
  return this;
};

/**
 * Returns the value defined by the path passed in
 *
 * @param  {String} $path string leading to a desired value
 * @return {Mixed}        a value in an object
 */
DataObjectParser.prototype.get = function($path) {
  var data = this._data;
  var regex = /[\$\w-|]+|\[\]|([^\[[\w]\]]|["'](.*?)['"])/g;
  //check if $path is truthy
  if (!$path) return void 0;
  //parse $path on dots and brackets
  var paths = $path.match(regex);
  //step through data object until all keys in path have been processed
  while (data !== null && paths.length > 0) {
    if(data.propertyIsEnumerable(paths[0].replace(/"/g, ''))){
      data = data[paths.shift().replace(/"/g, '')];
    }
    else{
      return undefined;
    }
  }
  return data;
};

DataObjectParser.prototype.data = function($data) {
  if(!_isUndefined($data)) {
    this._data = $data;
    return this;
  }
  return this._data;
};

/**
 * "Transposes" data; receives flat data and returns structured
 *
 * @param  {Object}           $data Structured object
 * @return {DataObjectParser} An instance of a DataObjectParser
 */
DataObjectParser.transpose = function($flat) {
  var parser = (new DataObjectParser());
  for(var n in $flat) {
    if($flat[n]!==undefined) {
      parser.set(n, $flat[n]);
    }
  }
  return parser;
};

/**
 * "Untransposes" data object; opposite of transpose
 *
 * @param  {Mixed}  $structured A Object or a DataObjectParser
 * @return {Object}             Flat object
 */
DataObjectParser.untranspose = function($structured) {
  //check to see if $structured is passed
  $structured = $structured || {};
  //handles if an object or a dataObjectParser is passed in
  var structuredData = $structured._data || $structured;

  var traverse = function($data, $isIndex) {
    var result = [];

    var createMapHandler = function($name, $data) {
      return function($item, $i) {
        var name = $name;
        //check if $name is a key of form "hello.world"
        if((/\./).test($name)) name = '["'+name+'"]';
        //add name to $item.key
        $item.key.unshift(name+".");
        //return $item.key with updated key
        return {
          key: $item.key,
          data: $item.data
        };
      };
    };

    for(var name in $data) {
      var modifiedName;
      // check if current name is an arrays index
      if($isIndex) modifiedName = "["+name+"]";
      else modifiedName = name;

      // check if current name is linked to a value
      if(_isString($data[name]) || _isNumber($data[name]) || $data[name]===null || _isBoolean($data[name]) || _isDate($data[name])) {
        if((/\./).test(name)) modifiedName = '["'+name+'"]';
        result.push({
          key: [modifiedName],
          data: $data[name]
        });
      }

      // check if current name is an array
      else if(_isArray($data[name])) {
        // tell traverse next name is an array's index
        var subArray = traverse($data[name],true);
        result = result.concat(subArray.map(createMapHandler(modifiedName, $data)));
      }

      //check if current name is an object
      else if(_isObject($data[name])) {
        var subObject = traverse($data[name],false);
        result = result.concat(subObject.map(createMapHandler(modifiedName, $data)));
      }
    }
    return result;
  };

  var flatArray = traverse(structuredData,false);
  var flatObj = {};

  flatArray.every(function($item) {
    //check for any dots followed by brackets and remove the dots
    for(var i = 0;i<$item.key.length-1;i++){
      var name = $item.key[i];
      var nextName = $item.key[i+1];
      if((/^\[/).test(nextName)){
        $item.key[i] = name.replace(/\.$/,"");
      }
    }
    //join all the keys in flatArray to form one key
    flatObj[$item.key.join("")] = $item.data;
    return true;
  });
  return flatObj;
};

var d = new DataObjectParser();

d.set("User.caravan.personel.leader","Travis");
d.set("User.caravan.personel.cook","Brent");
d.set("User.location.rooms[0]", "kitchen");
d.set("User.location.rooms[1]", "bathroom");
d.set("User.location.rowArr[0][0].Name", "Jun 00");
d.set("User.location.rowArr[0][0].Age", 19);
d.set("User.location.rowArr[0][1].Name", "Jun 01");
d.set("User.location.rowArr[0][1].Age", 20);
d.set("User.location.rowArr[1][0].Name", "Jun 10");
d.set("User.location.rowArr[1][0].Age", 21);
d.set("User.location.rowArr[1][1].Name", "Jun 11");
d.set("User.location.rowArr[1][1].Age", 22);
var obj1 = d.data();
console.log(obj1);

var flat = DataObjectParser.untranspose(obj1);
var obj2 = DataObjectParser.transpose(flat).data();

console.log(flat);
console.log(obj2);

Solution 2:

// Reference:
// https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects
// https://stackoverflow.com/questions/24833379/why-and-when-do-we-need-to-flatten-json-objects
// https://stackoverflow.com/questions/7793811/convert-javascript-dot-notation-object-to-nested-object
// https://github.com/henrytseng/dataobject-parser

function unflatten(table) {
    var result = {};

    for (var path in table) {
        var cursor = result, length = path.length, property = "", index = 0;

        while (index < length) {
            var char = path.charAt(index);

            if (char === "[") {
                var start = index + 1,
                    end = path.indexOf("]", start),
                    cursor = cursor[property] = cursor[property] || [],
                    property = path.slice(start, end),
                    index = end + 1;
            } else {
                var cursor = cursor[property] = cursor[property] || {},
                    start = char === "." ? index + 1 : index,
                    bracket = path.indexOf("[", start),
                    dot = path.indexOf(".", start);

                if (bracket < 0 && dot < 0) var end = index = length;
                else if (bracket < 0) var end = index = dot;
                else if (dot < 0) var end = index = bracket;
                else var end = index = bracket < dot ? bracket : dot;

                var property = path.slice(start, end);
            }
        }

        cursor[property] = table[path];
    }

    return result[""];
}

var flatten = (function (isArray, wrapped) {
    return function (table) {
        return reduce("", {}, table);
    };

    function reduce(path, accumulator, table) {
        if (isArray(table)) {
            var length = table.length;

            if (length) {
                var index = 0;

                while (index < length) {
                    var property = path + "[" + index + "]", item = table[index++];
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else accumulator[path] = table;
        } else {
            var empty = true;

            if (path) {
                for (var property in table) {
                    var item = table[property], property = path + "." + property, empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else {
                for (var property in table) {
                    var item = table[property], empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            }

            if (empty) accumulator[path] = table;
        }

        return accumulator;
    }
}(Array.isArray, Object));

let obj1 = {
  "User": {
   "UserID": 999,
    "Username": "Jun",
    "IsEnabled": true,
    "PermissionArr": {
     "52": ["VIEW", "UPDATE"],
      "53": ["VIEW", "UPDATE"],
      "54": [{"Name": "View", "Status": true}, {"Name": "Create", "Status": false}],
    },
    "CreditCardArr": [
      {"Num": "12345"},
      {"Num": "6789"},
    ],
    "RowArr": [
     [{"Name": "Jun 00"}, {"Name": "Jun 01"}],
      [{"Name": "Jun 10"}, {"Name": "Jun 11"}],
    ],
  }
};

let data1 = flatten(obj1);
let obj2 = unflatten(data1);
//console.log(data1);
console.log(obj2);

Reference:

https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects

https://stackoverflow.com/questions/7793811/convert-javascript-dot-notation-object-to-nested-object

No comments: