master
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