15
\$\begingroup\$

I've been writing far too many routines in JavaScript that get a record from a an object or array based off a matching property, much like jQuery does with DOM elements. Since all the good names are taken, I called it Hmm.

I went with the philosophy that if I don't need it yet, I wont provide for it.

var Hmm = (function()
{ //A snippet/library to find records in table-like JS arrays

  function getNamedValue( o , name  )
  { //Name can have dots, 'car.tire.brand.name' should get the expected
    var parts = name.split(".");
    while( parts.length && o ){
      o = o[parts.shift()];    
    }
    return o;
  }  

  function unique( )
  { //Assuming `this` is an array, will copy all unique entries of `this` to a returned new array
    var array = [], map = {}, json;
    this.forEach( function( value ){
      if( value instanceof Object ){
        json = JSON.stringify( value );
        if( !map[json] ){
          array.push( value );
          map[json] = true;
        }
      }
      else if( !~array.indexOf( value ) ){
        array.push( value );
      }
    });
    return array;
  }  

  function Hmm( data )
  { //data could be a string -> treat as JSON, parse it
    //data could be an object, or a string that got converted to an object -> convert to array
    //data could be an array ( original or through conversion ) -> leave as is
    this.data = data instanceof Object ? data : JSON.parse( data );
    this.data = this.data instanceof Array ? this.data  : [ this.data  ];    
  }

  Hmm.prototype.index = function( name )
  { //Name can have dots, 'car.tire.brand.name' should get the expected
    //Generates an index object 
    var index = {} , i;

    this.data.forEach( function( value )
    {
      var key   = getNamedValue( value , name ),
          keys  = key instanceof Array ? key : [key];
      for( i = 0 ; i < keys.length ; i++ ) 
      {
        key = keys[i];
        index[key] = index[key] || [];
        index[key].push( value );
      }
    });
    return index;
  };  

  Hmm.prototype.uniqueIndex = function( name )
  { //Create an index, point each index to the first of possible values
    var index = this.index( name );
    Object.keys( index ).forEach( function(key){
      index[key] = index[key][0];
    });
    return index;
  };

  Hmm.prototype.collect = function( name )
  { //Collect all the values for a given property name
    var values = this.data.map( function ( o ) {
      return getNamedValue( o , name );
    });
    values.unique = unique;
    return values;
  };

  Hmm.prototype.toString = function()
  { //More for jsbin than anything else
    return JSON.stringify( this.data );
  };

  return Hmm;

}());  

//Some tests

var foods = [

  { name : 'potatoes' , calories : 100 , allergens : [] , storage : 'ambient' },
  { name : 'pistachio ice cream' , calories : 1000 , allergens : ['nuts','milk'] , storage : 'frozen'  },
  { name : 'chocolate ice cream' , calories : 1200 , allergens : ['milk'] , storage : 'frozen' },
  { name : '2% fat milk' , calories : 600 , allergens : ['milk'] , storage : 'chilled' }  
];

var hmm = new Hmm( foods );

console.log( hmm.collect('name') );                 //Collect all names
console.log( hmm.collect('allergens') );            //
console.log( hmm.collect('allergens').unique() );
console.log( hmm.collect('allergens.0') );
console.log( hmm.collect('allergens.0').unique().sort() );
console.log( hmm.index('name') );
console.log( hmm.index('name').potatoes );
console.log( hmm.index('allergens').milk );
console.log( hmm.uniqueIndex('name') );
console.log( hmm.uniqueIndex('name').potatoes );
console.log( hmm.index('allergens') );
console.log( hmm.index('allergens.0') ); //Index on the first allergy
\$\endgroup\$
0
6
\$\begingroup\$

It's very nice. What few qualms I have are a result of me looking really hard for things to have qualms about (furrowing my brow and going "hmm", basically).

Anyway, some thoughts:

  • Your unique function relies on the assumption that there are no circular references in the object passed to JSON.stringify. It's probably a fair assumption, but an assumption nonetheless. Also, from a specification standpoint, JSON representation of objects doesn't impose or guarantee any ordering of properties. I highly doubt it'll ever be an issue anywhere, but it could mean that identical objects will have different JSON representations, and unique would thus include both those objects.

  • Similarly edge-case'y: Your instanceof usage is vulnerable to the usual cross-frame pitfalls.

  • The !~array.indexOf( value ) trick is a bit arcane. It's neat, but I don't think I've actually seen it used anywhere - except in some other code you posted :)
    A === -1 comparison isn't nearly as cool, but it wouldn't cause me to go "huh?". In other words: I like it, but I wouldn't call it obvious or idiomatic.

  • That curly-brace-on-new-line style just rubs me the wrong way, but you know what you're doing, so I'll spare you the "automatic semi-colon insertion" speech :)

On a more overall note: Like jQuery, it might be useful to always return a Hmm-wrapped array, instead of only adding unique to the array returned by collect. Of course it'd be quite a different API.

\$\endgroup\$
4
\$\begingroup\$

First off, a couple of your function names are slightly misleading/go against naming conventions in other libraries.

  • {hmm}.index seems to be more of a group function than a index finder.
  • {hmm}.collect appears to be a pluck function but I've heard it called collect as well so \o/
  • {hmm}.uniqueIndex should, imo, be called groupFirst

Alright, with that out of the way lets talk implementation

I think it would be useful for Hmm to inherit from Array as a pure wrapper - most of what you can do with an array you can do with an hmm instance (at least in modern browsers see Subclassing Javascript Arrays..

This would just involve refactoring your code a little:

var push = Array.prototype.push;
function Hmm( data ) {
  //data could be a string -> treat as JSON, parse it
  //data could be an object, or a string that got converted to an object -> convert to array
  //data could be an array ( original or through conversion ) -> leave as is
  var _data = data instanceof Object ? data : JSON.parse( data );
  _data = _data instanceof Array ? _data  : [ _data];
  push.apply(this, _data); //merge with this hmm instance
}

Hmm.prototype = [];
// ....

Disclaimer: this may be too clever and might not work in some environments :)

Next, you can decide if you want to rewrite map, filter, et al to produce a hmm rather than an array

Another advantage of doing this is you can define unique on hmm.prototype so

Object.keys(hmm.collect('allergens.0')) != ["0", "1", "2", "3", "unique"] which I would consider a bug.


Other stuff

Seeing you seem to be writing for modern environments anyway I would write your constructor logic

var _data = typeof data !== "string" ? data : JSON.parse( data );
this.data = Array.isArray(_data) ? this.data  : [ this.data  ];

If you want to convert Array-likes (such as a hmm or jQuery instance) to hmms I would use the underscore check see this issue for some discussion:

var _data = typeof data !== "string" ? data : JSON.parse( data );
this.data = _data && _data.length === +_data.length ? this.data  : [ this.data  ];

You may also want to consider catching the JSON.parse - if it fails wrap the string in the hmm object, however that may lead to weird edge cases.

I might come back to his later as I need to take off. Last thing as has been mentioned, you're not a hacker -- no need for !~array.indexOf( value ).. array.indexOf( value ) < 0 is the same number of bits and faster

\$\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.