Skip to content

Instantly share code, notes, and snippets.

@dcporter
Last active December 20, 2015 17:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dcporter/6169739 to your computer and use it in GitHub Desktop.
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.
// ==========================================================================
// 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