1 /**
  2  * Emily.js - http://flams.github.com/emily/
  3  * Copyright(c) 2012-2013 Olivier Scherrer <pode.fr@gmail.com>
  4  * MIT Licensed
  5  */
  6 "use strict";
  7 
  8 /**
  9  * Get the closest number in an array
 10  * @param {Number} item the base number
 11  * @param {Array} array the array to search into
 12  * @param {Function} getDiff returns the difference between the base number and
 13  *   and the currently read item in the array. The item which returned the smallest difference wins.
 14  * @private
 15  */
 16 function _getClosest(item, array, getDiff) {
 17     var closest,
 18         diff;
 19 
 20     if (!array) {
 21         return;
 22     }
 23 
 24     array.forEach(function (comparedItem, comparedItemIndex) {
 25         var thisDiff = getDiff(comparedItem, item);
 26 
 27         if (thisDiff >= 0 && (typeof diff == "undefined" || thisDiff < diff)) {
 28             diff = thisDiff;
 29             closest = comparedItemIndex;
 30         }
 31     });
 32 
 33     return closest;
 34 }
 35 
 36 /**
 37 * @class
 38 * Tools is a collection of tools
 39 */
 40 module.exports = {
 41     /**
 42      * For applications that don't run in a browser, window is not the global object.
 43      * This function returns the global object wherever the application runs.
 44      * @returns {Object} the global object
 45      */
 46     getGlobal: function getGlobal() {
 47         return Function('return this')();
 48     },
 49 
 50     /**
 51      * Mixes an object into another
 52      * @param {Object} source object to get values from
 53      * @param {Object} destination object to mix values into
 54      * @param {Boolean} optional, set to true to prevent overriding
 55      * @returns {Object} the destination object
 56      */
 57     mixin: function mixin(source, destination, dontOverride) {
 58         this.loop(source, function (value, idx) {
 59             if (!destination[idx] || !dontOverride) {
 60                 destination[idx] = source[idx];
 61             }
 62         });
 63         return destination;
 64     },
 65 
 66     /**
 67      * Count the number of properties in an object
 68      * It doesn't look up in the prototype chain
 69      * @param {Object} object the object to count
 70      * @returns {Number}
 71      */
 72     count: function count(object) {
 73         var nbItems = 0;
 74         this.loop(object, function () {
 75             nbItems++;
 76         });
 77 
 78         return nbItems;
 79     },
 80 
 81     /**
 82      * Compares the properties of two objects and returns true if they're the same
 83      * It's doesn't do it recursively
 84      * @param {Object} first object
 85      * @param {Object} second object
 86      * @returns {Boolean} true if the two objets have the same properties
 87      */
 88     compareObjects: function compareObjects(object1, object2) {
 89         var getOwnProperties = function (object) {
 90             return Object.getOwnPropertyNames(object).sort().join("");
 91         };
 92         return getOwnProperties(object1) == getOwnProperties(object2);
 93     },
 94 
 95     /**
 96      * Compares two numbers and tells if the first one is bigger (1), smaller (-1) or equal (0)
 97      * @param {Number} number1 the first number
 98      * @param {Number} number2 the second number
 99      * @returns 1 if number1>number2, -1 if number2>number1, 0 if equal
100      */
101     compareNumbers: function compareNumbers(number1, number2) {
102           if (number1>number2) {
103             return 1;
104           } else if (number1<number2) {
105             return -1;
106           } else {
107              return 0;
108           }
109     },
110 
111     /**
112      * Transform array-like objects to array, such as nodeLists or arguments
113      * @param {Array-like object}
114      * @returns {Array}
115      */
116     toArray: function toArray(array) {
117         return [].slice.call(array);
118     },
119 
120     /**
121      * Small adapter for looping over objects and arrays
122      * Warning: it's not meant to be used with nodeList
123      * To use with nodeList, convert to array first
124      * @param {Array/Object} iterated the array or object to loop through
125      * @param {Function} callback the function to execute for each iteration
126      * @param {Object} scope the scope in which to execute the callback
127      * @returns {Boolean} true if executed
128      */
129     loop: function loop(iterated, callback, scope) {
130         var i,
131             length;
132 
133         if (iterated instanceof Object && callback instanceof Function) {
134             if (iterated instanceof Array) {
135                 for (i=0; i<iterated.length; i++) {
136                     callback.call(scope, iterated[i], i, iterated);
137                 }
138             } else {
139                 for (i in iterated) {
140                     if (iterated.hasOwnProperty(i)) {
141                         callback.call(scope, iterated[i], i, iterated);
142                     }
143                 }
144             }
145             return true;
146         } else {
147             return false;
148         }
149     },
150 
151     /**
152      * Make a diff between two objects
153      * @param {Array/Object} before is the object as it was before
154      * @param {Array/Object} after is what it is now
155      * @example:
156      *  With objects:
157      *
158      *  before = {a:1, b:2, c:3, d:4, f:6}
159      *  after = {a:1, b:20, d: 4, e: 5}
160      *  will return :
161      *  {
162      *      unchanged: ["a", "d"],
163      *      updated: ["b"],
164      *      deleted: ["f"],
165      *      added: ["e"]
166      *  }
167      *
168      * It also works with Arrays:
169      *
170      *  before = [10, 20, 30]
171      *  after = [15, 20]
172      *  will return :
173      *  {
174      *      unchanged: [1],
175      *      updated: [0],
176      *      deleted: [2],
177      *      added: []
178      *  }
179      *
180      * @returns object
181      */
182     objectsDiffs : function objectsDiffs(before, after) {
183         if (before instanceof Object && after instanceof Object) {
184             var unchanged = [],
185                 updated = [],
186                 deleted = [],
187                 added = [];
188 
189              // Look through the after object
190              this.loop(after, function (value, idx) {
191 
192                  // To get the added
193                  if (typeof before[idx] == "undefined") {
194                      added.push(idx);
195 
196                  // The updated
197                  } else if (value !== before[idx]) {
198                      updated.push(idx);
199 
200                  // And the unchanged
201                  } else if (value === before[idx]) {
202                      unchanged.push(idx);
203                  }
204 
205              });
206 
207              // Loop through the before object
208              this.loop(before, function (value, idx) {
209 
210                 // To get the deleted
211                 if (typeof after[idx] == "undefined") {
212                     deleted.push(idx);
213                 }
214              });
215 
216             return {
217                 updated: updated,
218                 unchanged: unchanged,
219                 added: added,
220                 deleted: deleted
221             };
222 
223         } else {
224             return false;
225         }
226     },
227 
228     /**
229      * Transforms Arrays and Objects into valid JSON
230      * @param {Object/Array} object the object to JSONify
231      * @returns the JSONified object or false if failed
232      */
233     jsonify: function jsonify(object) {
234         if (object instanceof Object) {
235             return JSON.parse(JSON.stringify(object));
236         } else {
237             return false;
238         }
239     },
240 
241     /**
242      * Clone an Array or an Object
243      * @param {Array/Object} object the object to clone
244      * @returns {Array/Object} the cloned object
245      */
246     clone: function clone(object) {
247         if (object instanceof Array) {
248             return object.slice(0);
249         } else if (typeof object == "object" && object !== null && !(object instanceof RegExp)) {
250             return this.mixin(object, {});
251         } else {
252             return false;
253         }
254     },
255 
256 
257     /**
258      *
259      *
260      *
261      *
262      * Refactoring needed for the following
263      *
264      *
265      *
266      *
267      *
268      */
269 
270     /**
271      * Get the property of an object nested in one or more objects
272      * given an object such as a.b.c.d = 5, getNestedProperty(a, "b.c.d") will return 5.
273      * @param {Object} object the object to get the property from
274      * @param {String} property the path to the property as a string
275      * @returns the object or the the property value if found
276      */
277     getNestedProperty: function getNestedProperty(object, property) {
278         if (object && object instanceof Object) {
279             if (typeof property == "string" && property !== "") {
280                 var split = property.split(".");
281                 return split.reduce(function (obj, prop) {
282                     return obj && obj[prop];
283                 }, object);
284             } else if (typeof property == "number") {
285                 return object[property];
286             } else {
287                 return object;
288             }
289         } else {
290             return object;
291         }
292     },
293 
294     /**
295      * Set the property of an object nested in one or more objects
296      * If the property doesn't exist, it gets created.
297      * @param {Object} object
298      * @param {String} property
299      * @param value the value to set
300      * @returns object if no assignment was made or the value if the assignment was made
301      */
302     setNestedProperty: function setNestedProperty(object, property, value) {
303         if (object && object instanceof Object) {
304             if (typeof property == "string" && property !== "") {
305                 var split = property.split(".");
306                 return split.reduce(function (obj, prop, idx) {
307                     obj[prop] = obj[prop] || {};
308                     if (split.length == (idx + 1)) {
309                         obj[prop] = value;
310                     }
311                     return obj[prop];
312                 }, object);
313             } else if (typeof property == "number") {
314                 object[property] = value;
315                 return object[property];
316             } else {
317                 return object;
318             }
319         } else {
320             return object;
321         }
322     },
323 
324     /**
325      * Get the closest number in an array given a base number
326      * Example: closest(30, [20, 0, 50, 29]) will return 3 as 29 is the closest item
327      * @param {Number} item the base number
328      * @param {Array} array the array of numbers to search into
329      * @returns {Number} the index of the closest item in the array
330      */
331     closest: function closest(item, array) {
332         return _getClosest(item, array, function (comparedItem, item) {
333             return Math.abs(comparedItem - item);
334         });
335     },
336 
337     /**
338      * Get the closest greater number in an array given a base number
339      * Example: closest(30, [20, 0, 50, 29]) will return 2 as 50 is the closest greater item
340      * @param {Number} item the base number
341      * @param {Array} array the array of numbers to search into
342      * @returns {Number} the index of the closest item in the array
343      */
344     closestGreater: function closestGreater(item, array) {
345         return _getClosest(item, array, function (comparedItem, item) {
346             return comparedItem - item;
347         });
348     },
349 
350     /**
351      * Get the closest lower number in an array given a base number
352      * Example: closest(30, [20, 0, 50, 29]) will return 0 as 20 is the closest lower item
353      * @param {Number} item the base number
354      * @param {Array} array the array of numbers to search into
355      * @returns {Number} the index of the closest item in the array
356      */
357     closestLower: function closestLower(item, array) {
358         return _getClosest(item, array, function (comparedItem, item) {
359             return item - comparedItem;
360         });
361     }
362 };