Last active
December 20, 2015 17:38
-
-
Save dcporter/6169739 to your computer and use it in GitHub Desktop.
A proposed Timespan class for SproutCore to allow for easier comparison of time spans.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========================================================================== | |
// Project: SC.Timespan | |
// Copyright: @2013 My Company, Inc. | |
// ========================================================================== | |
/*globals SC */ | |
/** | |
Standard error thrown when trying to create a timespan with dates in | |
different timezones. | |
@static | |
@constant | |
@type Error | |
*/ | |
SC.TIMESPAN_TIMEZONE_ERROR = "Can't create a timespan with two DateTimes that don't have the same timezone."; | |
/** | |
Standard error thrown when trying to create a timespan with out-of-order | |
startDate and endDate. | |
@static | |
@constant | |
@type Error | |
*/ | |
SC.TIMESPAN_DATE_ORDER_ERROR = "Can't create a timespan with startDate later than endDate."; | |
/** | |
Standard error thrown when trying to create a timespan with unsupported | |
argument set. | |
@static | |
@constant | |
@type Error | |
*/ | |
SC.TIMESPAN_ARGUMENTS_ERROR = "Attempted to create SC.Timespan with unsupported argument set. See SC.Timespan#create documentation."; | |
/** | |
Standard error thrown when trying to compare timespan to an unsupported | |
class. | |
@static | |
@constant | |
@type Error | |
*/ | |
SC.TIMESPAN_CONTAINS_ERROR = "Can't determine if %@ instance is contained in timespan."; | |
/** @class | |
Represents a timespan. | |
@extends SC.Object | |
@version 0.1 | |
*/ | |
SC.Timespan = SC.Object.extend(SC.Freezable, SC.Copyable, | |
/** @scope SC.Timespan.prototype */ { | |
/** | |
The beginning of the timespan. A null value represents "from the beginning | |
of time". | |
@type SC.DateTime | |
@readonly | |
*/ | |
startDate: null, | |
/** | |
The end of the timespan. A null value represents "until the end of time". | |
@type SC.DateTime | |
@readonly | |
*/ | |
endDate: null, | |
/** | |
Returns a copy of the receiver with the timezone set to the passed | |
timezone. See SC.DateTime#toTimezone. | |
If you don't pass an argument, the target timezone is set to 0, i.e. UTC. | |
Note that this method does not change the underlying positions in time, | |
only the time zone in which it is displayed. In other words, the underlying | |
numbers of milliseconds since Jan 1, 1970 does not change. | |
@return {SC.Timespan} | |
*/ | |
toTimezone: function(timezone) { | |
if (timezone === undefined) timezone = 0; | |
var startDate = this.get('startDate'), | |
endDate = this.get('endDate'); | |
return SC.Timespan.create({ | |
startDate: startDate ? startDate.toTimezone(timezone) : null, | |
endDate: endDate ? endDate.toTimezone(timezone) : null | |
}); | |
}, | |
/** | |
Returns whether the receiver contains the passed SC.DateTime or entirely contains | |
the passed SC.Timespan. | |
@param {SC.DateTime|SC.Timespan} The timespan to compare to the receiver. | |
@returns {Boolean} | |
@throws {} | |
*/ | |
contains: function(datetimeOrTimespan) { | |
// Common case: Don't be a pain and throw an error when argument is missing. | |
if (SC.none(datetimeOrTimespan)) { | |
return false; | |
} | |
// Handle DateTime. | |
if (SC.instanceOf(datetimeOrTimespan, SC.DateTime)) { | |
var passedD = datetimeOrTimespan.get('milliseconds'), | |
thisSD = this.getPath('startDate.milliseconds') || -Infinity, | |
thisED = this.getPath('endDate.milliseconds') || Infinity; | |
// The receiver contains the passed date if it's greater than the start date | |
// and less than the end date. | |
if (thisSD <= passedD && passedD <= thisED) return YES; | |
// Otherwise, it does not. | |
return NO; | |
} | |
// Handle Timespan. | |
else if (SC.instanceOf(datetimeOrTimespan, SC.Timespan)) { | |
var timespan = datetimeOrTimespan | |
// Fast path: Longer timespans can't be contained within smaller ones. | |
if (timespan._durationInMs > this._durationInMs) return NO; | |
var thisSD = this.getPath('startDate.milliseconds') || -Infinity, | |
thisED = this.getPath('endDate.milliseconds') || Infinity, | |
passedSD = timespan.getPath('startDate.milliseconds') || -Infinity, | |
passedED = timespan.getPath('endDate.milliseconds') || Infinity; | |
// The receiver contains the passed timespan if its start date is less than or equal | |
// to the passed start date AND its end date is greater than or equal to the passed | |
// end date. | |
if (thisSD <= passedSD && thisED >= passedED) return YES; | |
// Otherwise, it does not. | |
return NO; | |
} | |
// Throw error if something else comes through. | |
else { | |
throw new Error(SC.TIMESPAN_CONTAINS_ERROR); | |
} | |
}, | |
/** | |
Returns whether the receiver overlaps the passed timespan. | |
Note that if one span's start date is the same as the other span's | |
end date, the spans are considered to overlap. | |
@param {SC.Timespan} The timespan to compare to the receiver. | |
@returns {Boolean} | |
*/ | |
overlaps: function(timespan) { | |
var thisSD = this.getPath('startDate.milliseconds') || -Infinity, | |
thisED = this.getPath('endDate.milliseconds') || Infinity, | |
passedSD = timespan.getPath('startDate.milliseconds') || -Infinity, | |
passedED = timespan.getPath('endDate.milliseconds') || Infinity; | |
// The spans overlap if any bounding date is within the other span's bounds. | |
// Test is thisSD within passed timespan? | |
if (passedSD <= thisSD && thisSD <= passedED) return YES; | |
// Test is thisED within passed timespan? | |
if (passedSD <= thisED && thisED <= passedED) return YES; | |
// Test is passedSD within this timespan? | |
if (thisSD <= passedSD && passedSD <= thisED) return YES; | |
// Test is passedED within this timespan? | |
if (thisSD <= passedED && passedED <= thisED) return YES; | |
// Otherwise not overlapping. | |
return NO; | |
}, | |
/** | |
A SC.Timespan instance is frozen for better performance. | |
@type Boolean | |
*/ | |
isFrozen: YES, | |
/** | |
Returns a copy of the receiver. Because of the way SC.Timespan is designed, | |
it just returns the receiver. | |
@returns {SC.Timespan} | |
*/ | |
copy: function() { | |
return this; | |
}, | |
/** | |
@private | |
Reserved property for when SC.Duration exists. | |
@type null | |
*/ | |
duration: null, | |
/** | |
@private | |
The duration in milliseconds of the timespan. If either or both of the timespan's | |
bounding dates are null, duration is Infinity. | |
@type Number | Infinity | |
*/ | |
_durationInMs: Infinity | |
}); | |
SC.Timespan.mixin({ | |
/** | |
Returns a new SC.Timespan object, specified by a startDate and an endDate. | |
Both values are optional: a missing startDate indicates "since the beginning | |
of time"; a missing endDate indicates "until the end of time". | |
You may specify these in a variety of ways: | |
- no arguments, to create a SC.Timespan object encompasing the whole history | |
of the universe, | |
- two SC.DateTimes in any order (neither of which may be null), | |
- an options hash that may contain a startDate and/or an endDate, | |
- TODO: an options hash that contains a SC.Duration object and either a | |
startDate or an endDate. | |
Creating SC.Timespan objects with mixins (multiple option hashes) is not | |
supported at this time. | |
Note that if you attempt to create a SC.Timespan instance that has already | |
been created, then, for performance reasons, a cached value may be | |
returned. | |
You may not create a timespan with two dates in different time zones. See | |
discussion on SC.DateTime#compareDate | |
@param options one of the three kind of parameters described above | |
@returns {SC.Timespan} the SC.Timespan instance that corresponds to the | |
passed parameters, possibly fetched from cache | |
*/ | |
create: function() { | |
// Normalize our arguments. | |
var startDate = null, | |
endDate = null; | |
// If we receive two (actual) dates, get them in the right order. | |
if (arguments.length === 2 && SC.instanceOf(arguments[0], SC.DateTime) && SC.instanceOf(arguments[1], SC.DateTime)) { | |
if (SC.DateTime.compare(arguments[0], arguments[1]) <= 0) { | |
startDate = arguments[0]; | |
endDate = arguments[1]; | |
} else { | |
startDate = arguments[1]; | |
endDate = arguments[0]; | |
} | |
} | |
// If we receive one object, extract the start date and end date (if provided). | |
else if (arguments.length === 1 && (SC.typeOf(arguments[0]) === SC.T_HASH || SC.typeOf(arguments[0]) === SC.T_OBJECT)) { | |
var arg = arguments[0]; | |
if (arg.get) { | |
startDate = arg.get('startDate'); | |
endDate = arg.get('endDate'); | |
} else { | |
startDate = arg.startDate; | |
endDate = arg.endDate; | |
} | |
// If we have both dates, verify date order. Throw error if they're wrong, because, developer intention?? | |
if (startDate && endDate && SC.DateTime.compare(startDate, endDate) > 0) { | |
throw new Error(SC.TIMESPAN_DATE_ORDER_ERROR); | |
} | |
} | |
// If we receive no arguments, we coo'. | |
else if (arguments.length === 0) { | |
// we coo' | |
} | |
// Otherwise, we've received an unsupported argument set. | |
else { | |
throw new Error(SC.TIMESPAN_ARGUMENTS_ERROR); | |
} | |
// If we have both dates, verify timezone match. | |
var startDateTimezone = startDate ? startDate.get('timezone') : null, | |
endDateTimezone = endDate ? endDate.get('timezone') : null; | |
if (startDate && endDate && startDateTimezone !== endDateTimezone) { | |
throw new Error(SC.TIMESPAN_TIMEZONE_ERROR); | |
} | |
// Quick implementation of a FIFO set for the cache. The cache stores both the hash and the map from | |
// indexes to hash keys. | |
var key = '%@ - %@'.fmt(startDate ? startDate.toString() : 'null', endDate ? endDate.toString() : null), | |
cache = this._ts_cache, | |
Timespan = this, | |
ret = cache[key]; | |
if (!ret) { | |
// Create and cache the thing. | |
ret = new Timespan([{ | |
startDate: startDate, | |
endDate: endDate, | |
_durationInMs: startDate && endDate ? endDate.get('milliseconds') - startDate.get('milliseconds') : Infinity | |
}]); | |
cache[key] = ret; | |
// Update the index map. | |
var previousKey, idx = this._ts_cache_index; | |
idx = this._ts_cache_index = (idx + 1) % this._TS_CACHE_MAX_LENGTH; | |
if (previousKey !== undefined && cache[previousKey]) delete cache[previousKey]; | |
cache[idx] = key; | |
} | |
return ret; | |
}, | |
/** | |
@private | |
A cache of SC.Timespan instances. If you attempt to create a SC.Timespan | |
instance that has already been created, then it will return the cached | |
value. | |
@type Object | |
*/ | |
_ts_cache: {}, | |
/** | |
@private | |
The index of the latest cached value. Used with _TS_CACHE_MAX_LENGTH to | |
limit the size of the cache. | |
@type Integer | |
*/ | |
_ts_cache_index: -1, | |
/** | |
@private | |
The maximum length of _ts_cache. If this limit is reached, then the cache | |
is overwritten, starting with the oldest element. | |
@type Integer | |
*/ | |
_TS_CACHE_MAX_LENGTH: 1000 | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment