/* <CoffeeScript> utils = require('./utils') timezoneJS = require('../lib/timezone-js.js').timezoneJS class Time </CoffeeScript> */ /** * @class tzTime.Time * * ## Basic usage ## * * {TimelineIterator, Timeline, Time} = require('../') * * Get Time objects from partial ISOStrings. The granularity is automatically inferred from how many segments you provide. * * d1 = new Time('2011-02-28') * console.log(d1.toString()) * # 2011-02-28 * * Spell it all out with a JavaScript object * * d2 = new Time({granularity: Time.DAY, year: 2011, month: 3, day: 1}) * console.log(d2.toString()) * # 2011-03-01 * * Increment/decrement and compare Times without regard to timezone * * console.log(d1.greaterThanOrEqual(d2)) * # false * * d1.increment() * console.log(d1.equal(d2)) * # true * * Do math on them. * * d3 = d1.add(5) * console.log(d3.toString()) * # 2011-03-06 * * Get the day of the week. * * console.log(d3.dowString()) * # Sunday * * Subtraction is just addition with negative numbers. * * d3.addInPlace(-6) * console.log(d3.toString()) * # 2011-02-28 * * If you start on the last day of a month, adding a month takes you to the last day of the next month, * even if the number of days are different. * * d3.addInPlace(1, 'month') * console.log(d3.toString()) * # 2011-03-31 * * Deals well with year-granularity math and leap year complexity. * * d4 = new Time('2004-02-29') # leap day * d4.addInPlace(1, 'year') # adding a year takes us to a non-leap year * console.log(d4.toString()) * # 2005-02-28 * * Week granularity correctly wraps and deals with 53-week years. * * w1 = new Time('2004W53-6') * console.log(w1.inGranularity(Time.DAY).toString()) * # 2005-01-01 * * Convert between any of the standard granularities. Also converts custom granularities (not shown) to * standard granularities if you provide a `rataDieNumber()` function with your custom granularities. * * d5 = new Time('2005-01-01') # goes the other direction also * console.log(d5.inGranularity('week_day').toString()) * # 2004W53-6 * * q1 = new Time('2011Q3') * console.log(q1.inGranularity(Time.MILLISECOND).toString()) * # 2011-07-01T00:00:00.000 * * ## Timezones ## * * Time does timezone sensitive conversions. * * console.log(new Time('2011-01-01').getJSDate('America/Denver').toISOString()) * # 2011-01-01T07:00:00.000Z * * @constructor * @param {Object/Number/Date/String} value * @param {String} [granularity] * @param {String} [tz] * * The constructor for Time supports the passing in of a String, a rata die number (RDN), or a config Object * * ## String ## * * There are two kinds of strings that can be passed into the constructor: * * 1. Human strings relative to now (e.g. "this day", "previous month", "next quarter", "this millisecond in Pacific/Fiji", etc.) * 2. ISO-8601 or custom masked (e.g. "I03D10" - 10th day of 3rd iteration) * * ## Human strings relative to now ## * * The string must be in the form `(this, previous, next) |granularity| [in |timezone|]` * * Examples * * * `this day` today * * `next month` next month * * `this day in Pacific/Fiji` the day that it currently is in Fiji * * `previous hour in America/New_York` the hour before the current hour in New York * * `next quarter` next quarter * * `previous week` last week * * ## ISO-8601 or custom masked ## * * When you pass in an ISO-8601 or custom mask string, Time uses the masks that are defined for each granularity to figure out the granularity... * unless you explicitly provide a granularity. This parser works on all valid ISO-8601 forms except orginal dates (e.g. `"2012-288"`) * It even supports week number form (`"2009W52-7"`) and we've added a form for Quarter granularity (e.g. `"2009Q4"`). * The canonical form (`"2009-01-01T12:34:56.789"`) will work as will any shortened subset of it (`"2009-01-01"`, * `"2009-01-01T12:34"`, etc.). Plus it will even parse strings in whatever custom granularity you provide based * upon the mask that you provide for that granularity. * * If the granularity is specified but not all of the segments are provided, Time will fill in the missing value * with the `lowest` value from _granularitySpecs. * * The ISO forms that omit the delimiters or use spaces as the delimeters are not supported. Also unsupported are strings * with a time shift indicator on the end (`...+05:00`). However, if you pass in a string with a "Z" at the end, Time * will assume that you want to convert from GMT to local (abstract) time and you must provide a timezone. * * There are two special Strings that are recognized: `BEFORE_FIRST` and `PAST_LAST`. You must provide a granularity if you * are instantiating a Time with these values. They are primarily used for custom granularities where your users * may mistakenly request charts for iterations and releases that have not yet been defined. They are particularly useful when * you want to iterate to the last defined iteration/release. * * ## Rata Die Number ## * * The **rata die number (RDN)** for a date is the number of days since 0001-01-01. You will probably never work * directly with this number but it's what Time uses to convert between granularities. When you are instantiating * a Time from an RDN, you must provide a granularity. Using RDN will work even for the granularities finer than day. * Time will populate the finer grained segments (hour, minute, etc.) with the approriate `lowest` value. * * ## Date ## * * You can also pass in a JavaScript Date() Object. The passing in of a tz with this option doesn't make sense. You'll end * up with the same Time value no matter what because the JS Date() already sorta has a timezone. I'm not sure if this * option is even really useful. In most cases, you are probably better off using Time.getISOStringFromJSDate() * * ## Object ## * * You can also explicitly spell out the segments in a specification Object in the form of * `{granularity: Time.DAY, year: 2009, month: 1, day: 1}`. If the granularity is specified but not all of the segments are * provided, Time will fill in the missing value with the appropriate `lowest` value from _granularitySpecs. * * ## granularity ## * * If you provide a granularity it will take precedence over whatever fields you've provided in your config or whatever segments * you have provided in your string. Time will leave off extra values and fill in missing ones with the appropriate `lowest` * value. * * ## tz ## * * Most of the time, Time assumes that any dates you pass in are timezone less. You'll specify Christmas as 12-25, then you'll * shift the boundaries of Christmas for a specific timezone for boundary comparison. * * However, if you provide a tz parameter to this constructor, Time will assume you are passing in a true GMT date/time and shift into * the provided timezone. So... * * d = new Time('2011-01-01T02:00:00:00.000Z', Time.DAY, 'America/New_York') * console.log(d.toString()) * # 2010-12-31 * * Rule of thumb on when you want to use timezones: * * 1. If you have true GMT date/times and you want to create a Time, provide the timezone to this constructor. * 2. If you have abstract days like Christmas or June 10th and you want to delay the timezone consideration, don't provide a timezone to this constructor. * 3. In either case, if the dates you want to compare to are in GMT, but you've got Times or Timelines, you'll have to provide a timezone on * the way back out of Time/Timeline */ /* <CoffeeScript> the way back out of Time/Timeline @beforePastFlag = '' switch utils.type(value) when 'string' s = value if (s.slice(-3, -2) == ':' and s.slice(-6, -5) in '+-') or s.slice(-1) == 'Z' if tz? # Remove the timezone stuff from the end if s.slice(-3, -2) == ':' and s.slice(-6, -5) in '+-' # s = s.slice(0, -6) throw new Error("tzTime.Time does not know how to deal with time shifted ISOStrings like what you sent: #{s}") if s.slice(-1) == 'Z' s = s.slice(0, -1) newCT = new Time(s, 'millisecond') jsDate = newCT.getJSDateFromGMTInTZ(tz) else throw new Error("Must provide a tz parameter when instantiating a Time object with ISOString that contains timeshift/timezone specification. You provided: #{s}.") else @_setFromString(s, granularity) tz = undefined when 'number' rdn = value if tz? newCT = new Time(rdn, 'millisecond') jsDate = newCT.getJSDateFromGMTInTZ(tz) else @_setFromRDN(rdn, granularity) when 'date' jsDate = value if tz? newCT = new Time(jsDate, 'millisecond') jsDate = newCT.getJSDateFromGMTInTZ(tz) unless tz? tz = 'GMT' when 'object' config = {} config.granularity = value.granularity config.beforePastFlag = value.beforePastFlag for segment in Time._granularitySpecs[value.granularity].segments config[segment] = value[segment] if tz? config.granularity = 'millisecond' newCT = new Time(config) jsDate = newCT.getJSDateFromGMTInTZ(tz) else @_setFromConfig(config) if tz? if @beforePastFlag in ['BEFORE_FIRST', 'PAST_LAST'] throw new Error("Cannot do timezone manipulation on #{@beforePastFlag}") if granularity? @granularity = granularity unless @granularity? @granularity = 'millisecond' newConfig = year: jsDate.getUTCFullYear() month: jsDate.getUTCMonth() + 1 day: jsDate.getUTCDate() hour: jsDate.getUTCHours() minute: jsDate.getUTCMinutes() second: jsDate.getUTCSeconds() millisecond: jsDate.getUTCMilliseconds() granularity: 'millisecond' newCT = new Time(newConfig).inGranularity(@granularity) @_setFromConfig(newCT) @_inBoundsCheck() @_overUnderFlow() ### `_granularitySpecs` is a static object that is used to tell Time what to do with particular granularties. You can think of each entry in it as a sort of sub-class of Time. In that sense Time is really a factory generating Time objects of type granularity. When custom timebox granularities are added to Time by `Time.addGranularity()`, it adds to this `_granularitySpecs` object. ### @_granularitySpecs = {} @_granularitySpecs['millisecond'] = { segments: ['year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'], mask: '####-##-##T##:##:##.###', lowest: 0, rolloverValue: () -> return 1000, } @_granularitySpecs['second'] = { segments: ['year', 'month', 'day', 'hour', 'minute', 'second'], mask: '####-##-##T##:##:##', lowest: 0, rolloverValue: () -> return 60 } @_granularitySpecs['minute'] = { segments: ['year', 'month', 'day', 'hour', 'minute'], mask: '####-##-##T##:##', lowest: 0, rolloverValue: () -> return 60 } @_granularitySpecs['hour'] = { segments: ['year', 'month', 'day', 'hour'], mask: '####-##-##T##', lowest: 0, rolloverValue: () -> return 24 } @_granularitySpecs['day'] = { segments: ['year', 'month', 'day'], mask: '####-##-##', lowest: 1, rolloverValue: (ct) -> return ct.daysInMonth() + 1 } @_granularitySpecs['month'] = { segments: ['year', 'month'], mask: '####-##', lowest: 1, rolloverValue: () -> return 12 + 1 } @_granularitySpecs['year'] = { segments: ['year'], mask: '####', lowest: 1, rolloverValue: () -> return 9999 + 1 } @_granularitySpecs['week'] = { segments: ['year', 'week'], mask: '####W##', lowest: 1, rolloverValue: (ct) -> if ct.is53WeekYear() return 53 + 1 else return 52 + 1 } @_granularitySpecs['week_day'] = { segments: ['year', 'week', 'week_day'], mask: '####W##-#' lowest: 1, rolloverValue: (ct) -> return 7 + 1 } @_granularitySpecs['quarter'] = { # !TODO: Support quarter_month and quarter_month_day segments: ['year', 'quarter'], mask: '####Q#', lowest: 1, rolloverValue: () -> return 4 + 1 } @_expandMask: (granularitySpec) -> mask = granularitySpec.mask if mask? if mask.indexOf('#') >= 0 i = mask.length - 1 while mask.charAt(i) != '#' i-- segmentEnd = i while mask.charAt(i) == '#' i-- granularitySpec.segmentStart = i + 1 granularitySpec.segmentLength = segmentEnd - i granularitySpec.regex = new RegExp(((if character == '#' then '\\d' else character) for character in mask.split('')).join('')) else # 'PAST_LAST' and other specials will have no mask granularitySpec.regex = new RegExp(mask) # The code below should run when Time is loaded. It mutates the _granularitySpecs object by converting # the mask into segmentStart, segmentLength, and regex. for g, spec of @_granularitySpecs # !TODO: Do consistency checks on _granularitySpecs in the loop below Time._expandMask(spec) Time[g.toUpperCase()] = g # It also preloads the tz files timezoneJS.timezone.zoneFileBasePath = '../files/tz' timezoneJS.timezone.init() _inBoundsCheck: () -> if @beforePastFlag == '' or !@beforePastFlag? unless @granularity throw new Error('@granularity should be set before _inBoundsCheck is ever called.') segments = Time._granularitySpecs[@granularity].segments for segment in segments gs = Time._granularitySpecs[segment] temp = this[segment] lowest = gs.lowest rolloverValue = gs.rolloverValue(this) if temp < lowest or temp >= rolloverValue if temp == lowest - 1 # Supports overflows of just 1 in one segment. If more than that, will fail this[segment]++ this.decrement(segment) else if temp == rolloverValue this[segment]-- this.increment(segment) else throw new Error("Tried to set #{segment} to #{temp}. It must be >= #{lowest} and < #{rolloverValue}") _setFromConfig: (config) -> utils.assert(config.granularity?, 'A granularity property must be part of the supplied config.') @granularity = config.granularity @beforePastFlag = if config.beforePastFlag? then config.beforePastFlag else '' segments = Time._granularitySpecs[@granularity].segments for segment in segments if config[segment]? this[segment] = config[segment] else this[segment] = Time._granularitySpecs[segment].lowest _setFromString: (s, granularity) -> if s in ['PAST_LAST', 'BEFORE_FIRST'] if granularity? @granularity = granularity @beforePastFlag = s return else throw new Error('PAST_LAST/BEFORE_FIRST must have a granularity') # "this month", "next day", "previous quarter", etc. sSplit = s.split(' ') if sSplit[0] in ['this', 'next', 'previous'] if sSplit[2] == 'in' and sSplit[3]? tz = sSplit[3] else tz = undefined zuluCT = new Time(new Date(), sSplit[1], tz) @_setFromConfig(zuluCT) if sSplit[0] == 'next' @increment() else if sSplit[0] == 'previous' @decrement() return for g, spec of Time._granularitySpecs if spec.segmentStart + spec.segmentLength == s.length or spec.mask.indexOf('#') < 0 # for special granularities like 'PAST_LAST' if spec.regex.test(s) granularity = g break if not granularity? throw new Error("Error parsing string '#{s}'. Couldn't identify granularity.") @granularity = granularity segments = Time._granularitySpecs[@granularity].segments stillParsing = true for segment in segments if stillParsing gs = Time._granularitySpecs[segment] l = gs.segmentLength sub = Time._getStringPart(s, segment) if sub.length != l stillParsing = false if stillParsing this[segment] = Number(sub) else this[segment] = Time._granularitySpecs[segment].lowest @_getStringPart = (s, segment) -> spec = Time._granularitySpecs[segment] l = spec.segmentLength st = spec.segmentStart sub = s.substr(st, l) return sub _setFromRDN: (rdn, granularity) -> config = {granularity: granularity} utils.assert(granularity?, "Must provide a granularity when constructing with a Rata Die Number.") switch granularity when 'week', 'week_day' # algorithm from http://en.wikipedia.org/wiki/Talk:ISO_week_date w = Math.floor((rdn - 1) / 7) d = (rdn - 1) % 7 n = Math.floor(w / 20871) w = w % 20871 z = w + (if w >= 10435 then 1 else 0) c = Math.floor(z / 5218) w = z % 5218 x = w * 28 + [15, 23, 3, 11][c] y = Math.floor(x / 1461) w = x % 1461 config['year'] = y + n*400 + c*100 + 1 config['week'] = Math.floor(w / 28) + 1 config['week_day'] = d + 1 @_setFromConfig(config) when 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond', 'quarter' # algorithm from http://en.wikipedia.org/wiki/Julian_day#Gregorian_calendar_from_Julian_day_number # J = rdn + 1721425.5 # With it set like this dates like 2011-05-31 show up as 2011-06-00 J = rdn + 1721425 j = J + 32044 g = Math.floor(j / 146097) dg = j % 146097 c = Math.floor((Math.floor(dg / 36524) + 1) * 3 / 4) dc = dg - c * 36524 b = Math.floor(dc / 1461) db = dc % 1461 a = Math.floor((Math.floor(db / 365) + 1) * 3 / 4) da = db - a * 365 y = g * 400 + c * 100 + b * 4 + a m = Math.floor((da * 5 + 308) / 153) - 2 d = da - Math.floor((m + 4) * 153 / 5) + 122 config['year'] = y - 4800 + Math.floor((m + 2) / 12) config['month'] = (m + 2) % 12 + 1 config['day'] = Math.floor(d) + 1 config['quarter'] = Math.floor((config.month - 1) / 3) + 1 @_setFromConfig(config) else granularitySpec = Time._granularitySpecs[granularity] # Build spec for lowest possible value specForLowest = {granularity: granularity} for segment in granularitySpec.segments specForLowest[segment] = Time._granularitySpecs[segment].lowest beforeCT = new Time(specForLowest) beforeRDN = beforeCT.rataDieNumber() afterCT = beforeCT.add(1) afterRDN = afterCT.rataDieNumber() if rdn < beforeRDN @beforePastFlag = 'BEFORE_FIRST' return while true if rdn < afterRDN and rdn >= beforeRDN @_setFromConfig(beforeCT) return beforeCT = afterCT beforeRDN = afterRDN afterCT = beforeCT.add(1) afterRDN = afterCT.rataDieNumber() if afterCT.beforePastFlag == 'PAST_LAST' if rdn >= Time._granularitySpecs[beforeCT.granularity].endBeforeDay.rataDieNumber() @_setFromConfig(afterCT) @beforePastFlag == 'PAST_LAST' return else if rdn >= beforeRDN @_setFromConfig(beforeCT) return else throw new Error("RDN: #{rdn} seems to be out of range for #{granularity}") throw new Error("Something went badly wrong setting custom granularity #{granularity} for RDN: #{rdn}") _isGranularityCoarserThanDay: () -> </CoffeeScript> */ /** * @method granularityAboveDay * @member tzTime.Time * @private * @return {Boolean} true if the Time Object's granularity is above (coarser than) "day" level */ /* <CoffeeScript> for segment in Time._granularitySpecs[@granularity].segments if segment.indexOf('day') >= 0 return false return true getJSDate: (tz) -> </CoffeeScript> */ /** * @method getJSDate * @member tzTime.Time * @param {String} tz * @return {Date} * * Returns a JavaScript Date Object properly shifted. This Date Object can be compared to other Date Objects that you know * are already in the desired timezone. If you have data that comes from an API in GMT. You can first create a Time object from * it and then (using this getJSDate() function) you can compare it to JavaScript Date Objects created in local time. * * The full name of this function should be getJSDateInGMTasummingThisCTDateIsInTimezone(tz). It converts **TO** GMT * (actually something that can be compared to GMT). It does **NOT** convert **FROM** GMT. Use getJSDateFromGMTInTZ() * if you want to go in the other direction. * * ## Usage ## * * ct = new Time('2011-01-01') * d = new Date(Date.UTC(2011, 0, 1)) * * console.log(ct.getJSDate('GMT').getTime() == d.getTime()) * # true * * console.log(ct.inGranularity(Time.HOUR).add(-5).getJSDate('America/New_York').getTime() == d.getTime()) * # true * */ /* <CoffeeScript> if @beforePastFlag == 'PAST_LAST' return new Date(9999, 0, 1) if @beforePastFlag == 'BEFORE_FIRST' return new Date('0001-01-01') # !TODO: This may not work on all browsers utils.assert(tz?, 'Must provide a timezone when calling getJSDate') ct = this.inGranularity('millisecond') utcMilliseconds = Date.UTC(ct.year, ct.month - 1, ct.day, ct.hour, ct.minute, ct.second, ct.millisecond) offset = timezoneJS.timezone.getTzInfo(new Date(utcMilliseconds), tz).tzOffset utcMilliseconds += offset * 1000 * 60 newDate = new Date(utcMilliseconds) return newDate getISOStringInTZ: (tz) -> </CoffeeScript> */ /** * @method getISOStringInTZ * @member tzTime.Time * @param {String} tz * @return {String} The canonical ISO-8601 date in zulu representation but shifted to the specified tz * * console.log(new Time('2012-01-01').getISOStringInTZ('Europe/Berlin')) * # 2011-12-31T23:00:00.000Z */ /* <CoffeeScript> utils.assert(tz?, 'Must provide a timezone when calling getShiftedISOString') jsDate = @getJSDate(tz) return Time.getISOStringFromJSDate(jsDate) @getISOStringFromJSDate: (jsDate) -> </CoffeeScript> */ /** * @method getISOStringFromJSDate * @member tzTime.Time * @static * @param {Date} jsDate * @return {String} * * Given a JavaScript Date() Object, this will return the canonical ISO-8601 form. * * If you don't provide any parameters, it will return now, like `new Date()` except this is a zulu string. * * console.log(Time.getISOStringFromJSDate(new Date(0))) * # 1970-01-01T00:00:00.000Z */ /* <CoffeeScript> unless jsDate? jsDate = new Date() year = jsDate.getUTCFullYear() month = jsDate.getUTCMonth() + 1 day = jsDate.getUTCDate() hour = jsDate.getUTCHours() minute = jsDate.getUTCMinutes() second = jsDate.getUTCSeconds() millisecond = jsDate.getUTCMilliseconds() s = Time._pad(year, 4) + '-' + Time._pad(month, 2) + '-' + Time._pad(day, 2) + 'T' + Time._pad(hour, 2) + ':' + Time._pad(minute, 2) + ':' + Time._pad(second, 2) + '.' + Time._pad(millisecond, 3) + 'Z' return s getJSDateFromGMTInTZ: (tz) -> </CoffeeScript> */ /** * @method getJSDateInTZfromGMT * @member tzTime.Time * @param {String} tz * @return {Date} * * This assumes that the Time is an actual GMT date/time as opposed to some abstract day like Christmas and shifts * it into the specified timezone. * * Note, this function will be off by an hour for the times near midnight on the days where there is a shift to/from daylight * savings time. The tz rules engine is designed to go in the other direction so we're mis-using it. This means we are using the wrong * moment in rules-space for that hour. The cost of fixing this issue was deemed to high for chart applications. * * console.log(new Time('2012-01-01').getJSDateFromGMTInTZ('Europe/Berlin').toISOString()) * # 2012-01-01T01:00:00.000Z */ /* <CoffeeScript> if @beforePastFlag == 'PAST_LAST' return new Date(9999, 0, 1) if @beforePastFlag == 'BEFORE_FIRST' return new Date('0001-01-01') # !TODO: This may not work on all browsers utils.assert(tz?, 'Must provide a timezone when calling getJSDate') ct = this.inGranularity('millisecond') utcMilliseconds = Date.UTC(ct.year, ct.month - 1, ct.day, ct.hour, ct.minute, ct.second, ct.millisecond) offset = timezoneJS.timezone.getTzInfo(new Date(utcMilliseconds), tz).tzOffset utcMilliseconds -= offset * 1000 * 60 newDate = new Date(utcMilliseconds) return newDate getSegmentsAsObject: () -> </CoffeeScript> */ /** * @method getSegmentsAsObject * @member tzTime.Time * @return {Object} Returns a simple JavaScript Object containing the segments. This is useful when using utils.match * for holiday comparison * * t = new Time('2011-01-10') * console.log(t.getSegmentsAsObject()) * # { year: 2011, month: 1, day: 10 } */ /* <CoffeeScript> segments = Time._granularitySpecs[@granularity].segments rawObject = {} for segment in segments rawObject[segment] = this[segment] return rawObject getSegmentsAsArray: () -> </CoffeeScript> */ /** * @method getSegmentsAsArray * @member tzTime.Time * @return {Array} Returns a simple JavaScript Array containing the segments. This is useful for doing hierarchical * aggregations using Lumenize.OLAPCube. * * t = new Time('2011-01-10') * console.log(t.getSegmentsAsArray()) * # [ 2011, 1, 10 ] */ /* <CoffeeScript> segments = Time._granularitySpecs[@granularity].segments a = [] for segment in segments a.push(this[segment]) return a toString: () -> </CoffeeScript> */ /** * @method toString * @member tzTime.Time * @return {String} Uses granularity `mask` in _granularitySpecs to generate the string representation. * * t = new Time({year: 2012, month: 1, day: 1, granularity: Time.MINUTE}).toString() * console.log(t.toString()) * console.log(t) * # 2012-01-01T00:00 * # 2012-01-01T00:00 */ /* <CoffeeScript> if @beforePastFlag in ['BEFORE_FIRST', 'PAST_LAST'] s = "#{@beforePastFlag}" else s = Time._granularitySpecs[@granularity].mask segments = Time._granularitySpecs[@granularity].segments for segment in segments granularitySpec = Time._granularitySpecs[segment] l = granularitySpec.segmentLength start = granularitySpec.segmentStart before = s.slice(0, start) after = s.slice(start + l) s = before + Time._pad(this[segment], l) + after return s @_pad = (n, l) -> result = n.toString() while result.length < l result = '0' + result return result @DOW_N_TO_S_MAP = {0: 'Sunday', 1: 'Monday', 2: 'Tuesday', 3: 'Wednesday', 4: 'Thursday', 5: 'Friday', 6: 'Saturday', 7: 'Sunday'} @DOW_MONTH_TABLE = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4] dowNumber: () -> </CoffeeScript> */ /** * @method dowNumber * @member tzTime.Time * @return {Number} * Returns the day of the week as a number. Monday = 1, Sunday = 7 * * console.log(new Time('2012-01-01').dowNumber()) * # 7 */ /* <CoffeeScript> if @granularity == 'week_day' return @week_day if @granularity in ['day', 'hour', 'minute', 'second', 'millisecond'] y = @year if (@month < 3) then y-- dayNumber = (y + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400) + Time.DOW_MONTH_TABLE[@month-1] + @day) % 7 if dayNumber == 0 return 7 else return dayNumber else return @inGranularity('day').dowNumber() dowString: () -> </CoffeeScript> */ /** * @method dowString * @member tzTime.Time * @return {String} Returns the day of the week as a String (e.g. "Monday") * * console.log(new Time('2012-01-01').dowString()) * # Sunday */ /* <CoffeeScript> return Time.DOW_N_TO_S_MAP[@dowNumber()] rataDieNumber: () -> </CoffeeScript> */ /** * @method rataDieNumber * @member tzTime.Time * @return {Number} Returns the counting number for days starting with 0001-01-01 (i.e. 0 AD). Note, this differs * from the Unix Epoch which starts on 1970-01-01. This function works for * granularities finer than day (hour, minute, second, millisecond) but ignores the segments of finer granularity than * day. Also called common era days. * * console.log(new Time('0001-01-01').rataDieNumber()) * # 1 * * rdn2012 = new Time('2012-01-01').rataDieNumber() * rdn1970 = new Time('1970-01-01').rataDieNumber() * ms1970To2012 = (rdn2012 - rdn1970) * 24 * 60 * 60 * 1000 * msJSDate2012 = Number(new Date('2012-01-01')) * console.log(ms1970To2012 == msJSDate2012) * # true */ /* <CoffeeScript> if @beforePastFlag == 'BEFORE_FIRST' return -1 else if @beforePastFlag == 'PAST_LAST' return utils.MAX_INT else if Time._granularitySpecs[@granularity].rataDieNumber? return Time._granularitySpecs[@granularity].rataDieNumber(this) else y = @year - 1 yearDays = y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400) ew = Math.floor((yearDays + 3) / 7) # algorithm for week/week_day from http://en.wikipedia.org/wiki/Talk:ISO_week_date if @month? monthDays = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][@month - 1] if @isLeapYear() and @month >= 3 monthDays++ else if @quarter? monthDays = [0, 90, 181, 273][@quarter - 1] if @isLeapYear() and @quarter >= 2 monthDays++ else monthDays = 0 switch @granularity when 'year' return yearDays + 1 when 'month', 'quarter' return yearDays + monthDays + 1 when 'day', 'hour', 'minute', 'second', 'millisecond' return yearDays + monthDays + @day when 'week' return (ew + @week - 1) * 7 + 1 when 'week_day' return (ew + @week - 1) * 7 + @week_day # !TODO: I think the code below should work but the mere adding of a valueOf() method changes the type coercion so 134 tests start failing with this method added. I'll fix the tests and add this method some other time. # @MILLISECONDS_IN_A_MINUTE = 60 * 1000 # @MILLISECONDS_IN_AN_HOUR = 60 * Time.MILLISECONDS_IN_A_MINUTE # @MILLISECONDS_IN_A_DAY = 24 * Time.MILLISECONDS_IN_AN_HOUR # valueOf: () -> # ### # @method getTime # Lumenize.Time is not meant to be a drop in replacement for the JavaScript Date() Object. However, it's sometimes # nice to do Date math with operators (e.g. +, -, ==, etc.). This function allows you to compare Date() Objects # to Lumenize.Time Object with these operators. Think of it as poor man's operator overload. # @return {Number} The milliseconds since the Unix Epoch (1970-01-01). # # console.log(new Time('1970-01-01').valueOf()) # # 0 # ### # time = this.inGranularity(Time.MILLISECOND) # days = time.rataDieNumber() - new Time('1970-01-01').rataDieNumber() # return days * Time.MILLISECONDS_IN_A_DAY + time.hour * Time.MILLISECONDS_IN_AN_HOUR + time.minute * Time.MILLISECONDS_IN_A_MINUTE + Time.second * 1000 + Time.millisecond inGranularity: (granularity) -> </CoffeeScript> */ /** * @method inGranularity * @member tzTime.Time * @param {String} granularity * @return {Time} Returns a new Time object for the same date-time as this object but in the specified granularity. * Fills in missing finer granularity segments with `lowest` values. Drops segments when convernting to a coarser * granularity. * * console.log(new Time('2012W01-1').inGranularity(Time.DAY).toString()) * # 2012-01-02 * * console.log(new Time('2012Q3').inGranularity(Time.MONTH).toString()) * # 2012-07 */ /* <CoffeeScript> if @granularity in ['year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'] if granularity in ['year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'] tempGranularity = @granularity @granularity = granularity newTime = new Time(this) @granularity = tempGranularity return newTime return new Time(this.rataDieNumber(), granularity) daysInMonth: () -> </CoffeeScript> */ /** * @method daysInMonth * @member tzTime.Time * @return {Number} Returns the number of days in the current month for this Time * * console.log(new Time('2012-02').daysInMonth()) * # 29 */ /* <CoffeeScript> # !TODO: Error on granularities without month switch @month when 4, 6, 9, 11 return 30 when 1, 3, 5, 7, 8, 10, 12, 0 # Treating 0 like 12 for wrapping on decrement (bit of a hack but it works) return 31 when 2 if @isLeapYear() return 29 else return 28 isLeapYear: () -> </CoffeeScript> */ /** * @method isLeapYear * @member tzTime.Time * @return {Boolean} true if this is a leap year * * console.log(new Time('2012').isLeapYear()) * # true */ /* <CoffeeScript> if (@year % 4 == 0) if (@year % 100 == 0) if (@year % 400 == 0) return true else return false else return true else return false @YEARS_WITH_53_WEEKS = [4, 9, 15, 20, 26, 32, 37, 43, 48, 54, 60, 65, 71, 76, 82, 88, 93, 99, 105, 111, 116, 122, 128, 133, 139, 144, 150, 156, 161, 167, 172, 178, 184, 189, 195, 201, 207, 212, 218, 224, 229, 235, 240, 246, 252, 257, 263, 268, 274, 280, 285, 291, 296, 303, 308, 314, 320, 325, 331, 336, 342, 348, 353, 359, 364, 370, 376, 381, 387, 392, 398] is53WeekYear: () -> </CoffeeScript> */ /** * @method is53WeekYear * @member tzTime.Time * @return {Boolean} true if this is a 53-week year * * console.log(new Time('2015').is53WeekYear()) * # true */ /* <CoffeeScript> lookup = @year % 400 return lookup in Time.YEARS_WITH_53_WEEKS equal: (other) -> </CoffeeScript> */ /** * @method equal * @member tzTime.Time * @param {Time} other * @return {Boolean} Returns true if this equals other. Throws an error if the granularities don't match. * * d3 = new Time({granularity: Time.DAY, year: 2011, month: 12, day: 31}) * d4 = new Time('2012-01-01').add(-1) * console.log(d3.equal(d4)) * # true */ /* <CoffeeScript> utils.assert(@granularity == other.granularity, "Granulary of #{this} does not match granularity of #{other} on equality/inequality test") if @beforePastFlag == 'PAST_LAST' and other.beforePastFlag == 'PAST_LAST' return true if @beforePastFlag == 'BEFORE_FIRST' and other.beforePastFlag == 'BEFORE_FIRST' return true if @beforePastFlag == 'PAST_LAST' and other.beforePastFlag != 'PAST_LAST' return false if @beforePastFlag == 'BEFORE_FIRST' and other.beforePastFlag != 'BEFORE_FIRST' return false if other.beforePastFlag == 'PAST_LAST' and @beforePastFlag != 'PAST_LAST' return false if other.beforePastFlag == 'BEFORE_FIRST' and @beforePastFlag != 'BEFORE_FIRST' return false segments = Time._granularitySpecs[@granularity].segments for segment in segments if this[segment] != other[segment] return false return true greaterThan: (other) -> </CoffeeScript> */ /** * @method greaterThan * @member tzTime.Time * @param {Time} other * @return {Boolean} Returns true if this is greater than other. Throws an error if the granularities don't match * * d1 = new Time({granularity: Time.DAY, year: 2011, month: 2, day: 28}) * d2 = new Time({granularity: Time.DAY, year: 2011, month: 3, day: 1}) * console.log(d1.greaterThan(d2)) * # false * console.log(d2.greaterThan(d1)) * # true */ /* <CoffeeScript> utils.assert(@granularity == other.granularity, "Granulary of #{this} does not match granularity of #{other} on equality/inequality test") if @beforePastFlag == 'PAST_LAST' and other.beforePastFlag == 'PAST_LAST' return false if @beforePastFlag == 'BEFORE_FIRST' and other.beforePastFlag == 'BEFORE_FIRST' return false if @beforePastFlag == 'PAST_LAST' and other.beforePastFlag != 'PAST_LAST' return true if @beforePastFlag == 'BEFORE_FIRST' and other.beforePastFlag != 'BEFORE_FIRST' return false if other.beforePastFlag == 'PAST_LAST' and @beforePastFlag != 'PAST_LAST' return false if other.beforePastFlag == 'BEFORE_FIRST' and @beforePastFlag != 'BEFORE_FIRST' return true segments = Time._granularitySpecs[@granularity].segments for segment in segments if this[segment] > other[segment] return true if this[segment] < other[segment] return false return false greaterThanOrEqual: (other) -> </CoffeeScript> */ /** * @method greaterThanOrEqual * @member tzTime.Time * @param {Time} other * @return {Boolean} Returns true if this is greater than or equal to other * * console.log(new Time('2012').greaterThanOrEqual(new Time('2012'))) * # true */ /* <CoffeeScript> gt = this.greaterThan(other) if gt return true return this.equal(other) lessThan: (other) -> </CoffeeScript> */ /** * @method lessThan * @member tzTime.Time * @param {Time} other * @return {Boolean} Returns true if this is less than other * * console.log(new Time(1000, Time.DAY).lessThan(new Time(999, Time.DAY))) # Using RDN constructor * # false */ /* <CoffeeScript> return other.greaterThan(this) lessThanOrEqual: (other) -> </CoffeeScript> */ /** * @method lessThanOrEqual * @member tzTime.Time * @param {Time} other * @return {Boolean} Returns true if this is less than or equal to other * * console.log(new Time('this day').lessThanOrEqual(new Time('next day'))) # Using relative constructor * # true */ /* <CoffeeScript> return other.greaterThanOrEqual(this) _overUnderFlow: () -> if @beforePastFlag in ['BEFORE_FIRST', 'PAST_LAST'] return true else granularitySpec = Time._granularitySpecs[@granularity] highestLevel = granularitySpec.segments[0] highestLevelSpec = Time._granularitySpecs[highestLevel] value = this[highestLevel] rolloverValue = highestLevelSpec.rolloverValue(this) lowest = highestLevelSpec.lowest if value >= rolloverValue @beforePastFlag = 'PAST_LAST' # !TODO: This won't erase the other segments. Maybe that's OK. return true else if value < lowest @beforePastFlag = 'BEFORE_FIRST' return true else return false decrement: (granularity) -> </CoffeeScript> */ /** * @method decrement * @member tzTime.Time * @param {String} [granularity] * @chainable * @return {Time} * Decrements this by 1 in the granularity of the Time or the granularity specified if it was specified * * console.log(new Time('2016W01').decrement().toString()) * # 2015W53 */ /* <CoffeeScript> if @beforePastFlag == 'PAST_LAST' @beforePastFlag = '' granularitySpec = Time._granularitySpecs[@granularity] segments = granularitySpec.segments for segment in segments gs = Time._granularitySpecs[segment] this[segment] = gs.rolloverValue(this) - 1 else lastDayInMonthFlag = (@day == @daysInMonth()) granularity ?= @granularity granularitySpec = Time._granularitySpecs[granularity] segments = granularitySpec.segments this[granularity]-- if granularity is 'year' # Fix it if you decrement from a leap year to a non-leap year if @day > @daysInMonth() @day = @daysInMonth() else i = segments.length - 1 # start just before the last one which should equal granularity segment = segments[i] granularitySpec = Time._granularitySpecs[segment] while (i > 0) and (this[segment] < granularitySpec.lowest) # stop before going back to year this[segments[i - 1]]-- this[segment] = granularitySpec.rolloverValue(this) - 1 i-- segment = segments[i] granularitySpec = Time._granularitySpecs[segment] if granularity == 'month' and (@granularity != 'month') if lastDayInMonthFlag or (@day > @daysInMonth()) @day = @daysInMonth() @_overUnderFlow() return this increment: (granularity) -> </CoffeeScript> */ /** * @method increment * @member tzTime.Time * @param {String} [granularity] * @chainable * @return {Time} * Increments this by 1 in the granularity of the Time or the granularity specified if it was specified * * console.log(new Time('2012Q4').increment().toString()) * # 2013Q1 */ /* <CoffeeScript> if @beforePastFlag == 'BEFORE_FIRST' @beforePastFlag = '' granularitySpec = Time._granularitySpecs[@granularity] segments = granularitySpec.segments for segment in segments gs = Time._granularitySpecs[segment] this[segment] = gs.lowest else lastDayInMonthFlag = (@day == @daysInMonth()) granularity ?= @granularity granularitySpec = Time._granularitySpecs[granularity] segments = granularitySpec.segments this[granularity]++ if granularity is 'year' # !TODO: Add support for week granularity and 53 week years # Fix it if you increment from a leap year to a non-leap year if @day > @daysInMonth() @day = @daysInMonth() else i = segments.length - 1 # start just before the last one which should equal granularity segment = segments[i] granularitySpec = Time._granularitySpecs[segment] while (i > 0) and (this[segment] >= granularitySpec.rolloverValue(this)) # stop before going back to year this[segment] = granularitySpec.lowest this[segments[i - 1]]++ i-- segment = segments[i] granularitySpec = Time._granularitySpecs[segment] if (granularity is 'month') and (@granularity isnt 'month') if lastDayInMonthFlag or (@day > @daysInMonth()) @day = @daysInMonth() @_overUnderFlow() return this addInPlace: (qty, granularity) -> </CoffeeScript> */ /** * @method addInPlace * @member tzTime.Time * @chainable * @param {Number} qty Can be negative for subtraction * @param {String} [granularity] * @return {Time} Adds qty to the Time object. It uses increment and decrement so it's not going to be efficient for large values * of qty, but it should be fine for charts where we'll increment/decrement small values of qty. * * console.log(new Time('2011-11-01').addInPlace(3, Time.MONTH).toString()) * # 2012-02-01 */ /* <CoffeeScript> granularity ?= @granularity if qty == 0 return this if qty == 1 @increment(granularity) else if qty > 1 @increment(granularity) @addInPlace(qty - 1, granularity) else if qty == -1 @decrement(granularity) else # must be < -1 @decrement(granularity) @addInPlace(qty + 1, granularity) return this add: (qty, granularity) -> </CoffeeScript> */ /** * @method add * @member tzTime.Time * @param {Number} qty * @param {String} [granularity] * @return {Time} * Adds (or subtracts) quantity (negative quantity) and returns a new Time. Not efficient for large qty. * * console.log(new Time('2012-01-01').add(-10, Time.MONTH)) * # 2011-03-01 */ /* <CoffeeScript> newTime = new Time(this) newTime.addInPlace(qty, granularity) return newTime @addGranularity: (granularitySpec) -> </CoffeeScript> */ /** * @method addGranularity * @member tzTime.Time * @static * @param {Object} granularitySpec see {@link Time#_granularitySpecs} for existing _granularitySpecs * @cfg {String[]} segments an Array identifying the ancestry (e.g. for 'day', it is: `['year', 'month', 'day']`) * @cfg {String} mask a String used to identify when this granularity is passed in and to serialize it on the way out. * @cfg {Number} lowest the lowest possible value for this granularity. 0 for millisecond but 1 for day. * @cfg {Function} rolloverValue a callback function that will say when to rollover the next coarser granularity. * * addGranularity allows you to add your own hierarchical granularities to Time. Once you add a granularity to Time * you can then instantiate Time objects in your newly specified granularity. You specify new granularities with * granularitySpec object like this: * * granularitySpec = { * release: { * segments: ['release'], * mask: 'R##', * lowest: 1, * endBeforeDay: new Time('2011-07-01') * rolloverValue: (ct) -> * return Time._granularitySpecs.iteration.timeBoxes.length + 1 # Yes, it's correct to use the length of iteration.timeBoxes * rataDieNumber: (ct) -> * return Time._granularitySpecs.iteration.timeBoxes[ct.release-1][1-1].startOn.rataDieNumber() * }, * iteration: { * segments: ['release', 'iteration'], * mask: 'R##I##', * lowest: 1, * endBeforeDay: new Time('2011-07-01') * timeBoxes: [ * [ * {startOn: new Time('2011-01-01'), label: 'R1 Iteration 1'}, * {startOn: new Time('2011-02-01'), label: 'R1 Iteration 2'}, * {startOn: new Time('2011-03-01'), label: 'R1 Iteration 3'}, * ], * [ * {startOn: new Time('2011-04-01'), label: 'R2 Iteration 1'}, * {startOn: new Time('2011-05-01'), label: 'R2 Iteration 2'}, * {startOn: new Time('2011-06-01'), label: 'R2 Iteration 3'}, * ] * ] * rolloverValue: (ct) -> * temp = Time._granularitySpecs.iteration.timeBoxes[ct.release-1]?.length + 1 * if temp? and not isNaN(temp) and ct.beforePastFlag != 'PAST_LAST' * return temp * else * numberOfReleases = Time._granularitySpecs.iteration.timeBoxes.length * return Time._granularitySpecs.iteration.timeBoxes[numberOfReleases-1].length + 1 * * rataDieNumber: (ct) -> * return Time._granularitySpecs.iteration.timeBoxes[ct.release-1][ct.iteration-1].startOn.rataDieNumber() * }, * iteration_day: { # By convention, it knows to use day functions on it. This is the lowest allowed custom granularity * segments: ['release', 'iteration', 'iteration_day'], * mask: 'R##I##-##', * lowest: 1, * endBeforeDay: new Time('2011-07-01'), * rolloverValue: (ct) -> * iterationTimeBox = Time._granularitySpecs.iteration.timeBoxes[ct.release-1]?[ct.iteration-1] * if !iterationTimeBox? or ct.beforePastFlag == 'PAST_LAST' * numberOfReleases = Time._granularitySpecs.iteration.timeBoxes.length * numberOfIterationsInLastRelease = Time._granularitySpecs.iteration.timeBoxes[numberOfReleases-1].length * iterationTimeBox = Time._granularitySpecs.iteration.timeBoxes[numberOfReleases-1][numberOfIterationsInLastRelease-1] * * thisIteration = iterationTimeBox.startOn.inGranularity('iteration') * nextIteration = thisIteration.add(1) * if nextIteration.beforePastFlag == 'PAST_LAST' * return Time._granularitySpecs.iteration_day.endBeforeDay.rataDieNumber() - iterationTimeBox.startOn.rataDieNumber() + 1 * else * return nextIteration.rataDieNumber() - iterationTimeBox.startOn.rataDieNumber() + 1 * * rataDieNumber: (ct) -> * return Time._granularitySpecs.iteration.timeBoxes[ct.release-1][ct.iteration-1].startOn.rataDieNumber() + ct.iteration_day - 1 * } * } * Time.addGranularity(granularitySpec) * * * The `mask` must cover all of the segments to get down to the granularity being specified. The digits of the granularity segments * are represented with `#`. Any other characters can be used as a delimeter, but it should always be one character to comply with * the expectations of the Lumenize hierarchy visualizations. All of the standard granularities start with a 4-digit year to * distinguish your custom granularity, your highest level must start with some number of digits other than 4 or a prefix letter * (`R` in the example above). * * In order for the TimelineIterator to work, you must provide `rolloverValue` and `rataDieNumber` callback functions. You should * be able to mimic (or use as-is) the example above for most use cases. Notice how the `rataDieNumber` function simply leverages * `rataDieNumber` functions for the standard granularities. * * In order to convert into this granularity from some other granularity, you must provide an `inGranularity` callback [NOT YET IMPLEMENTED]. * But Time will convert to any of the standard granularities from even custom granularities as long as a `rataDieNumber()` function * is provided. * * **The `timeBoxes` property in the `granularitySpec` Object above has no special meaning** to Time or TimelineIterator. It's simply used * by the `rolloverValue` and `rataDieNumber` functions. The boundaries could come from where ever you want and even have been encoded as * literals in the `rolloverValue` and `rataDieNumber` callback functions. * * The convention of naming the lowest order granularity with `_day` at the end IS signficant. Time knows to treat that as a day-level * granularity. If there is a use-case for it, Time could be upgraded to allow you to drill down into hours, minutes, etc. from any * `_day` granularity but right now those lower order time granularities are only supported for the canonical ISO-6801 form. * */ /* <CoffeeScript> for g, spec of granularitySpec # !TODO: Need a way for the user to provide a loose stream of timebox dates that are converted into this format. Would cleanup situations where the timeboxes overlapped and error out on impossible situations like nested timeboxes. Use the startOn. Time._expandMask(spec) # !TODO: Add @label() and @end() methods. @_granularitySpecs[g] = spec # !TODO: Assert that we don't have a conflict with existing granularities and that the granularity equals the last segment. Assert that the endBefore date of one equals the startOn date of the next. Time[g.toUpperCase()] = g exports.Time = Time </CoffeeScript> */