twix
Version:
Twix.js allows you to work with date ranges
469 lines (363 loc) • 14.6 kB
text/coffeescript
hasModule = module? && module.exports? && typeof(require) == 'function'
isArray = (input) ->
Object.prototype.toString.call(input) == '[object Array]'
makeTwix = (moment) ->
throw new Error("Can't find moment") unless moment?
class Twix
constructor: (start, end, parseFormat, options = {}) ->
unless typeof parseFormat == 'string'
options = parseFormat ? {}
parseFormat = null
options = {allDay: options} if typeof options == 'boolean'
= moment start, parseFormat, options.parseStrict
= moment end, parseFormat, options.parseStrict
= options.allDay ? false
: (first, others...) ->
for other in others
for attr of other
first[attr] = other[attr] unless typeof other[attr] == 'undefined'
first
# -- INFORMATIONAL --
start: -> .clone()
end: -> .clone()
isSame: (period) -> .isSame , period
length: (period, floatingPoint = false) ->
.diff , period, floatingPoint
count: (period) ->
start = .startOf period
end = .startOf period
end.diff(start, period) + 1
countInner: (period) ->
[start, end] = period
return 0 if start >= end
end.diff(start, period)
iterate: (intervalAmount, period, minHours) ->
[intervalAmount, period, minHours] = intervalAmount, period, minHours
start = .startOf period
end = .startOf period
end = end.add(1, 'd') if
hasNext = => (! && start <= end && (!minHours || !start.isSame(end) || .hours() > minHours)) || ( && start < end)
period, start, hasNext, intervalAmount
iterateInner: (intervalAmount, period) ->
[intervalAmount, period] = intervalAmount, period
[start, end] = period, intervalAmount
hasNext = -> start < end
period, start, hasNext, intervalAmount
humanizeLength: ->
if
if 'd'
'all day'
else
.from(.add(1, 'd'), true)
else
.from(, true)
asDuration: (units) ->
diff = .diff
moment.duration(diff)
isPast: ->
< moment()
isFuture: ->
> moment()
isCurrent: -> ! && !
contains: (mom) ->
mom = moment mom unless moment.isMoment(mom)
<= mom && >= mom
isEmpty: ->
.isSame()
# -- WORK WITH MULTIPLE RANGES --
overlaps: (other) -> (.isAfter(other._start) && .isBefore(other._displayEnd))
engulfs: (other) -> <= other._start && >= other._displayEnd
union: (other) ->
allDay = && other.allDay
newStart = if < other._start then else other._start
newEnd = if > other._lastMilli
(if allDay then else )
else
(if allDay then other._end else other._displayEnd)
new Twix(newStart, newEnd, allDay)
intersection: (other) ->
allDay = && other.allDay
newStart = if > other._start then else other._start
newEnd = if < other._lastMilli
(if allDay then else )
else
(if allDay then other._end else other._displayEnd)
new Twix(newStart, newEnd, allDay)
xor: (others...) ->
open = 0
start = null
results = []
allDay = (o for o in others when o.allDay).length == others.length
arr = []
for item, i in [this].concat(others)
arr.push({time: item._start, i: i, type: 0})
arr.push({time: item._displayEnd, i: i, type: 1})
arr = arr.sort((a, b) -> a.time - b.time)
for other in arr
open -= 1 if other.type == 1
if open == other.type
start = other.time
if open == (other.type + 1) % 2
if start
last = results[results.length - 1]
if last && last._end.isSame(start)
last._oEnd = other.time
last._mutated()
else
#because we used the diffable end, we have to subtract back off a day. blech
endTime = if allDay then other.time.clone().subtract(1, 'd') else other.time
t = new Twix(start, endTime, allDay)
results.push(t) if !t.isEmpty()
start = null
open += 1 if other.type == 0
results
difference: (others...) ->
t for t in .map((i) => ) when !t.isEmpty() && t.isValid()
split: (args...) ->
end = start =
if moment.isDuration(args[0])
dur = args[0]
else if (!moment.isMoment(args[0]) && !isArray(args[0]) && typeof args[0] == 'object') || (typeof args[0] == 'number' && typeof args[1] == 'string')
dur = moment.duration args[0], args[1]
else if isArray(args[0])
times = args[0]
else
times = args
if times
times = (moment(time) for time in times)
times = (mom for mom in times when mom.isValid() && mom >= start).sort((a, b) -> a.valueOf() - b.valueOf())
return [this] if (dur && dur.asMilliseconds() == 0) || (times && times.length == 0)
vals = []; i = 0; final =
while start < final && (!times? || times[i])
end = if dur then start.clone().add(dur) else times[i].clone()
end = moment.min(final, end)
vals.push(moment.twix(start, end)) if !start.isSame(end)
start = end
i += 1
if !end.isSame() && times
vals.push(moment.twix(end, ))
vals
divide: (parts) ->
[0..(parts - 1)]
isValid: ->
.isValid() && .isValid() && <=
equals: (other) ->
(other instanceof Twix) &&
== other.allDay &&
.valueOf() == other._start.valueOf() &&
.valueOf() == other._end.valueOf()
# -- FORMATING --
toString: -> "{start: #{@_start.format()}, end: #{@_end.format()}, allDay: #{if @allDay then 'true' else 'false'}}"
toArray: (intervalAmount, period, minHours) ->
itr =
range = []
while itr.hasNext()
range.push itr.next()
range
simpleFormat: (momentOpts, inopts) ->
options =
allDay: '(all day)'
template: Twix.formatTemplate
Twix._extend options, (inopts || {})
s = options.template .format(momentOpts), .format(momentOpts)
s += " #{options.allDay}" if && options.allDay
s
format: (inopts) ->
return '' if
momentHourFormat = .localeData()._longDateFormat['LT'][0]
options =
groupMeridiems: true
spaceBeforeMeridiem: true
spaceBeforeMonth: true
spaceBeforeDay: true
showDayOfWeek: false
showYearFirst: false
hideTime: false
hideYear: false
implicitMinutes: true
implicitDate: false
implicitYear: true
yearFormat: 'YYYY'
monthFormat: 'MMM'
weekdayFormat: 'ddd'
dayFormat: 'D'
meridiemFormat: 'A'
hourFormat: momentHourFormat
minuteFormat: 'mm'
allDay: 'all day'
explicitAllDay: false
lastNightEndsAt: 0
template: Twix.formatTemplate
Twix._extend options, (inopts || {})
fs = []
needsMeridiem = options.hourFormat && options.hourFormat[0] == 'h'
localFormat = .localeData()._longDateFormat['L']
americanish = localFormat.indexOf('M') < localFormat.indexOf('D')
goesIntoTheMorning =
options.lastNightEndsAt > 0 &&
! &&
.startOf('d').valueOf() == .add(1, 'd').startOf('d').valueOf() &&
.hours() > 12 &&
.hours() < options.lastNightEndsAt
needDate = !options.hideDate &&
(!options.implicitDate || .startOf('d').valueOf() != moment().startOf('d').valueOf() || !( || goesIntoTheMorning))
atomicMonthDate = !( || options.hideTime)
if && && (options.implicitDate || options.explicitAllDay)
fs.push
name: 'all day simple'
fn: () -> options.allDay
pre: ' '
slot: 0
if needDate && !options.hideYear && (!options.implicitYear || .year() != moment().year() || !)
fs.push
name: 'year',
fn: (date) -> date.format options.yearFormat
pre: if americanish then ', ' else ' '
slot: if options.showYearFirst then -1 else 4
if atomicMonthDate && needDate
fs.push
name: 'month-date'
fn: (date) ->
format =
if americanish
"#{options.monthFormat} #{options.dayFormat}"
else
"#{options.dayFormat} #{options.monthFormat}"
date.format format
ignoreEnd: -> goesIntoTheMorning
pre: ' '
slot: 2
if !atomicMonthDate && needDate
fs.push
name: 'month'
fn: (date) -> date.format options.monthFormat
pre: if options.spaceBeforeMonth then ' ' else ''
slot: if americanish then 2 else 3
if !atomicMonthDate && needDate
fs.push
name: 'date'
fn: (date) -> date.format options.dayFormat
pre: if options.spaceBeforeDay then ' ' else ''
slot: if americanish then 3 else 2
if needDate && options.showDayOfWeek
fs.push
name: 'day of week',
fn: (date) -> date.format options.weekdayFormat
pre: ' '
slot: 1
if options.groupMeridiems && needsMeridiem && ! && !options.hideTime
fs.push
name: 'meridiem',
fn: (t) -> t.format options.meridiemFormat
slot: 6
pre: if options.spaceBeforeMeridiem then ' ' else ''
if ! && !options.hideTime
fs.push
name: 'time',
fn: (date) ->
str =
if date.minutes() == 0 && options.implicitMinutes && needsMeridiem
date.format options.hourFormat
else
date.format "#{options.hourFormat}:#{options.minuteFormat}"
if !options.groupMeridiems && needsMeridiem
str += ' ' if options.spaceBeforeMeridiem
str += date.format options.meridiemFormat
str
slot: 5
pre: ', '
start_bucket = []
end_bucket = []
common_bucket = []
together = true
process = (format) =>
start_str = format.fn
end_str =
if format.ignoreEnd && format.ignoreEnd()
start_str
else format.fn
start_group = {format: format, value: -> start_str}
if end_str == start_str && together
common_bucket.push start_group
else
if together
together = false
common_bucket.push {
format: {slot: format.slot, pre: ''}
value: -> options.template(fold(start_bucket), fold(end_bucket, true).trim())
}
start_bucket.push start_group
end_bucket.push {format: format, value: -> end_str}
process format for format in fs
global_first = true
fold = (array, skip_pre) ->
local_first = true
str = ''
for section in array.sort((a, b) -> a.format.slot - b.format.slot)
unless global_first
if local_first && skip_pre
str += ' '
else
str += section.format.pre
str += section.value()
global_first = false
local_first = false
str
fold common_bucket
# -- INTERNAL --
_iterateHelper: (period, iter, hasNext, intervalAmount) ->
next: ->
unless hasNext()
null
else
val = iter.clone()
iter.add(intervalAmount, period)
val
hasNext: hasNext
_prepIterateInputs: (inputs...) ->
return inputs if typeof inputs[0] is 'number'
if typeof inputs[0] == 'string'
period = inputs.shift()
intervalAmount = inputs.pop() ? 1
if inputs.length
minHours = inputs[0] ? false
if moment.isDuration inputs[0]
period = 'ms'
intervalAmount = inputs[0].as period
[intervalAmount, period, minHours]
_inner: (period = 'ms', intervalAmount = 1) ->
start =
end = .clone()
start.startOf(period).add(intervalAmount, period) if start > start.clone().startOf(period)
end.startOf(period) if end < end.clone().endOf(period)
durationPeriod = start.twix(end).asDuration period
durationCount = durationPeriod.get(period)
modulus = durationCount % intervalAmount
end.subtract(modulus, period)
[start, end]
_mutated: ->
= if then .clone().startOf('d') else
= if then .clone().endOf('d') else
= if then .clone().startOf('d') else
= if then .clone().add(1, 'd') else
# -- PLUGIN --
Twix._extend(moment.locale(), _twix: Twix.defaults)
Twix.formatTemplate = (leftSide, rightSide) -> "#{leftSide} - #{rightSide}"
moment.twix = -> new Twix(arguments...)
moment.fn.twix = -> new Twix(this, arguments...)
moment.fn.forDuration = (duration, allDay) -> new Twix(this, this.clone().add(duration), allDay)
if moment.duration.fn
moment.duration.fn.afterMoment = (startingTime, allDay) -> new Twix(startingTime, moment(startingTime).clone().add(this), allDay)
moment.duration.fn.beforeMoment = (startingTime, allDay) -> new Twix(moment(startingTime).clone().subtract(this), startingTime, allDay)
moment.twixClass = Twix
Twix
# -- MAKE AVAILABLE
return module.exports = makeTwix(require 'moment') if hasModule
if typeof(define) == 'function' && define.amd
define 'twix', ['moment'], (moment) -> makeTwix(moment)
if
= makeTwix()
else if moment?
# Also checks globals (Meteor)
= makeTwix(moment)