master
Raw Download raw file
  1
  2  'use strict';
  3
  4  /***
  5   * @package Range
  6   * @dependency core
  7   * @description Ranges allow creating spans of numbers, strings, or dates. They can enumerate over specific points within that range, and be manipulated and compared.
  8   *
  9   ***/
 10
 11  function Range(start, end) {
 12    this.start = cloneRangeMember(start);
 13    this.end   = cloneRangeMember(end);
 14  };
 15
 16  function getRangeMemberNumericValue(m) {
 17    return isString(m) ? m.charCodeAt(0) : m;
 18  }
 19
 20  function getRangeMemberPrimitiveValue(m) {
 21    if(m == null) return m;
 22    return isDate(m) ? m.getTime() : m.valueOf();
 23  }
 24
 25  function cloneRangeMember(m) {
 26    if(isDate(m)) {
 27      return new date(m.getTime());
 28    } else {
 29      return getRangeMemberPrimitiveValue(m);
 30    }
 31  }
 32
 33  function isValidRangeMember(m) {
 34    var val = getRangeMemberPrimitiveValue(m);
 35    return !!val || val === 0;
 36  }
 37
 38  function getDuration(amt) {
 39    var match, val, unit;
 40    if(isNumber(amt)) {
 41      return amt;
 42    }
 43    match = amt.toLowerCase().match(/^(\d+)?\s?(\w+?)s?$/i);
 44    val = parseInt(match[1]) || 1;
 45    unit = match[2].slice(0,1).toUpperCase() + match[2].slice(1);
 46    if(unit.match(/hour|minute|second/i)) {
 47      unit += 's';
 48    } else if(unit === 'Year') {
 49      unit = 'FullYear';
 50    } else if(unit === 'Day') {
 51      unit = 'Date';
 52    }
 53    return [val, unit];
 54  }
 55
 56  function incrementDate(current, amount) {
 57    var num, unit, val, d;
 58    if(isNumber(amount)) {
 59      return new date(current.getTime() + amount);
 60    }
 61    num  = amount[0];
 62    unit = amount[1];
 63    val  = callDateGet(current, unit);
 64    d    = new date(current.getTime());
 65    callDateSet(d, unit, val + num);
 66    return d;
 67  }
 68
 69  function incrementString(current, amount) {
 70    return string.fromCharCode(current.charCodeAt(0) + amount);
 71  }
 72
 73  function incrementNumber(current, amount) {
 74    return current + amount;
 75  }
 76
 77  /***
 78   * @method toString()
 79   * @returns String
 80   * @short Returns a string representation of the range.
 81   * @example
 82   *
 83   *   Number.range(1, 5).toString()                               -> 1..5
 84   *   Date.range(new Date(2003, 0), new Date(2005, 0)).toString() -> January 1, 2003..January 1, 2005
 85   *
 86   ***/
 87
 88  // Note: 'toString' doesn't appear in a for..in loop in IE even though
 89  // hasOwnProperty reports true, so extend() can't be used here.
 90  // Also tried simply setting the prototype = {} up front for all
 91  // methods but GCC very oddly started dropping properties in the
 92  // object randomly (maybe because of the global scope?) hence
 93  // the need for the split logic here.
 94  Range.prototype.toString = function() {
 95    return this.isValid() ? this.start + ".." + this.end : 'Invalid Range';
 96  };
 97
 98  extend(Range, true, true, {
 99
100    /***
101     * @method isValid()
102     * @returns Boolean
103     * @short Returns true if the range is valid, false otherwise.
104     * @example
105     *
106     *   Date.range(new Date(2003, 0), new Date(2005, 0)).isValid() -> true
107     *   Number.range(NaN, NaN).isValid()                           -> false
108     *
109     ***/
110    'isValid': function() {
111      return isValidRangeMember(this.start) && isValidRangeMember(this.end) && typeof this.start === typeof this.end;
112    },
113
114    /***
115     * @method span()
116     * @returns Number
117     * @short Returns the span of the range. If the range is a date range, the value is in milliseconds.
118     * @extra The span includes both the start and the end.
119     * @example
120     *
121     *   Number.range(5, 10).span()                              -> 6
122     *   Date.range(new Date(2003, 0), new Date(2005, 0)).span() -> 94694400000
123     *
124     ***/
125    'span': function() {
126      return this.isValid() ? abs(
127        getRangeMemberNumericValue(this.end) - getRangeMemberNumericValue(this.start)
128      ) + 1 : NaN;
129    },
130
131    /***
132     * @method contains(<obj>)
133     * @returns Boolean
134     * @short Returns true if <obj> is contained inside the range. <obj> may be a value or another range.
135     * @example
136     *
137     *   Number.range(5, 10).contains(7)                                              -> true
138     *   Date.range(new Date(2003, 0), new Date(2005, 0)).contains(new Date(2004, 0)) -> true
139     *
140     ***/
141    'contains': function(obj) {
142      var self = this, arr;
143      if(obj == null) return false;
144      if(obj.start && obj.end) {
145        return obj.start >= this.start && obj.start <= this.end &&
146               obj.end   >= this.start && obj.end   <= this.end;
147      } else {
148        return obj >= this.start && obj <= this.end;
149      }
150    },
151
152    /***
153     * @method every(<amount>, [fn])
154     * @returns Array
155     * @short Iterates through the range for every <amount>, calling [fn] if it is passed. Returns an array of each increment visited.
156     * @extra In the case of date ranges, <amount> can also be a string, in which case it will increment a number of  units. Note that %(2).months()% first resolves to a number, which will be interpreted as milliseconds and is an approximation, so stepping through the actual months by passing %"2 months"% is usually preferable.
157     * @example
158     *
159     *   Number.range(2, 8).every(2)                                       -> [2,4,6,8]
160     *   Date.range(new Date(2003, 1), new Date(2003,3)).every("2 months") -> [...]
161     *
162     ***/
163    'every': function(amount, fn) {
164      var increment,
165          start   = this.start,
166          end     = this.end,
167          inverse = end < start,
168          current = start,
169          index   = 0,
170          result  = [];
171
172      if(isFunction(amount)) {
173        fn = amount;
174        amount = null;
175      }
176      amount = amount || 1;
177      if(isNumber(start)) {
178        increment = incrementNumber;
179      } else if(isString(start)) {
180        increment = incrementString;
181      } else if(isDate(start)) {
182        amount    = getDuration(amount);
183        increment = incrementDate;
184      }
185      // Avoiding infinite loops
186      if(inverse && amount > 0) {
187        amount *= -1;
188      }
189      while(inverse ? current >= end : current <= end) {
190        result.push(current);
191        if(fn) {
192          fn(current, index);
193        }
194        current = increment(current, amount);
195        index++;
196      }
197      return result;
198    },
199
200    /***
201     * @method union(<range>)
202     * @returns Range
203     * @short Returns a new range with the earliest starting point as its start, and the latest ending point as its end. If the two ranges do not intersect this will effectively remove the "gap" between them.
204     * @example
205     *
206     *   Number.range(1, 3).union(Number.range(2, 5)) -> 1..5
207     *   Date.range(new Date(2003, 1), new Date(2005, 1)).union(Date.range(new Date(2004, 1), new Date(2006, 1))) -> Jan 1, 2003..Jan 1, 2006
208     *
209     ***/
210    'union': function(range) {
211      return new Range(
212        this.start < range.start ? this.start : range.start,
213        this.end   > range.end   ? this.end   : range.end
214      );
215    },
216
217    /***
218     * @method intersect(<range>)
219     * @returns Range
220     * @short Returns a new range with the latest starting point as its start, and the earliest ending point as its end. If the two ranges do not intersect this will effectively produce an invalid range.
221     * @example
222     *
223     *   Number.range(1, 5).intersect(Number.range(4, 8)) -> 4..5
224     *   Date.range(new Date(2003, 1), new Date(2005, 1)).intersect(Date.range(new Date(2004, 1), new Date(2006, 1))) -> Jan 1, 2004..Jan 1, 2005
225     *
226     ***/
227    'intersect': function(range) {
228      if(range.start > this.end || range.end < this.start) {
229        return new Range(NaN, NaN);
230      }
231      return new Range(
232        this.start > range.start ? this.start : range.start,
233        this.end   < range.end   ? this.end   : range.end
234      );
235    },
236
237    /***
238     * @method clone()
239     * @returns Range
240     * @short Clones the range.
241     * @extra Members of the range will also be cloned.
242     * @example
243     *
244     *   Number.range(1, 5).clone() -> Returns a copy of the range.
245     *
246     ***/
247    'clone': function(range) {
248      return new Range(this.start, this.end);
249    },
250
251    /***
252     * @method clamp(<obj>)
253     * @returns Mixed
254     * @short Clamps <obj> to be within the range if it falls outside.
255     * @example
256     *
257     *   Number.range(1, 5).clamp(8) -> 5
258     *   Date.range(new Date(2010, 0), new Date(2012, 0)).clamp(new Date(2013, 0)) -> 2012-01
259     *
260     ***/
261    'clamp': function(obj) {
262      var clamped,
263          start = this.start,
264          end = this.end,
265          min = end < start ? end : start,
266          max = start > end ? start : end;
267      if(obj < min) {
268        clamped = min;
269      } else if(obj > max) {
270        clamped = max;
271      } else {
272        clamped = obj;
273      }
274      return cloneRangeMember(clamped);
275    }
276
277  });
278
279
280  /***
281   * Number module
282   ***
283   * @method Number.range([start], [end])
284   * @returns Range
285   * @short Creates a new range between [start] and [end]. See @ranges for more.
286   * @example
287   *
288   *   Number.range(5, 10)
289   *
290   ***
291   * String module
292   ***
293   * @method String.range([start], [end])
294   * @returns Range
295   * @short Creates a new range between [start] and [end]. See @ranges for more.
296   * @example
297   *
298   *   String.range('a', 'z')
299   *
300   ***
301   * Date module
302   ***
303   * @method Date.range([start], [end])
304   * @returns Range
305   * @short Creates a new range between [start] and [end].
306   * @extra If either [start] or [end] are null, they will default to the current date. See @ranges for more.
307   * @example
308   *
309   *   Date.range('today', 'tomorrow')
310   *
311   ***/
312  [number, string, date].forEach(function(klass) {
313     extend(klass, false, true, {
314
315      'range': function(start, end) {
316        if(klass.create) {
317          start = klass.create(start);
318          end   = klass.create(end);
319        }
320        return new Range(start, end);
321      }
322
323    });
324
325  });
326
327  /***
328   * Number module
329   *
330   ***/
331
332  extend(number, true, true, {
333
334    /***
335     * @method upto(<num>, [fn], [step] = 1)
336     * @returns Array
337     * @short Returns an array containing numbers from the number up to <num>.
338     * @extra Optionally calls [fn] callback for each number in that array. [step] allows multiples greater than 1.
339     * @example
340     *
341     *   (2).upto(6) -> [2, 3, 4, 5, 6]
342     *   (2).upto(6, function(n) {
343     *     // This function is called 5 times receiving n as the value.
344     *   });
345     *   (2).upto(8, null, 2) -> [2, 4, 6, 8]
346     *
347     ***/
348    'upto': function(num, fn, step) {
349      return number.range(this, num).every(step, fn);
350    },
351
352     /***
353     * @method clamp([start] = Infinity, [end] = Infinity)
354     * @returns Number
355     * @short Constrains the number so that it is between [start] and [end].
356     * @extra This will build a range object that has an equivalent %clamp% method.
357     * @example
358     *
359     *   (3).clamp(50, 100)  -> 50
360     *   (85).clamp(50, 100) -> 85
361     *
362     ***/
363    'clamp': function(start, end) {
364      return new Range(start, end).clamp(this);
365    },
366
367     /***
368     * @method cap([max] = Infinity)
369     * @returns Number
370     * @short Constrains the number so that it is no greater than [max].
371     * @extra This will build a range object that has an equivalent %cap% method.
372     * @example
373     *
374     *   (100).cap(80) -> 80
375     *
376     ***/
377    'cap': function(max) {
378      return this.clamp(Undefined, max);
379    }
380
381  });
382
383  extend(number, true, true, {
384
385    /***
386     * @method downto(<num>, [fn], [step] = 1)
387     * @returns Array
388     * @short Returns an array containing numbers from the number down to <num>.
389     * @extra Optionally calls [fn] callback for each number in that array. [step] allows multiples greater than 1.
390     * @example
391     *
392     *   (8).downto(3) -> [8, 7, 6, 5, 4, 3]
393     *   (8).downto(3, function(n) {
394     *     // This function is called 6 times receiving n as the value.
395     *   });
396     *   (8).downto(2, null, 2) -> [8, 6, 4, 2]
397     *
398     ***/
399    'downto': number.prototype.upto
400
401  });
402
403
404  /***
405   * Array module
406   *
407   ***/
408
409  extend(array, false, function(a) { return a instanceof Range; }, {
410
411    'create': function(range) {
412      return range.every();
413    }
414
415  });
416