7
\$\begingroup\$

I found Python's defaultdict and OrderedDict incredibly useful, hence I attempted an implementation in JavaScript. As JavaScript objects only support string as key, it is not as powerful as the Python version. Can anyone suggest a way of using an object as a key in JS, like some kind of hash function? Also, is there anything wrong with this implementation?

(function () {
  'use strict';

  function forEach(collection, func, thisArg) {
    var i;
    if (Array.isArray(collection)) {
      if (thisArg) {
        for (i = 0; i < collection.length; ++i) {
          if (func.call(thisArg, collection[i], i) === false) {
            break;
          }
        }
      } else {
        for (i = 0; i < collection.length; ++i) {
          if (func.call(collection[i], collection[i], i) === false) {
            break;
          }
        }
      }
    } else {
      if (thisArg) {
        for (i in collection) {
          if (func.call(thisArg, collection[i], i) === false) {
            break;
          }
        }
      } else {
        for (i in collection) {
          if (func.call(collection[i], collection[i], i) === false) {
            break;
          }
        }
      }
    }
    return collection;
  }

  function deepCopy(obj) {
    if (typeof obj != 'object') {
      return obj;
    }
    var newObj = Array.isArray(obj) ? new Array(obj.length) : {};
    forEach(obj, function (v, k) {
      newObj[k] = v;
    });
    return newObj;
  }

  function assert(expression) {
    if (!expression) {
      throw 'Assertion faild';
    }
  }

  function implicitEqual(val1, val2) {
    return val1 == val2;
  }

  function explicitEqual(val1, val2) {
    return val1 === val2;
  }

  function deleteFromArray(arr, value, flag) {
    var ret = null;
    var method = flag == 'i' ? implicitEqual : explicitEqual;
    forEach(arr, function (val, index) {
      if (method(val, value)) {
        arr.splice(index, 1);
        ret = val;
        return false;
      }
    })
    return ret;
  }

  function createChildClass(parent, constructor) {
    constructor.prototype = deepCopy(parent.prototype);
    constructor._parent = parent;
    return constructor;
  }

  var DictionaryCollection = (function () {

    function DictionaryCollection() {
      this.dictionary = {};
    }

    DictionaryCollection.prototype.set = function (key, value) {
      this.dictionary[key] = value;
      return this;
    };

    DictionaryCollection.prototype.applyFunction = function (key, func) {
      this.set(key, func(this.get(key)));
      return this;
    };

    DictionaryCollection.prototype.get = function (key) {
      return this.dictionary[key];
    };

    DictionaryCollection.prototype.delete = function (key) {
      this.dictionary[key] = null;
      delete this.dictionary[key];
    };

    return DictionaryCollection;
  })();

  var DefaultDict = (function () {
    var DefaultDict = createChildClass(DictionaryCollection, function DefaultDict(defaultFunction) {
      if (typeof defaultFunction != 'function') {
        throw 'Default function must be a function';
      }
      DictionaryCollection.call(this);
      this.defaultFunction = defaultFunction;
    });

    DefaultDict.prototype.get = function (key) {
      return this.dictionary.hasOwnProperty(key) ? this.dictionary[key] : (this.dictionary[key] = this.defaultFunction());
    };
    return DefaultDict;

  })();

  var OrderedDict = (function () {
    var OrderedDict = createChildClass(DictionaryCollection, function OrderedDict() {
      DictionaryCollection.call(this);
      this.orderArray = [];
    });

    // @override
    OrderedDict.prototype.set = function (key, value) {
      if (!this.dictionary.hasOwnProperty(key)) {
        this.orderArray.push(key);
      }
      DictionaryCollection.prototype.set.apply(this, arguments);
      return this;
    };

    // @override
    OrderedDict.prototype.delete = function (key) {
      DictionaryCollection.prototype.delete.apply(this, arguments);
      deleteFromArray(this.orderArray, key);
    };

    OrderedDict.prototype.getNth = function (index) {
      return this.dictionary[this.orderArray[index]];
    };

    OrderedDict.prototype.toArray = function () {
      var result = [];
      forEach(this.orderArray, function (key) {
        result.push([key, this.get(key)]);
      }, this);
      return result;
    };

    return OrderedDict;
  })();



  function testDC() {
    var dict = new DictionaryCollection();
    dict.set('a', 1);
    dict.set('a', 2);
    dict.set('b', 3);
    assert(dict.get('a') === 2);
    assert(dict.get('b') === 3);
    dict.applyFunction('b', function (val) {
      return ++val;
    });
    assert(dict.get('b') === 4);
    dict.delete('b');
    assert(dict.get('b') === undefined);
  }

  function testDD() {
    var dict = new DefaultDict(function () {
      return 70;
    });
    dict.set('a', 50);
    assert(dict.get('a') === 50);
    assert(dict.get('b') === 70);
  }

  function testOD() {
    var dict = new OrderedDict();
    dict.set('a', 50);
    dict.set('b', -1);
    dict.set('c', -3);

    assert(dict.getNth(0) === 50);
    assert(dict.getNth(1) === -1);
    assert(dict.getNth(2) === -3);

    var arr = dict.toArray();

    assert(arr[0][0] == 'a');
    assert(arr[0][1] === 50);

    assert(arr[1][0] == 'b');
    assert(arr[1][1] === -1);

    assert(arr[2][0] == 'c');
    assert(arr[2][1] === -3);

    dict.delete('b');
    assert(dict.get('b') === undefined);

    arr = dict.toArray();
    assert(arr[1][0] != 'b');
    assert(arr[1][1] !== -1);
    assert(arr[1][0] == 'c');
    assert(arr[1][1] === -3);
  }

  testDC();
  testDD();
  testOD();


  /*
   * example, using DefaultDict to count duplicates in an array
   */

  function int(value) {
    return value ? parseInt(value) : 0;
  }

  function countDuplicatesArray(arr) {
    var dict = new DefaultDict(int);
    forEach(arr, function (val) {
      dict.applyFunction(val, function (x) {
        return ++x;
      });
    });
    return dict.dictionary;
  }

  console.log(
    countDuplicatesArray([0, 0, 0, 0, 5, 0, 7, 8, 7, 5, 78, 8, 7, 8, 78, 0, 0, 0, 1, 1, 5, 7])
  );

})();
\$\endgroup\$
3
\$\begingroup\$

Readability

Method forEach has 4 nested if-statements, with each statement doing things a little differently. This is a good candidate to refactor for compactness. There are 2 distinct ways you are looping arg collection, so I suggest to make a method for each of them.


(1) Complex Object

One for looping a complex object:

  function forEach(collection, func, thisArg) {
    var i;
    if (Array.isArray(collection)) {
       // .. 
    }
    else {
       // .. this part
    }
  }

New method propertyEach:

function propertyEach(source, iteratee) {
  for (const key in source) {
    if (iteratee(source[key], key, source) === false) {
      break;
    }
  }
  return source;
}

As an example:

const myComplexObject = {
  name: 'Doe',
  firstName: 'John'
};

propertyEach(myComplexObject, (item, i) => console.log(i + ': ' + item));

yielding

'name: Doe'  
'firstName: John'

(2) Array

And one for looping an array:

  function forEach(collection, func, thisArg) {
    var i;
    if (Array.isArray(collection)) {
       // .. this part
    }
    else {
       // .. 
    }
  }

New method arrayEach:

// courtesy of lodash
function arrayEach(array, iteratee) {
  var index = -1,
      length = array == null ? 0 : array.length;

  while (++index < length) {
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}

As an example:

const myArray = [ 1, 2, 3 ];
arrayEach(myArray, (item, i) => console.log(i + ': ' + item));

yielding

'0: 1'  
'1: 2'  
'2: 3'  

Don't Repeat Yourself (DRY)

Then we can refactor your method to call either method depending on whether the source is an array and avoid redudancy when thisArg is undefined.

function forEach(source, func, thisArg) {
  if (thisArg === undefined) thisArg = source;
  if (Array.isArray(source)) {
    arrayEach(source, (item, i) => func.call(thisArg, item, i));
  } else {
    propertyEach(source, (item, i) => func.call(thisArg, item, i));
  }
}

As compared to the initial beast:

  function forEach(collection, func, thisArg) {
    var i;
    if (Array.isArray(collection)) {
      if (thisArg) {
        for (i = 0; i < collection.length; ++i) {
          if (func.call(thisArg, collection[i], i) === false) {
            break;
          }
        }
      } else {
        for (i = 0; i < collection.length; ++i) {
          if (func.call(collection[i], collection[i], i) === false) {
            break;
          }
        }
      }
    } else {
      if (thisArg) {
        for (i in collection) {
          if (func.call(thisArg, collection[i], i) === false) {
            break;
          }
        }
      } else {
        for (i in collection) {
          if (func.call(collection[i], collection[i], i) === false) {
            break;
          }
        }
      }
    }
    return collection;
  }

The previous examples through your refactored method:

forEach(myArray, (item, i) => console.log(i + ': ' + item));
forEach(myComplexObject, (item, i) => console.log(i + ': ' + item));
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.