Adam Shaw %!s(int64=7) %!d(string=hai) anos
pai
achega
1ed1cf3c25
Modificáronse 58 ficheiros con 1341 adicións e 2180 borrados
  1. 14 18
      plugins/gcal/GcalEventSource.ts
  2. 72 228
      src/Calendar.ts
  3. 14 21
      src/Constraints.ts
  4. 73 72
      src/DateProfileGenerator.ts
  5. 1 2
      src/OptionsManager.ts
  6. 88 58
      src/View.ts
  7. 13 14
      src/ViewSpecManager.ts
  8. 6 5
      src/agenda/AgendaView.ts
  9. 55 47
      src/agenda/TimeGrid.ts
  10. 16 9
      src/agenda/TimeGridEventRenderer.ts
  11. 2 1
      src/basic/BasicView.ts
  12. 9 5
      src/basic/BasicViewDateProfileGenerator.ts
  13. 18 17
      src/basic/DayGrid.ts
  14. 6 1
      src/basic/DayGridEventRenderer.ts
  15. 5 3
      src/basic/MonthView.ts
  16. 6 4
      src/basic/MonthViewDateProfileGenerator.ts
  17. 54 52
      src/component/DateComponent.ts
  18. 38 33
      src/component/DayTableMixin.ts
  19. 10 7
      src/component/InteractiveDateComponent.ts
  20. 4 4
      src/component/interactions/DateSelecting.ts
  21. 6 6
      src/component/interactions/EventDragging.ts
  22. 3 3
      src/component/interactions/EventPointing.ts
  23. 10 8
      src/component/interactions/EventResizing.ts
  24. 12 17
      src/component/interactions/ExternalDropping.ts
  25. 47 24
      src/component/renderers/EventRenderer.ts
  26. 0 441
      src/date-formatting.ts
  27. 144 2
      src/datelib/duration.ts
  28. 201 24
      src/datelib/env.ts
  29. 8 7
      src/datelib/formatting.ts
  30. 6 7
      src/datelib/parsing.ts
  31. 107 0
      src/datelib/util.ts
  32. 5 19
      src/exports.ts
  33. 6 3
      src/list/ListEventRenderer.ts
  34. 24 16
      src/list/ListView.ts
  35. 6 6
      src/list/config.ts
  36. 2 180
      src/locale.ts
  37. 0 2
      src/main.ts
  38. 2 2
      src/models/ComponentFootprint.ts
  39. 3 3
      src/models/EventManager.ts
  40. 9 12
      src/models/EventPeriod.ts
  41. 53 96
      src/models/UnzonedRange.ts
  42. 2 20
      src/models/event-source/ArrayEventSource.ts
  43. 3 1
      src/models/event-source/EventSource.ts
  44. 9 3
      src/models/event-source/FuncEventSource.ts
  45. 9 7
      src/models/event-source/JsonFeedEventSource.ts
  46. 39 59
      src/models/event/EventDateProfile.ts
  47. 47 65
      src/models/event/EventDefDateMutation.ts
  48. 3 2
      src/models/event/EventDefMutation.ts
  49. 11 6
      src/models/event/EventDefParser.ts
  50. 2 2
      src/models/event/EventFootprint.ts
  51. 6 3
      src/models/event/EventInstance.ts
  52. 23 23
      src/models/event/RecurringEventDef.ts
  53. 1 13
      src/models/event/SingleEventDef.ts
  54. 0 308
      src/moment-ext.ts
  55. 2 8
      src/options.ts
  56. 26 26
      src/types/input-types.ts
  57. 0 150
      src/util/date.ts
  58. 0 5
      src/util/html.ts

+ 14 - 18
plugins/gcal/GcalEventSource.ts

@@ -1,5 +1,5 @@
 import * as request from 'superagent'
-import { EventSource, warn, applyAll, assignTo } from 'fullcalendar'
+import { EventSource, warn, applyAll, assignTo, DateEnv, DateMarker, addDays } from 'fullcalendar'
 
 
 export default class GcalEventSource extends EventSource {
@@ -30,9 +30,9 @@ export default class GcalEventSource extends EventSource {
   }
 
 
-  fetch(start, end, timezone, onSuccess, onFailure) {
+  fetch(start: DateMarker, end: DateMarker, dateEnv: DateEnv, onSuccess, onFailure) {
     let url = this.buildUrl()
-    let requestParams = this.buildRequestParams(start, end, timezone)
+    let requestParams = this.buildRequestParams(start, end, dateEnv)
     let ajaxSettings = this.ajaxSettings || {}
 
     if (!requestParams) { // could have failed
@@ -108,7 +108,7 @@ export default class GcalEventSource extends EventSource {
   }
 
 
-  buildRequestParams(start, end, timezone) {
+  buildRequestParams(start: DateMarker, end: DateMarker, dateEnv: DateEnv) {
     let apiKey = this.googleCalendarApiKey || this.calendar.opt('googleCalendarApiKey')
     let params
 
@@ -117,31 +117,27 @@ export default class GcalEventSource extends EventSource {
       return null
     }
 
-    // The API expects an ISO8601 datetime with a time and timezone part.
-    // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each
-    // side, guaranteeing we will receive all events in the desired range, albeit a superset.
-    // .utc() will set a zone and give it a 00:00:00 time.
-    if (!start.hasZone()) {
-      start = start.clone().utc().add(-1, 'day')
-    }
-    if (!end.hasZone()) {
-      end = end.clone().utc().add(1, 'day')
+    // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day
+    // from the UTC day-start to guarantee we're getting all the events
+    if (!dateEnv.canComputeTimeZoneOffset()) {
+      start = addDays(start, -1)
+      end = addDays(end, 1)
     }
 
     params = assignTo(
       this.ajaxSettings.data || {},
       {
         key: apiKey,
-        timeMin: start.format(),
-        timeMax: end.format(),
+        timeMin: dateEnv.toIso(start),
+        timeMax: dateEnv.toIso(end),
         singleEvents: true,
         maxResults: 9999
       }
     )
 
-    if (timezone && timezone !== 'local') {
-      // when sending timezone names to Google, only accepts underscores, not spaces
-      params.timeZone = timezone.replace(' ', '_')
+    // when sending timezone names to Google, only accepts underscores, not spaces
+    if (dateEnv.timeZone !== 'local') {
+      params.timeZone = dateEnv.timeZone.replace(' ', '_')
     }
 
     return params

+ 72 - 228
src/Calendar.ts

@@ -1,9 +1,8 @@
-import * as moment from 'moment'
 import { createElement, removeElement, applyStyle, prependToElement, forceClassName } from './util/dom-manip'
 import { computeHeightAndMargins } from './util/dom-geom'
 import { listenBySelector } from './util/dom-event'
 import { capitaliseFirstLetter, debounce } from './util/misc'
-import { globalDefaults, englishDefaults, rtlDefaults } from './options'
+import { globalDefaults, rtlDefaults } from './options'
 import Iterator from './common/Iterator'
 import GlobalEmitter from './common/GlobalEmitter'
 import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
@@ -14,8 +13,6 @@ import ViewSpecManager from './ViewSpecManager'
 import View from './View'
 import Theme from './theme/Theme'
 import Constraints from './Constraints'
-import { getMomentLocaleData } from './locale'
-import momentExt from './moment-ext'
 import UnzonedRange from './models/UnzonedRange'
 import ComponentFootprint from './models/ComponentFootprint'
 import EventDateProfile from './models/event/EventDateProfile'
@@ -27,14 +24,15 @@ import SingleEventDef from './models/event/SingleEventDef'
 import EventDefMutation from './models/event/EventDefMutation'
 import EventSource from './models/event-source/EventSource'
 import { getThemeSystemClass } from './theme/ThemeRegistry'
-import { RangeInput, MomentInput, OptionsInput, EventObjectInput, EventSourceInput } from './types/input-types'
-
+import { RangeInput, OptionsInput, EventObjectInput, EventSourceInput } from './types/input-types'
+import { DateEnv, DateInput } from './datelib/env'
+import { DateMarker } from './datelib/util'
+import { Duration, createDuration } from './datelib/duration'
 
 export default class Calendar {
 
   // not for internal use. use options module directly instead.
   static defaults: any = globalDefaults
-  static englishDefaults: any = englishDefaults
   static rtlDefaults: any = rtlDefaults
 
   // global handler registry
@@ -53,7 +51,7 @@ export default class Calendar {
 
   view: View // current View object
   viewsByType: { [viewName: string]: View } // holds all instantiated view instances, current or not
-  currentDate: moment.Moment // unzoned moment. private (public API should use getDate instead)
+  currentDate: DateMarker // private (public API should use getDate instead)
   theme: Theme
   eventManager: EventManager
   constraints: Constraints
@@ -62,9 +60,9 @@ export default class Calendar {
   businessHourGenerator: BusinessHourGenerator
   loadingLevel: number = 0 // number of simultaneous loading tasks
 
-  defaultAllDayEventDuration: moment.Duration
-  defaultTimedEventDuration: moment.Duration
-  localeData: object
+  defaultAllDayEventDuration: Duration
+  defaultTimedEventDuration: Duration
+  dateEnv: DateEnv
 
   el: HTMLElement
   contentEl: HTMLElement
@@ -90,7 +88,7 @@ export default class Calendar {
 
     this.optionsManager = new OptionsManager(this, overrides)
     this.viewSpecManager = new ViewSpecManager(this.optionsManager, this)
-    this.initMomentInternals() // needs to happen after options hash initialized
+    this.initDateEnv() // needs to happen after options hash initialized
     this.initCurrentDate()
     this.initEventManager()
     this.constraints = new Constraints(this.eventManager, this)
@@ -193,7 +191,7 @@ export default class Calendar {
   }
 
 
-  changeView(viewName: string, dateOrRange: RangeInput | MomentInput) {
+  changeView(viewName: string, dateOrRange: RangeInput | DateInput) {
 
     if (dateOrRange) {
       if ((dateOrRange as RangeInput).start && (dateOrRange as RangeInput).end) { // a range
@@ -201,7 +199,7 @@ export default class Calendar {
           visibleRange: dateOrRange
         })
       } else { // a date
-        this.currentDate = this.moment(dateOrRange).stripZone() // just like gotoDate
+        this.currentDate = this.dateEnv.createMarker(dateOrRange as DateInput) // just like gotoDate
       }
     }
 
@@ -211,14 +209,15 @@ export default class Calendar {
 
   // Forces navigation to a view for the given date.
   // `viewType` can be a specific view name or a generic one like "week" or "day".
-  zoomTo(newDate: moment.Moment, viewType?: string) {
+  // needs to change
+  zoomTo(newDate: DateMarker, viewType?: string) {
     let spec
 
     viewType = viewType || 'day' // day is default zoom
     spec = this.viewSpecManager.getViewSpec(viewType) ||
       this.viewSpecManager.getUnitViewSpec(viewType)
 
-    this.currentDate = newDate.clone()
+    this.currentDate = newDate
     this.renderView(spec ? spec.type : null)
   }
 
@@ -232,7 +231,7 @@ export default class Calendar {
 
     // compute the initial ambig-timezone date
     if (defaultDateInput != null) {
-      this.currentDate = this.moment(defaultDateInput).stripZone()
+      this.currentDate = this.dateEnv.createMarker(defaultDateInput)
     } else {
       this.currentDate = this.getNow() // getNow already returns unzoned
     }
@@ -262,13 +261,13 @@ export default class Calendar {
 
 
   prevYear() {
-    this.currentDate.add(-1, 'years')
+    this.currentDate = this.dateEnv.addYears(this.currentDate, -1)
     this.renderView()
   }
 
 
   nextYear() {
-    this.currentDate.add(1, 'years')
+    this.currentDate = this.dateEnv.addYears(this.currentDate, 1)
     this.renderView()
   }
 
@@ -280,20 +279,20 @@ export default class Calendar {
 
 
   gotoDate(zonedDateInput) {
-    this.currentDate = this.moment(zonedDateInput).stripZone()
+    this.currentDate = this.dateEnv.createMarker(zonedDateInput)
     this.renderView()
   }
 
 
-  incrementDate(delta) {
-    this.currentDate.add(moment.duration(delta))
+  incrementDate(delta) { // is public facing
+    this.currentDate = this.dateEnv.add(this.currentDate, createDuration(delta))
     this.renderView()
   }
 
 
   // for external API
-  getDate(): moment.Moment {
-    return this.applyTimezone(this.currentDate) // infuse the calendar's timezone
+  getDate(): Date {
+    return this.dateEnv.toDate(this.currentDate)
   }
 
 
@@ -341,7 +340,7 @@ export default class Calendar {
       let gotoOptions: any = anchorEl.getAttribute('data-goto')
       gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {}
 
-      let date = this.moment(gotoOptions.date)
+      let date = this.dateEnv.createMarker(gotoOptions.date)
       let viewType = gotoOptions.type
 
       // property like "navLinkDayClick". might be a string or a function
@@ -802,7 +801,7 @@ export default class Calendar {
 
 
   // this public method receives start/end dates in any format, with any timezone
-  select(zonedStartInput: MomentInput, zonedEndInput?: MomentInput) {
+  select(zonedStartInput: DateInput, zonedEndInput?: DateInput) {
     this.view.select(
       this.buildSelectFootprint.apply(this, arguments)
     )
@@ -817,21 +816,22 @@ export default class Calendar {
 
 
   // Given arguments to the select method in the API, returns a span (unzoned start/end and other info)
-  buildSelectFootprint(zonedStartInput: MomentInput, zonedEndInput?: MomentInput): ComponentFootprint {
-    let start = this.moment(zonedStartInput).stripZone()
+  buildSelectFootprint(zonedStartInput: DateInput, zonedEndInput?: DateInput): ComponentFootprint {
+    let startMeta = this.dateEnv.createMarkerMeta(zonedStartInput)
+    let start = startMeta.marker
     let end
 
     if (zonedEndInput) {
-      end = this.moment(zonedEndInput).stripZone()
-    } else if (start.hasTime()) {
-      end = start.clone().add(this.defaultTimedEventDuration)
+      end = this.dateEnv.createMarker(zonedEndInput)
+    } else if (startMeta.isTimeUnspecified) {
+      end = this.dateEnv.add(start, this.defaultAllDayEventDuration)
     } else {
-      end = start.clone().add(this.defaultAllDayEventDuration)
+      end = this.dateEnv.add(start, this.defaultTimedEventDuration)
     }
 
     return new ComponentFootprint(
       new UnzonedRange(start, end),
-      !start.hasTime()
+      startMeta.isTimeUnspecified
     )
   }
 
@@ -865,202 +865,58 @@ export default class Calendar {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  initMomentInternals() {
+  initDateEnv() {
 
-    this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration'))
-    this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration'))
+    // not really date-env
+    this.defaultAllDayEventDuration = createDuration(this.opt('defaultAllDayEventDuration'))
+    this.defaultTimedEventDuration = createDuration(this.opt('defaultTimedEventDuration'))
 
-    // Called immediately, and when any of the options change.
-    // Happens before any internal objects rebuild or rerender, because this is very core.
-    this.optionsManager.watch('buildingMomentLocale', [
-      '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort',
+    this.optionsManager.watch('buildDateEnv', [
+      '?locale', '?timezone',
       '?firstDay', '?weekNumberCalculation'
     ], (opts) => {
-      let weekNumberCalculation = opts.weekNumberCalculation
-      let firstDay = opts.firstDay
-      let _week
-
-      // normalize
-      if (weekNumberCalculation === 'iso') {
-        weekNumberCalculation = 'ISO' // normalize
-      }
-
-      let localeData = Object.create( // make a cheap copy
-        getMomentLocaleData(opts.locale) // will fall back to en
-      )
-
-      if (opts.monthNames) {
-        localeData._months = opts.monthNames
-      }
-      if (opts.monthNamesShort) {
-        localeData._monthsShort = opts.monthNamesShort
-      }
-      if (opts.dayNames) {
-        localeData._weekdays = opts.dayNames
-      }
-      if (opts.dayNamesShort) {
-        localeData._weekdaysShort = opts.dayNamesShort
-      }
-
-      if (firstDay == null && weekNumberCalculation === 'ISO') {
-        firstDay = 1
-      }
-      if (firstDay != null) {
-        _week = Object.create(localeData._week) // _week: { dow: # }
-        _week.dow = firstDay
-        localeData._week = _week
-      }
-
-      if ( // whitelist certain kinds of input
-        weekNumberCalculation === 'ISO' ||
-        weekNumberCalculation === 'local' ||
-        typeof weekNumberCalculation === 'function'
-      ) {
-        localeData._fullCalendar_weekCalc = weekNumberCalculation // moment-ext will know what to do with it
-      }
-
-      this.localeData = localeData
-
-      // If the internal current date object already exists, move to new locale.
-      // We do NOT need to do this technique for event dates, because this happens when converting to "segments".
-      if (this.currentDate) {
-        this.localizeMoment(this.currentDate) // sets to localeData
-      }
+      this.dateEnv = new DateEnv({
+        calendarSystem: 'gregorian',
+        timeZone: opts.timezone,
+        locale: opts.locale,
+        weekNumberCalculation: opts.weekNumberCalculation,
+        firstDay: opts.firstDay
+      })
     })
   }
 
 
-  // Builds a moment using the settings of the current calendar: timezone and locale.
-  // Accepts anything the vanilla moment() constructor accepts.
-  moment(...args): moment.Moment {
-    let mom
-
-    if (this.opt('timezone') === 'local') {
-      mom = momentExt.apply(null, args)
-
-      // Force the moment to be local, because momentExt doesn't guarantee it.
-      if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
-        mom.local()
-      }
-    } else if (this.opt('timezone') === 'UTC') {
-      mom = momentExt.utc.apply(null, args) // process as UTC
-    } else {
-      mom = momentExt.parseZone.apply(null, args) // let the input decide the zone
-    }
-
-    this.localizeMoment(mom) // TODO
-
-    return mom
-  }
-
-
-  msToMoment(ms: number, forceAllDay: boolean): moment.Moment {
-    let mom = momentExt.utc(ms) // TODO: optimize by using Date.UTC
-
-    if (forceAllDay) {
-      mom.stripTime()
-    } else {
-      mom = this.applyTimezone(mom) // may or may not apply locale
-    }
-
-    this.localizeMoment(mom)
-
-    return mom
-  }
-
-
-  msToUtcMoment(ms: number, forceAllDay: boolean): moment.Moment {
-    let mom = momentExt.utc(ms) // TODO: optimize by using Date.UTC
-
-    if (forceAllDay) {
-      mom.stripTime()
-    }
-
-    this.localizeMoment(mom)
-
-    return mom
-  }
-
-
-  // Updates the given moment's locale settings to the current calendar locale settings.
-  localizeMoment(mom) {
-    mom._locale = this.localeData
-  }
-
-
-  // Returns a boolean about whether or not the calendar knows how to calculate
-  // the timezone offset of arbitrary dates in the current timezone.
-  getIsAmbigTimezone(): boolean {
-    return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC'
-  }
-
-
-  // Returns a copy of the given date in the current timezone. Has no effect on dates without times.
-  applyTimezone(date: moment.Moment): moment.Moment {
-    if (!date.hasTime()) {
-      return date.clone()
-    }
-
-    let zonedDate = this.moment(date.toArray())
-    let timeAdjust = date.time().asMilliseconds() - zonedDate.time().asMilliseconds()
-    let adjustedZonedDate
-
-    // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396)
-    if (timeAdjust) { // is the time result different than expected?
-      adjustedZonedDate = zonedDate.clone().add(timeAdjust) // add milliseconds
-      if (date.time().asMilliseconds() - adjustedZonedDate.time().asMilliseconds() === 0) { // does it match perfectly now?
-        zonedDate = adjustedZonedDate
-      }
-    }
-
-    return zonedDate
-  }
-
-
   /*
   Assumes the footprint is non-open-ended.
   */
   footprintToDateProfile(componentFootprint, ignoreEnd = false) {
-    let start = momentExt.utc(componentFootprint.unzonedRange.startMs)
-    let end
+    const dateEnv = this.dateEnv
+    let startMarker = componentFootprint.unzonedRange.start
+    let endMarker
 
     if (!ignoreEnd) {
-      end = momentExt.utc(componentFootprint.unzonedRange.endMs)
+      endMarker = componentFootprint.unzonedRange.end
     }
 
     if (componentFootprint.isAllDay) {
-      start.stripTime()
+      startMarker = dateEnv.startOfDay(startMarker)
 
-      if (end) {
-        end.stripTime()
-      }
-    } else {
-      start = this.applyTimezone(start)
-
-      if (end) {
-        end = this.applyTimezone(end)
+      if (endMarker) {
+        endMarker = dateEnv.startOfDay(endMarker)
       }
     }
 
-    return new EventDateProfile(start, end, this)
+    return new EventDateProfile(startMarker, endMarker, componentFootprint.isAllDay, this)
   }
 
 
-  // Returns a moment for the current date, as defined by the client's computer or from the `now` option.
-  // Will return an moment with an ambiguous timezone.
-  getNow(): moment.Moment {
+  // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
+  getNow(): DateMarker {
     let now = this.opt('now')
     if (typeof now === 'function') {
       now = now()
     }
-    return this.moment(now).stripZone()
-  }
-
-
-  // Produces a human-readable string for the given duration.
-  // Side-effect: changes the locale of the given duration.
-  humanizeDuration(duration: moment.Duration): string {
-    return duration.locale(this.opt('locale')).humanize()
+    return this.dateEnv.createMarker(now)
   }
 
 
@@ -1070,11 +926,11 @@ export default class Calendar {
     let end = null
 
     if (rangeInput.start) {
-      start = this.moment(rangeInput.start).stripZone()
+      start = this.dateEnv.createMarker(rangeInput.start)
     }
 
     if (rangeInput.end) {
-      end = this.moment(rangeInput.end).stripZone()
+      end = this.dateEnv.createMarker(rangeInput.end)
     }
 
     if (!start && !end) {
@@ -1122,40 +978,27 @@ export default class Calendar {
   }
 
 
-  requestEvents(start: moment.Moment, end: moment.Moment, callback) {
+  requestEvents(start: DateMarker, end: DateMarker, callback) {
     return this.eventManager.requestEvents(
       start,
       end,
-      this.opt('timezone'),
+      this.dateEnv,
       !this.opt('lazyFetching'),
       callback
     )
   }
 
 
-  // Get an event's normalized end date. If not present, calculate it from the defaults.
-  getEventEnd(event): moment.Moment {
-    if (event.end) {
-      return event.end.clone()
-    } else {
-      return this.getDefaultEventEnd(event.allDay, event.start)
-    }
-  }
-
-
   // Given an event's allDay status and start date, return what its fallback end date should be.
   // TODO: rename to computeDefaultEventEnd
-  getDefaultEventEnd(allDay: boolean, zonedStart: moment.Moment) {
-    let end = zonedStart.clone()
+  getDefaultEventEnd(allDay: boolean, marker: DateMarker): DateMarker {
+    let end = marker
 
     if (allDay) {
-      end.stripTime().add(this.defaultAllDayEventDuration)
+      end = this.dateEnv.startOfDay(end)
+      end = this.dateEnv.add(end, this.defaultAllDayEventDuration)
     } else {
-      end.add(this.defaultTimedEventDuration)
-    }
-
-    if (this.getIsAmbigTimezone()) {
-      end.stripZone() // we don't know what the tzo should be
+      end = this.dateEnv.add(end, this.defaultTimedEventDuration)
     }
 
     return end
@@ -1211,8 +1054,8 @@ export default class Calendar {
     if (legacyQuery == null) { // shortcut for removing all
       eventManager.removeAllEventDefs() // persist=true
     } else {
-      eventManager.getEventInstances().forEach(function(eventInstance) {
-        legacyInstances.push(eventInstance.toLegacy())
+      eventManager.getEventInstances().forEach((eventInstance) => {
+        legacyInstances.push(eventInstance.toLegacy(this))
       })
 
       legacyInstances = filterLegacyEventInstances(legacyInstances, legacyQuery)
@@ -1238,8 +1081,8 @@ export default class Calendar {
   clientEvents(legacyQuery) {
     let legacyEventInstances = []
 
-    this.eventManager.getEventInstances().forEach(function(eventInstance) {
-      legacyEventInstances.push(eventInstance.toLegacy())
+    this.eventManager.getEventInstances().forEach((eventInstance) => {
+      legacyEventInstances.push(eventInstance.toLegacy(this))
     })
 
     return filterLegacyEventInstances(legacyEventInstances, legacyQuery)
@@ -1268,7 +1111,8 @@ export default class Calendar {
       eventDefMutation = EventDefMutation.createFromRawProps(
         eventInstance,
         eventProps, // raw props
-        null // largeUnit -- who uses it?
+        null, // largeUnit -- who uses it?
+        this
       )
 
       this.eventManager.mutateEventsWithId(eventDef.id, eventDefMutation) // will release

+ 14 - 21
src/Constraints.ts

@@ -64,7 +64,7 @@ export default class Constraints {
         if (
           eventAllowFunc(
             eventFootprints[i].componentFootprint.toLegacy(this._calendar),
-            eventFootprints[i].getEventLegacy()
+            eventFootprints[i].getEventLegacy(this._calendar)
           ) === false
         ) {
           return false
@@ -140,7 +140,7 @@ export default class Constraints {
     }
 
     if (subjectEventInstance) {
-      if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance)) {
+      if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance, this._calendar)) {
         return false
       }
     }
@@ -283,29 +283,22 @@ export default class Constraints {
   Very similar to EventDateProfile::parse :(
   */
   parseFootprints(rawInput) {
-    let start
-    let end
+    const dateEnv = this._calendar.dateEnv
+    let startMeta
+    let endMeta
 
     if (rawInput.start) {
-      start = this._calendar.moment(rawInput.start)
-
-      if (!start.isValid()) {
-        start = null
-      }
+      startMeta = dateEnv.createMarkerMeta(rawInput.start)
     }
 
     if (rawInput.end) {
-      end = this._calendar.moment(rawInput.end)
-
-      if (!end.isValid()) {
-        end = null
-      }
+      endMeta = dateEnv.createMarkerMeta(rawInput.end)
     }
 
     return [
       new ComponentFootprint(
-        new UnzonedRange(start, end),
-        (start && !start.hasTime()) || (end && !end.hasTime()) // isAllDay
+        new UnzonedRange(startMeta ? startMeta.marker : null, endMeta ? endMeta.marker : null),
+        (startMeta && startMeta.isTimeUnspecified || (endMeta && endMeta.isTimeUnspecified)) // isAllDay
       )
     ]
   }
@@ -334,8 +327,8 @@ function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEve
   for (i = 0; i < overlapEventFootprints.length; i++) {
     if (
       !overlapFunc(
-        overlapEventFootprints[i].eventInstance.toLegacy(),
-        subjectEventInstance ? subjectEventInstance.toLegacy() : null
+        overlapEventFootprints[i].eventInstance.toLegacy(this._calendar),
+        subjectEventInstance ? subjectEventInstance.toLegacy(this._calendar) : null
       )
     ) {
       return false
@@ -346,8 +339,8 @@ function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEve
 }
 
 
-function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance) {
-  let subjectLegacyInstance = subjectEventInstance.toLegacy()
+function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance, calendar) {
+  let subjectLegacyInstance = subjectEventInstance.toLegacy(calendar)
   let i
   let overlapEventInstance
   let overlapEventDef
@@ -366,7 +359,7 @@ function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInst
     } else if (typeof overlapVal === 'function') {
       if (
         !overlapVal(
-          overlapEventInstance.toLegacy(),
+          overlapEventInstance.toLegacy(calendar),
           subjectLegacyInstance
         )
       ) {

+ 73 - 72
src/DateProfileGenerator.ts

@@ -1,6 +1,7 @@
-import * as moment from 'moment'
-import { computeGreatestUnit, computeDurationGreatestUnit } from './util/date'
+import View from './View'
 import UnzonedRange from './models/UnzonedRange'
+import { Duration, createDuration, getWeeksFromInput, asRoughDays, computeGreatestUnit } from './datelib/duration'
+import { computeGreatestDurationDenominator, DateMarker, startOfDay, addDays } from './datelib/util'
 
 
 export interface DateProfile {
@@ -10,17 +11,17 @@ export interface DateProfile {
   isRangeAllDay: boolean
   activeUnzonedRange: UnzonedRange
   renderUnzonedRange: UnzonedRange
-  minTime: moment.Duration
-  maxTime: moment.Duration
+  minTime: Duration
+  maxTime: Duration
   isValid: boolean
-  date: moment.Moment
-  dateIncrement: moment.Duration
+  date: DateMarker
+  dateIncrement: Duration
 }
 
 
 export default class DateProfileGenerator {
 
-  _view: any // discourage
+  _view: View // discourage
 
 
   constructor(_view) {
@@ -38,20 +39,18 @@ export default class DateProfileGenerator {
   }
 
 
-  msToUtcMoment(ms, forceAllDay) {
-    return this._view.calendar.msToUtcMoment(ms, forceAllDay)
-  }
-
-
   /* Date Range Computation
   ------------------------------------------------------------------------------------------------------------------*/
 
 
   // Builds a structure with info about what the dates/ranges will be for the "prev" view.
   buildPrev(currentDateProfile): DateProfile {
-    let prevDate = currentDateProfile.date.clone()
-      .startOf(currentDateProfile.currentRangeUnit)
-      .subtract(currentDateProfile.dateIncrement)
+    const dateEnv = this._view.calendar.dateEnv
+
+    let prevDate = dateEnv.subtract(
+      dateEnv.startOf(currentDateProfile.date, currentDateProfile.currentRangeUnit),
+      currentDateProfile.dateIncrement
+    )
 
     return this.build(prevDate, -1)
   }
@@ -59,9 +58,12 @@ export default class DateProfileGenerator {
 
   // Builds a structure with info about what the dates/ranges will be for the "next" view.
   buildNext(currentDateProfile): DateProfile {
-    let nextDate = currentDateProfile.date.clone()
-      .startOf(currentDateProfile.currentRangeUnit)
-      .add(currentDateProfile.dateIncrement)
+    const dateEnv = this._view.calendar.dateEnv
+
+    let nextDate = dateEnv.add(
+      dateEnv.startOf(currentDateProfile.date, currentDateProfile.currentRangeUnit),
+      currentDateProfile.dateIncrement
+    )
 
     return this.build(nextDate, 1)
   }
@@ -70,8 +72,7 @@ export default class DateProfileGenerator {
   // Builds a structure holding dates/ranges for rendering around the given date.
   // Optional direction param indicates whether the date is being incremented/decremented
   // from its previous value. decremented = -1, incremented = 1 (default).
-  build(date, direction?, forceToValid = false): DateProfile {
-    let isDateAllDay = !date.hasTime()
+  build(date: DateMarker, direction?, forceToValid = false): DateProfile {
     let validUnzonedRange
     let minTime = null
     let maxTime = null
@@ -85,10 +86,7 @@ export default class DateProfileGenerator {
     validUnzonedRange = this.trimHiddenDays(validUnzonedRange)
 
     if (forceToValid) {
-      date = this.msToUtcMoment(
-        validUnzonedRange.constrainDate(date), // returns MS
-        isDateAllDay
-      )
+      date = validUnzonedRange.constrainDate(date)
     }
 
     currentInfo = this.buildCurrentRangeInfo(date, direction)
@@ -105,16 +103,13 @@ export default class DateProfileGenerator {
       activeUnzonedRange = activeUnzonedRange.intersect(currentInfo.unzonedRange)
     }
 
-    minTime = moment.duration(this.opt('minTime'))
-    maxTime = moment.duration(this.opt('maxTime'))
+    minTime = createDuration(this.opt('minTime'))
+    maxTime = createDuration(this.opt('maxTime'))
     activeUnzonedRange = this.adjustActiveRange(activeUnzonedRange, minTime, maxTime)
     activeUnzonedRange = activeUnzonedRange.intersect(validUnzonedRange) // might return null
 
     if (activeUnzonedRange) {
-      date = this.msToUtcMoment(
-        activeUnzonedRange.constrainDate(date), // returns MS
-        isDateAllDay
-      )
+      date = activeUnzonedRange.constrainDate(date)
     }
 
     // it's invalid if the originally requested date is not contained,
@@ -173,8 +168,8 @@ export default class DateProfileGenerator {
   // highlighted as being the current month for example.
   // See build() for a description of `direction`.
   // Guaranteed to have `range` and `unit` properties. `duration` is optional.
-  // TODO: accept a MS-time instead of a moment `date`?
-  buildCurrentRangeInfo(date, direction) {
+  buildCurrentRangeInfo(date: DateMarker, direction) {
+    const dateEnv = this._view.calendar.dateEnv
     let viewSpec = this._view.viewSpec
     let duration = null
     let unit = null
@@ -189,7 +184,7 @@ export default class DateProfileGenerator {
       unit = 'day'
       unzonedRange = this.buildRangeFromDayCount(date, direction, dayCount)
     } else if ((unzonedRange = this.buildCustomVisibleRange(date))) {
-      unit = computeGreatestUnit(unzonedRange.getStart(), unzonedRange.getEnd())
+      unit = dateEnv.computeGreatestDenominator(unzonedRange.start, unzonedRange.end).unit
     } else {
       duration = this.getFallbackDuration()
       unit = computeGreatestUnit(duration)
@@ -200,25 +195,31 @@ export default class DateProfileGenerator {
   }
 
 
-  getFallbackDuration() {
-    return moment.duration({ days: 1 })
+  getFallbackDuration(): Duration {
+    return createDuration({ day: 1 })
   }
 
 
   // Returns a new activeUnzonedRange to have time values (un-ambiguate)
   // minTime or maxTime causes the range to expand.
-  adjustActiveRange(unzonedRange, minTime, maxTime) {
-    let start = unzonedRange.getStart()
-    let end = unzonedRange.getEnd()
+  adjustActiveRange(unzonedRange: UnzonedRange, minTime: Duration, maxTime: Duration) {
+    const dateEnv = this._view.calendar.dateEnv
+    let start = unzonedRange.start
+    let end = unzonedRange.end
 
     if (this._view.usesMinMaxTime) {
 
-      if (minTime < 0) {
-        start.time(0).add(minTime)
+      // expand active range if minTime is negative (why not when positive?)
+      if (asRoughDays(minTime) < 0) {
+        start = dateEnv.startOfDay(start) // necessary?
+        start = dateEnv.add(start, minTime)
       }
 
-      if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours?
-        end.time(maxTime - (24 * 60 * 60 * 1000))
+      // expand active range if maxTime is beyond one day (why not when positive?)
+      if (asRoughDays(maxTime) > 1) {
+        end = dateEnv.startOfDay(end) // necessary?
+        end = addDays(end, -1)
+        end = dateEnv.add(end, maxTime)
       }
     }
 
@@ -228,13 +229,13 @@ export default class DateProfileGenerator {
 
   // Builds the "current" range when it is specified as an explicit duration.
   // `unit` is the already-computed computeGreatestUnit value of duration.
-  // TODO: accept a MS-time instead of a moment `date`?
-  buildRangeFromDuration(date, direction, duration, unit) {
+  buildRangeFromDuration(date: DateMarker, direction, duration: Duration, unit) {
+    const dateEnv = this._view.calendar.dateEnv
     let alignment = this.opt('dateAlignment')
     let dateIncrementInput
     let dateIncrementDuration
-    let start
-    let end
+    let start: DateMarker
+    let end: DateMarker
     let res
 
     // compute what the alignment should be
@@ -242,11 +243,14 @@ export default class DateProfileGenerator {
       dateIncrementInput = this.opt('dateIncrement')
 
       if (dateIncrementInput) {
-        dateIncrementDuration = moment.duration(dateIncrementInput)
+        dateIncrementDuration = createDuration(dateIncrementInput)
 
         // use the smaller of the two units
         if (dateIncrementDuration < duration) {
-          alignment = computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput)
+          alignment = computeGreatestDurationDenominator(
+            dateIncrementDuration,
+            Boolean(getWeeksFromInput(dateIncrementInput))
+          ).unit
         } else {
           alignment = unit
         }
@@ -256,16 +260,16 @@ export default class DateProfileGenerator {
     }
 
     // if the view displays a single day or smaller
-    if (duration.as('days') <= 1) {
+    if (asRoughDays(duration) <= 1) {
       if (this._view.isHiddenDay(start)) {
         start = this._view.skipHiddenDays(start, direction)
-        start.startOf('day')
+        start = startOfDay(start)
       }
     }
 
     function computeRes() {
-      start = date.clone().startOf(alignment)
-      end = start.clone().add(duration)
+      start = dateEnv.startOf(date, alignment)
+      end = dateEnv.add(date, duration)
       res = new UnzonedRange(start, end)
     }
 
@@ -282,23 +286,23 @@ export default class DateProfileGenerator {
 
 
   // Builds the "current" range when a dayCount is specified.
-  // TODO: accept a MS-time instead of a moment `date`?
-  buildRangeFromDayCount(date, direction, dayCount) {
+  buildRangeFromDayCount(date: DateMarker, direction, dayCount) {
+    const dateEnv = this._view.calendar.dateEnv
     let customAlignment = this.opt('dateAlignment')
     let runningCount = 0
-    let start = date.clone()
-    let end
+    let start: DateMarker = date
+    let end: DateMarker
 
     if (customAlignment) {
-      start.startOf(customAlignment)
+      start = dateEnv.startOf(start, customAlignment)
     }
 
-    start.startOf('day')
+    start = startOfDay(start)
     start = this._view.skipHiddenDays(start, direction)
 
-    end = start.clone()
+    end = start
     do {
-      end.add(1, 'day')
+      end = addDays(end, 1)
       if (!this._view.isHiddenDay(end)) {
         runningCount++
       }
@@ -310,14 +314,11 @@ export default class DateProfileGenerator {
 
   // Builds a normalized range object for the "visible" range,
   // which is a way to define the currentUnzonedRange and activeUnzonedRange at the same time.
-  // TODO: accept a MS-time instead of a moment `date`?
-  buildCustomVisibleRange(date) {
-    let visibleUnzonedRange = this._view.getUnzonedRangeOption(
-      'visibleRange',
-      this._view.calendar.applyTimezone(date) // correct zone. also generates new obj that avoids mutations
-    )
+  buildCustomVisibleRange(date: DateMarker) {
+    const dateEnv = this._view.calendar.dateEnv
+    let visibleUnzonedRange = this._view.getUnzonedRangeOption('visibleRange', dateEnv.toDate(date))
 
-    if (visibleUnzonedRange && (visibleUnzonedRange.startMs == null || visibleUnzonedRange.endMs == null)) {
+    if (visibleUnzonedRange && (visibleUnzonedRange.start == null || visibleUnzonedRange.end == null)) {
       return null
     }
 
@@ -328,25 +329,25 @@ export default class DateProfileGenerator {
   // Computes the range that will represent the element/cells for *rendering*,
   // but which may have voided days/times.
   // not responsible for trimming hidden days.
-  buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
+  buildRenderRange(currentUnzonedRange: UnzonedRange, currentRangeUnit, isRangeAllDay) {
     return currentUnzonedRange.clone()
   }
 
 
   // Compute the duration value that should be added/substracted to the current date
   // when a prev/next operation happens.
-  buildDateIncrement(fallback) {
+  buildDateIncrement(fallback): Duration {
     let dateIncrementInput = this.opt('dateIncrement')
     let customAlignment
 
     if (dateIncrementInput) {
-      return moment.duration(dateIncrementInput)
+      return createDuration(dateIncrementInput)
     } else if ((customAlignment = this.opt('dateAlignment'))) {
-      return moment.duration(1, customAlignment)
+      return createDuration(1, customAlignment)
     } else if (fallback) {
       return fallback
     } else {
-      return moment.duration({ days: 1 })
+      return createDuration({ days: 1 })
     }
   }
 

+ 1 - 2
src/OptionsManager.ts

@@ -1,7 +1,7 @@
 import { assignTo } from './util/object'
 import { firstDefined } from './util/misc'
 import { globalDefaults, rtlDefaults, mergeOptions } from './options'
-import { localeOptionHash, populateInstanceComputableOptions } from './locale'
+import { localeOptionHash } from './locale'
 import Model from './common/Model'
 
 
@@ -100,7 +100,6 @@ export default class OptionsManager extends Model {
       this.overrides,
       this.dynamicOverrides
     ])
-    populateInstanceComputableOptions(rawOptions) // fill in gaps with computed options
 
     this.reset(rawOptions)
   }

+ 88 - 58
src/View.ts

@@ -1,4 +1,3 @@
-import * as moment from 'moment'
 import { assignTo } from './util/object'
 import { elementClosest } from './util/dom-manip'
 import { isPrimaryMouseButton } from './util/dom-event'
@@ -10,6 +9,10 @@ import InteractiveDateComponent from './component/InteractiveDateComponent'
 import GlobalEmitter from './common/GlobalEmitter'
 import UnzonedRange from './models/UnzonedRange'
 import EventInstance from './models/event/EventInstance'
+import { DateMarker, addDays, addMs } from './datelib/util'
+import { createDuration } from './datelib/duration'
+import { createFormatter } from './datelib/formatting'
+import { diffDays } from './datelib/env'
 
 
 /* An abstract class from which other views inherit from
@@ -39,7 +42,7 @@ export default abstract class View extends InteractiveDateComponent {
 
   // now indicator
   isNowIndicatorRendered: boolean
-  initialNowDate: moment.Moment // result first getNow call
+  initialNowDate: DateMarker // result first getNow call
   initialNowQueriedMs: number // ms time the getNow was called
   nowIndicatorTimeoutID: any // for refresh timing of now indicator
   nowIndicatorIntervalID: any // "
@@ -52,10 +55,10 @@ export default abstract class View extends InteractiveDateComponent {
   usesMinMaxTime: boolean
 
   // DEPRECATED
-  start: moment.Moment // use activeUnzonedRange
-  end: moment.Moment // use activeUnzonedRange
-  intervalStart: moment.Moment // use currentUnzonedRange
-  intervalEnd: moment.Moment // use currentUnzonedRange
+  start: Date // use activeUnzonedRange
+  end: Date // use activeUnzonedRange
+  intervalStart: Date // use currentUnzonedRange
+  intervalEnd: Date // use currentUnzonedRange
 
 
   constructor(calendar, viewSpec) {
@@ -169,14 +172,21 @@ export default abstract class View extends InteractiveDateComponent {
       unzonedRange = dateProfile.activeUnzonedRange
     }
 
-    return this.formatRange(
-      {
-        start: this.calendar.msToMoment(unzonedRange.startMs, dateProfile.isRangeAllDay),
-        end: this.calendar.msToMoment(unzonedRange.endMs, dateProfile.isRangeAllDay)
-      },
-      dateProfile.isRangeAllDay,
-      this.opt('titleFormat') || this.computeTitleFormat(dateProfile),
-      this.opt('titleRangeSeparator')
+    // TODO: precompute
+    // TODO: how will moment plugin deal with this?
+    let rawTitleFormat = this.opt('titleFormat') || this.computeTitleFormat(dateProfile)
+    if (typeof rawTitleFormat === 'object') {
+      rawTitleFormat = assignTo(
+        { separator: this.opt('titleRangeSeparator') },
+        rawTitleFormat
+      )
+    }
+
+    return this.calendar.dateEnv.toRangeFormat(
+      unzonedRange.start,
+      unzonedRange.end,
+      createFormatter(rawTitleFormat),
+      { isExclusive: dateProfile.isRangeAllDay }
     )
   }
 
@@ -187,13 +197,21 @@ export default abstract class View extends InteractiveDateComponent {
     let currentRangeUnit = dateProfile.currentRangeUnit
 
     if (currentRangeUnit === 'year') {
-      return 'YYYY'
+      return { year: 'numeric' }
     } else if (currentRangeUnit === 'month') {
-      return this.opt('monthYearFormat') // like "September 2014"
-    } else if (dateProfile.currentUnzonedRange.as('days') > 1) {
-      return 'll' // multi-day range. shorter, like "Sep 9 - 10 2014"
+      return { year: 'numeric', month: 'long' } // like "September 2014"
     } else {
-      return 'LL' // one day. longer, like "September 9 2014"
+      let days = diffDays(
+        dateProfile.currentUnzonedRange.start,
+        dateProfile.currentUnzonedRange.end
+      )
+      if (days !== null && days > 1) {
+        // multi-day range. shorter, like "Sep 9 - 10 2014"
+        return { year: 'numeric', month: 'short', date: 'numeric' }
+      } else {
+        // one day. longer, like "September 9 2014"
+        return { year: 'numeric', month: 'long', date: 'numeric' }
+      }
     }
   }
 
@@ -202,7 +220,7 @@ export default abstract class View extends InteractiveDateComponent {
   // -----------------------------------------------------------------------------------------------------------------
 
 
-  setDate(date: moment.Moment) {
+  setDate(date: DateMarker) {
     let currentDateProfile = this.get('dateProfile')
     let newDateProfile = this.dateProfileGenerator.build(date, undefined, true) // forceToValid=true
 
@@ -225,12 +243,9 @@ export default abstract class View extends InteractiveDateComponent {
 
 
   fetchInitialEvents(dateProfile, callback) {
-    let calendar = this.calendar
-    let forceAllDay = dateProfile.isRangeAllDay && !this.usesMinMaxTime
-
-    calendar.requestEvents(
-      calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, forceAllDay),
-      calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, forceAllDay),
+    this.calendar.requestEvents(
+      dateProfile.activeUnzonedRange.start,
+      dateProfile.activeUnzonedRange.end,
       callback
     )
   }
@@ -414,6 +429,7 @@ export default abstract class View extends InteractiveDateComponent {
   // which is defined by this.getNowIndicatorUnit().
   // TODO: somehow do this for the current whole day's background too
   startNowIndicator() {
+    const dateEnv = this.calendar.dateEnv
     let unit
     let update
     let delay // ms wait value
@@ -427,12 +443,22 @@ export default abstract class View extends InteractiveDateComponent {
         this.initialNowQueriedMs = new Date().valueOf()
 
         // wait until the beginning of the next interval
-        delay = this.initialNowDate.clone().startOf(unit).add(1, unit).valueOf() - this.initialNowDate.valueOf()
+        delay = dateEnv.add(
+          dateEnv.startOf(this.initialNowDate, unit),
+          createDuration(1, unit)
+        ).valueOf() - this.initialNowDate.valueOf()
+
+        // TODO: maybe always use setTimeout, waiting until start of next unit
         this.nowIndicatorTimeoutID = setTimeout(() => {
           this.nowIndicatorTimeoutID = null
           update()
-          delay = +moment.duration(1, unit)
-          delay = Math.max(100, delay) // prevent too frequent
+
+          if (unit === 'second') {
+            delay = 1000 * 60 // every second
+          } else {
+            delay = 1000 * 60 * 60 // otherwise, every minute
+          }
+
           this.nowIndicatorIntervalID = setInterval(update, delay) // update every interval
         }, delay)
       }
@@ -451,7 +477,7 @@ export default abstract class View extends InteractiveDateComponent {
     ) {
       this.unrenderNowIndicator() // won't unrender if unnecessary
       this.renderNowIndicator(
-        this.initialNowDate.clone().add(new Date().valueOf() - this.initialNowQueriedMs) // add ms
+        addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)
       )
       this.isNowIndicatorRendered = true
     }
@@ -578,7 +604,7 @@ export default abstract class View extends InteractiveDateComponent {
     this.triggerEventDrop(
       eventInstance,
       // a drop doesn't necessarily mean a date mutation (ex: resource change)
-      (dateMutation && dateMutation.dateDelta) || moment.duration(),
+      (dateMutation && dateMutation.dateDelta) || createDuration(0),
       undoFunc,
       el, ev
     )
@@ -590,7 +616,7 @@ export default abstract class View extends InteractiveDateComponent {
     this.publiclyTrigger('eventDrop', {
       context: el,
       args: [
-        eventInstance.toLegacy(),
+        eventInstance.toLegacy(this.calendar),
         dateDelta,
         undoFunc,
         ev,
@@ -625,7 +651,7 @@ export default abstract class View extends InteractiveDateComponent {
     this.publiclyTrigger('drop', {
       context: el,
       args: [
-        singleEventDef.dateProfile.start.clone(),
+        singleEventDef.dateProfile.start,
         ev,
         this
       ]
@@ -636,7 +662,7 @@ export default abstract class View extends InteractiveDateComponent {
       this.publiclyTrigger('eventReceive', {
         context: this,
         args: [
-          singleEventDef.buildInstance().toLegacy(),
+          singleEventDef.buildInstance().toLegacy(this.calendar),
           this
         ]
       })
@@ -676,7 +702,7 @@ export default abstract class View extends InteractiveDateComponent {
     this.publiclyTrigger('eventResize', {
       context: el,
       args: [
-        eventInstance.toLegacy(),
+        eventInstance.toLegacy(this.calendar),
         durationDelta,
         undoFunc,
         ev,
@@ -720,13 +746,14 @@ export default abstract class View extends InteractiveDateComponent {
 
   // Triggers handlers to 'select'
   triggerSelect(footprint, ev?) {
+    const dateEnv = this.calendar.dateEnv
     let dateProfile = this.calendar.footprintToDateProfile(footprint) // abuse of "Event"DateProfile?
 
     this.publiclyTrigger('select', {
       context: this,
       args: [
-        dateProfile.start,
-        dateProfile.end,
+        dateEnv.toDate(dateProfile.unzonedRange.start),
+        dateEnv.toDate(dateProfile.unzonedRange.end),
         ev,
         this
       ]
@@ -862,11 +889,16 @@ export default abstract class View extends InteractiveDateComponent {
   // Triggers handlers to 'dayClick'
   // Span has start/end of the clicked area. Only the start is useful.
   triggerDayClick(footprint, dayEl, ev) {
+    const dateEnv = this.calendar.dateEnv
     let dateProfile = this.calendar.footprintToDateProfile(footprint) // abuse of "Event"DateProfile?
 
     this.publiclyTrigger('dayClick', {
       context: dayEl,
-      args: [ dateProfile.start, ev, this ]
+      args: [
+        dateEnv.toDate(dateProfile.unzonedRange.start),
+        ev,
+        this
+      ]
     })
   }
 
@@ -876,7 +908,7 @@ export default abstract class View extends InteractiveDateComponent {
 
 
   // For DateComponent::getDayClasses
-  isDateInOtherMonth(date, dateProfile) {
+  isDateInOtherMonth(date: DateMarker, dateProfile) {
     return false
   }
 
@@ -884,14 +916,11 @@ export default abstract class View extends InteractiveDateComponent {
   // Arguments after name will be forwarded to a hypothetical function value
   // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
   // Always clone your objects if you fear mutation.
-  getUnzonedRangeOption(name) {
+  getUnzonedRangeOption(name, ...otherArgs) {
     let val = this.opt(name)
 
     if (typeof val === 'function') {
-      val = val.apply(
-        null,
-        Array.prototype.slice.call(arguments, 1)
-      )
+      val = val.apply(null, otherArgs)
     }
 
     if (val) {
@@ -934,8 +963,8 @@ export default abstract class View extends InteractiveDateComponent {
   // Remove days from the beginning and end of the range that are computed as hidden.
   // If the whole range is trimmed off, returns null
   trimHiddenDays(inputUnzonedRange) {
-    let start = inputUnzonedRange.getStart()
-    let end = inputUnzonedRange.getEnd()
+    let start = inputUnzonedRange.start
+    let end = inputUnzonedRange.end
 
     if (start) {
       start = this.skipHiddenDays(start)
@@ -948,15 +977,16 @@ export default abstract class View extends InteractiveDateComponent {
     if (start === null || end === null || start < end) {
       return new UnzonedRange(start, end)
     }
+
     return null
   }
 
 
   // Is the current day hidden?
-  // `day` is a day-of-week index (0-6), or a Moment
+  // `day` is a day-of-week index (0-6), or a Date (used for UTC)
   isHiddenDay(day) {
-    if (moment.isMoment(day)) {
-      day = day.day()
+    if (day instanceof Date) {
+      day = day.getUTCDay()
     }
     return this.isHiddenDayHash[day]
   }
@@ -967,14 +997,13 @@ export default abstract class View extends InteractiveDateComponent {
   // If the initial value of `date` is not a hidden day, don't do anything.
   // Pass `isExclusive` as `true` if you are dealing with an end date.
   // `inc` defaults to `1` (increment one day forward each time)
-  skipHiddenDays(date, inc= 1, isExclusive= false) {
-    let out = date.clone()
+  skipHiddenDays(date: DateMarker, inc = 1, isExclusive = false) {
     while (
-      this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
+      this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]
     ) {
-      out.add(inc, 'days')
+      date = addDays(date, inc)
     }
-    return out
+    return date
   }
 
 }
@@ -1025,11 +1054,12 @@ View.watch('title', [ 'dateProfile' ], function(deps) {
 
 View.watch('legacyDateProps', [ 'dateProfile' ], function(deps) {
   let calendar = this.calendar
+  let dateEnv = calendar.dateEnv
   let dateProfile = deps.dateProfile
 
   // DEPRECATED, but we need to keep it updated...
-  this.start = calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, dateProfile.isRangeAllDay)
-  this.end = calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, dateProfile.isRangeAllDay)
-  this.intervalStart = calendar.msToMoment(dateProfile.currentUnzonedRange.startMs, dateProfile.isRangeAllDay)
-  this.intervalEnd = calendar.msToMoment(dateProfile.currentUnzonedRange.endMs, dateProfile.isRangeAllDay)
+  this.start = dateEnv.toDate(dateProfile.activeUnzonedRange.start)
+  this.end = dateEnv.toDate(dateProfile.activeUnzonedRange.end)
+  this.intervalStart = dateEnv.toDate(dateProfile.currentUnzonedRange.start)
+  this.intervalEnd = dateEnv.toDate(dateProfile.currentUnzonedRange.end)
 })

+ 13 - 14
src/ViewSpecManager.ts

@@ -1,9 +1,8 @@
-import * as moment from 'moment'
 import { viewHash } from './ViewRegistry'
 import { mergeProps } from './util/object'
-import { unitsDesc, computeDurationGreatestUnit } from './util/date'
 import { mergeOptions, globalDefaults } from './options'
-import { populateInstanceComputableOptions } from './locale'
+import { Duration, createDuration, getWeeksFromInput } from './datelib/duration'
+import { computeGreatestDurationDenominator, unitsDesc } from './datelib/util'
 
 
 export default class ViewSpecManager {
@@ -71,8 +70,7 @@ export default class ViewSpecManager {
     let spec // for the view
     let overrides // for the view
     let durationInput
-    let duration
-    let unit
+    let duration: Duration
 
     // iterate from the specific view definition to a more general one until we hit an actual View class
     while (viewType) {
@@ -110,20 +108,23 @@ export default class ViewSpecManager {
       this.optionsManager.overrides.duration
 
     if (durationInput) {
-      duration = moment.duration(durationInput)
+      duration = createDuration(durationInput)
 
-      if (duration.valueOf()) { // valid?
+      if (duration) { // valid?
 
-        unit = computeDurationGreatestUnit(duration, durationInput)
+        let denom = computeGreatestDurationDenominator(
+          duration,
+          Boolean(getWeeksFromInput(durationInput))
+        )
 
         spec.duration = duration
-        spec.durationUnit = unit
+        spec.durationUnit = denom.unit
 
         // view is a single-unit duration, like "week" or "day"
         // incorporate options for this. lowest priority
-        if (duration.as(unit) === 1) {
-          spec.singleUnit = unit
-          overridesChain.unshift(viewOverrides[unit] || {})
+        if (denom.value === 1) {
+          spec.singleUnit = denom.unit
+          overridesChain.unshift(viewOverrides[denom.unit] || {})
         }
       }
     }
@@ -151,7 +152,6 @@ export default class ViewSpecManager {
       spec.overrides, // view's overrides (view-specific options)
       optionsManager.dynamicOverrides // dynamically set via setter. highest precedence
     ])
-    populateInstanceComputableOptions(spec.options)
   }
 
 
@@ -182,7 +182,6 @@ export default class ViewSpecManager {
       queryButtonText(optionsManager.dirDefaults) ||
       spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
       queryButtonText(globalDefaults) ||
-      (spec.duration ? this._calendar.humanizeDuration(spec.duration) : null) || // like "3 days"
       requestedViewType // fall back to given view name
   }
 

+ 6 - 5
src/agenda/AgendaView.ts

@@ -1,4 +1,3 @@
-import * as moment from 'moment'
 import { htmlEscape } from '../util/html'
 import { copyOwnProps } from '../util/object'
 import { findElements, createElement } from '../util/dom-manip'
@@ -12,6 +11,7 @@ import Scroller from '../common/Scroller'
 import View from '../View'
 import TimeGrid from './TimeGrid'
 import DayGrid from '../basic/DayGrid'
+import { createDuration } from '../datelib/duration'
 
 const AGENDA_ALL_DAY_EVENT_LIMIT = 5
 
@@ -255,8 +255,8 @@ export default class AgendaView extends View {
 
   // Computes the initial pre-configured scroll state prior to allowing the user to change it
   computeInitialDateScroll() {
-    let scrollTime = moment.duration(this.opt('scrollTime'))
-    let top = this.timeGrid.computeTimeTop(scrollTime)
+    let scrollTime = createDuration(this.opt('scrollTime'))
+    let top = this.timeGrid.computeTimeTop(scrollTime.time)
 
     // zoom can give weird floating-point values. rather scroll a little bit further
     top = Math.ceil(top)
@@ -384,11 +384,12 @@ agendaTimeGridMethods = {
   renderHeadIntroHtml() {
     let view = this.view
     let calendar = view.calendar
-    let weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true)
+    let dateEnv = calendar.dateEnv
+    let weekStart = this.dateProfile.renderUnzonedRange.start
     let weekText
 
     if (this.opt('weekNumbers')) {
-      weekText = weekStart.format(this.opt('smallWeekFormat'))
+      weekText = dateEnv.formatWeek(weekStart, true)
 
       return '' +
         '<th class="fc-axis fc-week-number ' + calendar.theme.getClass('widgetHeader') + '" ' + view.axisStyleAttr() + '>' +

+ 55 - 47
src/agenda/TimeGrid.ts

@@ -1,8 +1,5 @@
-import * as moment from 'moment'
 import { htmlEscape } from '../util/html'
-import { divideDurationByDuration } from '../util/date'
 import { htmlToElement, findElements, createElement, removeElement, applyStyle } from '../util/dom-manip'
-import { isInt } from '../util/misc'
 import InteractiveDateComponent from '../component/InteractiveDateComponent'
 import BusinessHourRenderer from '../component/renderers/BusinessHourRenderer'
 import StandardInteractionsMixin from '../component/interactions/StandardInteractionsMixin'
@@ -13,6 +10,9 @@ import ComponentFootprint from '../models/ComponentFootprint'
 import TimeGridEventRenderer from './TimeGridEventRenderer'
 import TimeGridHelperRenderer from './TimeGridHelperRenderer'
 import TimeGridFillRenderer from './TimeGridFillRenderer'
+import { Duration, createDuration, addDurations, wholeDivideDurationByDuration, asRoughMs } from '../datelib/duration'
+import { startOfDay, DateMarker, addMs } from '../datelib/util'
+import { createFormatter, DateFormatter } from '../datelib/formatting'
 
 /* A component that renders one or more columns of vertical time slots
 ----------------------------------------------------------------------------------------------------------------------*/
@@ -20,7 +20,7 @@ import TimeGridFillRenderer from './TimeGridFillRenderer'
 
 // potential nice values for the slot-duration and interval-duration
 // from largest to smallest
-let AGENDA_STOCK_SUB_DURATIONS = [
+const AGENDA_STOCK_SUB_DURATIONS = [
   { hours: 1 },
   { minutes: 30 },
   { minutes: 15 },
@@ -28,6 +28,8 @@ let AGENDA_STOCK_SUB_DURATIONS = [
   { seconds: 15 }
 ]
 
+const HMS_FORMAT = createFormatter({ hour: '2-digit', minute: '2-digit', second: '2-digit' })
+
 export default class TimeGrid extends InteractiveDateComponent {
 
   dayDates: DayTableInterface['dayDates']
@@ -43,11 +45,11 @@ export default class TimeGrid extends InteractiveDateComponent {
   helperRenderer: any
 
   dayRanges: any // UnzonedRange[], of start-end of each day
-  slotDuration: any // duration of a "slot", a distinct time segment on given day, visualized by lines
-  snapDuration: any // granularity of time for dragging and selecting
+  slotDuration: Duration // duration of a "slot", a distinct time segment on given day, visualized by lines
+  snapDuration: Duration // granularity of time for dragging and selecting
   snapsPerSlot: any
-  labelFormat: any // formatting string for times running along vertical axis
-  labelInterval: any // duration of how often a label should be displayed for a slot
+  labelFormat: DateFormatter // formatting string for times running along vertical axis
+  labelInterval: Duration // duration of how often a label should be displayed for a slot
 
   headContainerEl: HTMLElement // div that hold's the date header
   colEls: HTMLElement[] // cells elements in the day-row background
@@ -114,8 +116,8 @@ export default class TimeGrid extends InteractiveDateComponent {
 
       if (segRange) {
         segs.push({
-          startMs: segRange.startMs,
-          endMs: segRange.endMs,
+          start: segRange.start,
+          end: segRange.end,
           isStart: segRange.isStart,
           isEnd: segRange.isEnd,
           dayIndex: dayIndex
@@ -137,8 +139,8 @@ export default class TimeGrid extends InteractiveDateComponent {
     let snapDuration = this.opt('snapDuration')
     let input
 
-    slotDuration = moment.duration(slotDuration)
-    snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration
+    slotDuration = createDuration(slotDuration)
+    snapDuration = snapDuration ? createDuration(snapDuration) : slotDuration
 
     this.slotDuration = slotDuration
     this.snapDuration = snapDuration
@@ -151,12 +153,19 @@ export default class TimeGrid extends InteractiveDateComponent {
       input = input[input.length - 1]
     }
 
-    this.labelFormat = input ||
-      this.opt('smallTimeFormat') // the computed default
+    this.labelFormat = createFormatter(
+      input ||
+      {
+        // like "h(:mm)a" -> "6pm" / "6:30pm"
+        hour: 'numeric',
+        minute: '2-digit',
+        // TODO: omit minute if possible
+      }
+    )
 
     input = this.opt('slotLabelInterval')
     this.labelInterval = input ?
-      moment.duration(input) :
+      createDuration(input) :
       this.computeLabelInterval(slotDuration)
   }
 
@@ -169,14 +178,14 @@ export default class TimeGrid extends InteractiveDateComponent {
 
     // find the smallest stock label interval that results in more than one slots-per-label
     for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
-      labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i])
-      slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration)
-      if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
+      labelInterval = createDuration(AGENDA_STOCK_SUB_DURATIONS[i])
+      slotsPerLabel = wholeDivideDurationByDuration(labelInterval, slotDuration)
+      if (slotsPerLabel !== null && slotsPerLabel > 1) {
         return labelInterval
       }
     }
 
-    return moment.duration(slotDuration) // fall back. clone
+    return slotDuration // fall back
   }
 
 
@@ -233,33 +242,35 @@ export default class TimeGrid extends InteractiveDateComponent {
   renderSlatRowHtml() {
     let view = this.view
     let calendar = view.calendar
+    let dateEnv = calendar.dateEnv
     let theme = calendar.theme
     let isRTL = this.isRTL
     let dateProfile = this.dateProfile
     let html = ''
-    let slotTime = moment.duration(+dateProfile.minTime) // wish there was .clone() for durations
-    let slotIterator = moment.duration(0)
+    let dayStart = startOfDay(dateProfile.renderUnzonedRange.start)
+    let slotTime = dateProfile.minTime
+    let slotIterator = createDuration(0)
     let slotDate // will be on the view's first day, but we only care about its time
     let isLabeled
     let axisHtml
 
     // Calculate the time for each slot
-    while (slotTime < dateProfile.maxTime) {
-      slotDate = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs).time(slotTime)
-      isLabeled = isInt(divideDurationByDuration(slotIterator, this.labelInterval))
+    while (asRoughMs(slotTime) < asRoughMs(dateProfile.maxTime)) {
+      slotDate = dateEnv.add(dayStart, slotTime)
+      isLabeled = wholeDivideDurationByDuration(slotIterator, this.labelInterval) !== null
 
       axisHtml =
         '<td class="fc-axis fc-time ' + theme.getClass('widgetContent') + '" ' + view.axisStyleAttr() + '>' +
           (isLabeled ?
             '<span>' + // for matchCellWidths
-              htmlEscape(slotDate.format(this.labelFormat)) +
+              htmlEscape(dateEnv.toFormat(slotDate, this.labelFormat)) +
             '</span>' :
             ''
             ) +
         '</td>'
 
       html +=
-        '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
+        '<tr data-time="' + dateEnv.toFormat(slotDate, HMS_FORMAT) + '"' +
           (isLabeled ? '' : ' class="fc-minor"') +
           '>' +
           (!isRTL ? axisHtml : '') +
@@ -267,8 +278,8 @@ export default class TimeGrid extends InteractiveDateComponent {
           (isRTL ? axisHtml : '') +
         '</tr>'
 
-      slotTime.add(this.slotDuration)
-      slotIterator.add(this.slotDuration)
+      slotTime = addDurations(slotTime, this.slotDuration)
+      slotIterator = addDurations(slotIterator, this.slotDuration)
     }
 
     return html
@@ -278,11 +289,12 @@ export default class TimeGrid extends InteractiveDateComponent {
   renderColumns() {
     let dateProfile = this.dateProfile
     let theme = this.view.calendar.theme
+    const dateEnv = this.view.calendar.dateEnv
 
     this.dayRanges = this.dayDates.map(function(dayDate) {
       return new UnzonedRange(
-        dayDate.clone().add(dateProfile.minTime),
-        dayDate.clone().add(dateProfile.maxTime)
+        dateEnv.add(dayDate, dateProfile.minTime),
+        dateEnv.add(dayDate, dateProfile.maxTime)
       )
     })
 
@@ -422,7 +434,7 @@ export default class TimeGrid extends InteractiveDateComponent {
     //  more than once because of columns with the same date (resources columns for example)
     let segs = this.componentFootprintToSegs(
       new ComponentFootprint(
-        new UnzonedRange(date, date.valueOf() + 1), // protect against null range
+        new UnzonedRange(date, addMs(date, 1)), // protect against null range
         false // all-day
       )
     )
@@ -481,22 +493,17 @@ export default class TimeGrid extends InteractiveDateComponent {
 
 
   // Computes the top coordinate, relative to the bounds of the grid, of the given date.
-  // `ms` can be a millisecond UTC time OR a UTC moment.
   // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
-  computeDateTop(ms, startOfDayDate) {
-    return this.computeTimeTop(
-      moment.duration(
-        ms - startOfDayDate.clone().stripTime()
-      )
-    )
+  computeDateTop(when: DateMarker, startOfDayDate: DateMarker) {
+    return this.computeTimeTop(when.valueOf() - startOfDayDate.valueOf())
   }
 
 
   // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
-  computeTimeTop(time) {
+  computeTimeTop(timeMs: number) {
     let len = this.slatEls.length
     let dateProfile = this.dateProfile
-    let slatCoverage = (time - dateProfile.minTime) / this.slotDuration // floating-point value of # of slots covered
+    let slatCoverage = (timeMs - asRoughMs(dateProfile.minTime)) / asRoughMs(this.slotDuration) // floating-point value of # of slots covered
     let slatIndex
     let slatRemainder
 
@@ -539,10 +546,10 @@ export default class TimeGrid extends InteractiveDateComponent {
       seg = segs[i]
       dayDate = this.dayDates[seg.dayIndex]
 
-      seg.top = this.computeDateTop(seg.startMs, dayDate)
+      seg.top = this.computeDateTop(seg.start, dayDate)
       seg.bottom = Math.max(
         seg.top + eventMinHeight,
-        this.computeDateTop(seg.endMs, dayDate)
+        this.computeDateTop(seg.end, dayDate)
       )
     }
   }
@@ -619,12 +626,13 @@ export default class TimeGrid extends InteractiveDateComponent {
 
 
   getHitFootprint(hit) {
+    const dateEnv = this.view.calendar.dateEnd
     let start = this.getCellDate(0, hit.col) // row=0
-    let time = this.computeSnapTime(hit.snap) // pass in the snap-index
+    let timeMs = this.computeSnapTime(hit.snap) // pass in the snap-index
     let end
 
-    start.time(time)
-    end = start.clone().add(this.snapDuration)
+    start = addMs(start, timeMs)
+    end = dateEnv.add(start, this.snapDuration)
 
     return new ComponentFootprint(
       new UnzonedRange(start, end),
@@ -634,8 +642,8 @@ export default class TimeGrid extends InteractiveDateComponent {
 
 
   // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
-  computeSnapTime(snapIndex) {
-    return moment.duration(this.dateProfile.minTime + this.snapDuration * snapIndex)
+  computeSnapTime(snapIndex): number {
+    return asRoughMs(this.dateProfile.minTime) + asRoughMs(this.snapDuration) * snapIndex
   }
 
 

+ 16 - 9
src/agenda/TimeGridEventRenderer.ts

@@ -1,6 +1,10 @@
 import { htmlEscape, cssToStr } from '../util/html'
 import { removeElement, applyStyle } from '../util/dom-manip'
+import { createFormatter } from '../datelib/formatting'
 import EventRenderer from '../component/renderers/EventRenderer'
+import EventDef from '../models/event/EventDef'
+
+const FULL_TIME_FORMAT = createFormatter({ hour: 'numeric', minute: '2-digit' })
 
 /*
 Only handles foreground segs.
@@ -49,7 +53,11 @@ export default class TimeGridEventRenderer extends EventRenderer {
 
   // Computes a default event time formatting string if `timeFormat` is not explicitly defined
   computeEventTimeFormat() {
-    return this.opt('noMeridiemTimeFormat') // like "6:30" (no AM/PM)
+    return {
+      hour: 'numeric',
+      minute: '2-digit',
+      // TODO: remove am/pm
+    }
   }
 
 
@@ -62,10 +70,9 @@ export default class TimeGridEventRenderer extends EventRenderer {
   // Renders the HTML for a single event segment's default rendering
   fgSegHtml(seg, disableResizing) {
     let view = this.view
-    let calendar = view.calendar
     let componentFootprint = seg.footprint.componentFootprint
     let isAllDay = componentFootprint.isAllDay
-    let eventDef = seg.footprint.eventDef
+    let eventDef: EventDef = seg.footprint.eventDef
     let isDraggable = view.isEventDefDraggable(eventDef)
     let isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef)
     let isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef)
@@ -83,16 +90,16 @@ export default class TimeGridEventRenderer extends EventRenderer {
       // That would appear as midnight-midnight and would look dumb.
       // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
       if (seg.isStart || seg.isEnd) {
-        let zonedStart = calendar.msToMoment(seg.startMs)
-        let zonedEnd = calendar.msToMoment(seg.endMs)
-        timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay)
-        fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT')
-        startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false) // displayEnd=false
+        let unzonedStart = seg.start
+        let unzonedEnd = seg.end
+        timeText = this._getTimeText(unzonedStart, unzonedEnd, isAllDay) // TODO: give the timezones
+        fullTimeText = this._getTimeText(unzonedStart, unzonedEnd, isAllDay, FULL_TIME_FORMAT)
+        startTimeText = this._getTimeText(unzonedStart, unzonedEnd, isAllDay, null, false) // displayEnd=false
       }
     } else {
       // Display the normal time text for the *event's* times
       timeText = this.getTimeText(seg.footprint)
-      fullTimeText = this.getTimeText(seg.footprint, 'LT')
+      fullTimeText = this.getTimeText(seg.footprint, FULL_TIME_FORMAT)
       startTimeText = this.getTimeText(seg.footprint, null, false) // displayEnd=false
     }
 

+ 2 - 1
src/basic/BasicView.ts

@@ -291,6 +291,7 @@ function makeDayGridSubclass(SuperClass) {
 
     // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
     renderNumberIntroHtml(row) {
+      const dateEnv = this.calendar.dateEnv
       let view = this.view
       let weekStart = this.getCellDate(row, 0)
 
@@ -299,7 +300,7 @@ function makeDayGridSubclass(SuperClass) {
           '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' +
             view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths
               { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
-              weekStart.format('w') // inner HTML
+              dateEnv.formatWeek(weekStart) // inner HTML
             ) +
           '</td>'
       }

+ 9 - 5
src/basic/BasicViewDateProfileGenerator.ts

@@ -1,22 +1,26 @@
 import UnzonedRange from '../models/UnzonedRange'
 import DateProfileGenerator from '../DateProfileGenerator'
+import { addWeeks } from '../datelib/util'
 
 
 export default class BasicViewDateProfileGenerator extends DateProfileGenerator {
 
   // Computes the date range that will be rendered.
   buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
+    const dateEnv = this._view.calendar.dateEnv
     let renderUnzonedRange = super.buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) // an UnzonedRange
-    let start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay)
-    let end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay)
+    let start = renderUnzonedRange.start
+    let end = renderUnzonedRange.end
+    let endOfWeek
 
     // year and month views should be aligned with weeks. this is already done for week
     if (/^(year|month)$/.test(currentRangeUnit)) {
-      start.startOf('week')
+      start = dateEnv.startOfWeek(start)
 
       // make end-of-week if not already
-      if (end.weekday()) {
-        end.add(1, 'week').startOf('week') // exclusively move backwards
+      endOfWeek = dateEnv.startOfWeek(end)
+      if (endOfWeek.valueOf() !== end.valueOf()) {
+        end = addWeeks(endOfWeek, 1)
       }
     }
 

+ 18 - 17
src/basic/DayGrid.ts

@@ -21,6 +21,10 @@ import { default as DayTableMixin, DayTableInterface } from '../component/DayTab
 import DayGridEventRenderer from './DayGridEventRenderer'
 import DayGridHelperRenderer from './DayGridHelperRenderer'
 import DayGridFillRenderer from './DayGridFillRenderer'
+import { addDays } from '../datelib/util'
+import { createFormatter } from '../datelib/formatting'
+
+const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' })
 
 
 /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
@@ -231,6 +235,7 @@ export default class DayGrid extends InteractiveDateComponent {
   // The number row will only exist if either day numbers or week numbers are turned on.
   renderNumberCellHtml(date) {
     let view = this.view
+    const dateEnv = view.calendar.dateEnv
     let html = ''
     let isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid
@@ -246,21 +251,12 @@ export default class DayGrid extends InteractiveDateComponent {
     classes.unshift('fc-day-top')
 
     if (this.cellWeekNumbersVisible) {
-      // To determine the day of week number change under ISO, we cannot
-      // rely on moment.js methods such as firstDayOfWeek() or weekday(),
-      // because they rely on the locale's dow (possibly overridden by
-      // our firstDay option), which may not be Monday. We cannot change
-      // dow, because that would affect the calendar start day as well.
-      if (date._locale._fullCalendar_weekCalc === 'ISO') {
-        weekCalcFirstDoW = 1  // Monday by ISO 8601 definition
-      } else {
-        weekCalcFirstDoW = date._locale.firstDayOfWeek()
-      }
+      weekCalcFirstDoW = dateEnv.weekMeta.dow
     }
 
     html += '<td class="' + classes.join(' ') + '"' +
       (isDateValid ?
-        ' data-date="' + date.format() + '"' :
+        ' data-date="' + dateEnv.toIso(date, { omitTime: true }) + '"' :
         ''
         ) +
       '>'
@@ -269,7 +265,7 @@ export default class DayGrid extends InteractiveDateComponent {
       html += view.buildGotoAnchorHtml(
         { date: date, type: 'week' },
         { 'class': 'fc-week-number' },
-        date.format('w') // inner HTML
+        dateEnv.formatWeek(date) // inner HTML
       )
     }
 
@@ -277,7 +273,7 @@ export default class DayGrid extends InteractiveDateComponent {
       html += view.buildGotoAnchorHtml(
         date,
         { 'class': 'fc-day-number' },
-        date.format('D') // inner HTML
+        dateEnv.toFormat(date, DAY_NUM_FORMAT) // inner HTML
       )
     }
 
@@ -623,7 +619,7 @@ export default class DayGrid extends InteractiveDateComponent {
           context: view,
           args: [
             {
-              date: date.clone(),
+              date: date,
               dayEl: dayEl,
               moreEl: moreEl,
               segs: reslicedAllSegs,
@@ -700,8 +696,13 @@ export default class DayGrid extends InteractiveDateComponent {
   // Builds the inner DOM contents of the segment popover
   renderSegPopoverContent(row, col, segs): ElementContent {
     let view = this.view
+    const dateEnv = view.calendar.dateEnv
     let theme = view.calendar.theme
-    let title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'))
+    let title = dateEnv.toFormat(
+      this.getCellDate(row, col),
+      createFormatter(this.opt('dayPopoverFormat')) // TODO: cache
+    )
+
     let content = htmlToElements(
       '<div class="fc-header ' + theme.getClass('popoverHeader') + '">' +
         '<span class="fc-close ' + theme.getIconClass('close') + '"></span>' +
@@ -738,8 +739,8 @@ export default class DayGrid extends InteractiveDateComponent {
 
   // Given the events within an array of segment objects, reslice them to be in a single day
   resliceDaySegs(segs, dayDate) {
-    let dayStart = dayDate.clone()
-    let dayEnd = dayStart.clone().add(1, 'days')
+    let dayStart = dayDate
+    let dayEnd = addDays(dayStart, 1)
     let dayRange = new UnzonedRange(dayStart, dayEnd)
     let newSegs = []
     let i

+ 6 - 1
src/basic/DayGridEventRenderer.ts

@@ -219,7 +219,12 @@ export default class DayGridEventRenderer extends EventRenderer {
 
   // Computes a default event time formatting string if `timeFormat` is not explicitly defined
   computeEventTimeFormat() {
-    return this.opt('extraSmallTimeFormat') // like "6p" or "6:30p"
+    return {
+      hour: 'numeric',
+      minute: '2-digit',
+      // TODO: remove :00
+      // TODO: convert am/pm -> a/p
+    }
   }
 
 

+ 5 - 3
src/basic/MonthView.ts

@@ -1,7 +1,7 @@
-import * as moment from 'moment'
 import { distributeHeight } from '../util/misc'
 import BasicView from './BasicView'
 import MonthViewDateProfileGenerator from './MonthViewDateProfileGenerator'
+import { DateMarker } from '../datelib/util'
 
 
 /* A month view with day cells running in rows (one-per-week) and columns
@@ -21,8 +21,10 @@ export default class MonthView extends BasicView {
   }
 
 
-  isDateInOtherMonth(date, dateProfile) {
-    return date.month() !== moment.utc(dateProfile.currentUnzonedRange.startMs).month() // TODO: optimize
+  isDateInOtherMonth(date: DateMarker, dateProfile) {
+    const dateEnv = this.calendar.dateEnv
+
+    return dateEnv.getMonth(date) !== dateEnv.getMonth(dateProfile.currentUnzonedRange.start)
   }
 
 }

+ 6 - 4
src/basic/MonthViewDateProfileGenerator.ts

@@ -1,5 +1,7 @@
 import BasicViewDateProfileGenerator from './BasicViewDateProfileGenerator'
 import UnzonedRange from '../models/UnzonedRange'
+import { diffWeeks } from '../datelib/env'
+import { addWeeks } from '../datelib/util'
 
 
 export default class MonthViewDateProfileGenerator extends BasicViewDateProfileGenerator {
@@ -7,16 +9,16 @@ export default class MonthViewDateProfileGenerator extends BasicViewDateProfileG
   // Computes the date range that will be rendered.
   buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay) {
     let renderUnzonedRange = super.buildRenderRange(currentUnzonedRange, currentRangeUnit, isRangeAllDay)
-    let start = this.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay)
-    let end = this.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay)
+    let start = renderUnzonedRange.start
+    let end = renderUnzonedRange.end
     let rowCnt
 
     // ensure 6 weeks
     if (this.opt('fixedWeekCount')) {
       rowCnt = Math.ceil( // could be partial weeks due to hiddenDays
-        end.diff(start, 'weeks', true) // dontRound=true
+        diffWeeks(start, end)
       )
-      end.add(6 - rowCnt, 'weeks')
+      end = addWeeks(end, 6 - rowCnt)
     }
 
     return new UnzonedRange(start, end)

+ 54 - 52
src/component/DateComponent.ts

@@ -1,12 +1,11 @@
-import * as moment from 'moment'
 import { attrsToStr, htmlEscape } from '../util/html'
-import { dayIDs } from '../util/date'
-import momentExt from '../moment-ext'
-import { formatRange } from '../date-formatting'
 import Component from './Component'
 import { eventRangeToEventFootprint } from '../models/event/util'
 import EventFootprint from '../models/event/EventFootprint'
 import { DateProfile } from '../DateProfileGenerator'
+import { addDays, DateMarker, startOfDay, dayIDs } from '../datelib/util'
+import { diffDays } from '../datelib/env'
+import { Duration, createDuration, asRoughMs } from '../datelib/duration'
 
 
 export default abstract class DateComponent extends Component {
@@ -21,8 +20,8 @@ export default abstract class DateComponent extends Component {
   uid: any
   childrenByUid: any
   isRTL: boolean = false // frequently accessed options
-  nextDayThreshold: any // "
-  dateProfile: any // hack
+  nextDayThreshold: Duration // "
+  dateProfile: DateProfile // hack
 
   eventRenderer: any
   helperRenderer: any
@@ -50,7 +49,7 @@ export default abstract class DateComponent extends Component {
     this.uid = String(DateComponent.guid++)
     this.childrenByUid = {}
 
-    this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'))
+    this.nextDayThreshold = createDuration(this.opt('nextDayThreshold'))
     this.isRTL = this.opt('isRTL')
 
     if (this.fillRendererClass) {
@@ -210,7 +209,7 @@ export default abstract class DateComponent extends Component {
       this.eventRenderer.rangeUpdated() // poorly named now
       this.eventRenderer.render(eventsPayload)
     } else if (this['renderEvents']) { // legacy
-      this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload))
+      this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload, this._getCalendar()))
     }
 
     this.callChildren('executeEventRender', arguments)
@@ -291,7 +290,7 @@ export default abstract class DateComponent extends Component {
         let legacy
 
         if (seg.el) { // necessary?
-          legacy = seg.footprint.getEventLegacy()
+          legacy = seg.footprint.getEventLegacy(this._getCalendar())
 
           this.publiclyTrigger('eventAfterRender', {
             context: legacy,
@@ -316,7 +315,7 @@ export default abstract class DateComponent extends Component {
         let legacy
 
         if (seg.el) { // necessary?
-          legacy = seg.footprint.getEventLegacy()
+          legacy = seg.footprint.getEventLegacy(this._getCalendar())
 
           this.publiclyTrigger('eventDestroy', {
             context: legacy,
@@ -674,27 +673,28 @@ export default abstract class DateComponent extends Component {
 
   // Generates HTML for an anchor to another view into the calendar.
   // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
-  // `gotoOptions` can either be a moment input, or an object with the form:
+  // `gotoOptions` can either be a date input, or an object with the form:
   // { date, type, forceOff }
   // `type` is a view-type like "day" or "week". default value is "day".
   // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
   buildGotoAnchorHtml(gotoOptions, attrs, innerHtml) {
+    const dateEnv = this._getCalendar().dateEnv
     let date
     let type
     let forceOff
     let finalOptions
 
-    if (moment.isMoment(gotoOptions) || typeof gotoOptions !== 'object') {
-      date = gotoOptions // a single moment input
+    if (gotoOptions instanceof Date || typeof gotoOptions !== 'object') {
+      date = gotoOptions // a single date-like input
     } else {
       date = gotoOptions.date
       type = gotoOptions.type
       forceOff = gotoOptions.forceOff
     }
-    date = momentExt(date) // if a string, parse it
+    date = dateEnv.createMarker(date) // if a string, parse it
 
     finalOptions = { // for serialization into the link
-      date: date.format('YYYY-MM-DD'),
+      date: dateEnv.toIso(date, { omitTime: true }),
       type: type || 'day'
     }
 
@@ -725,32 +725,34 @@ export default abstract class DateComponent extends Component {
 
 
   // Computes HTML classNames for a single-day element
-  getDayClasses(date, noThemeHighlight?) {
+  getDayClasses(date: DateMarker, noThemeHighlight?) {
     let view = this._getView()
     let classes = []
-    let today
+    let todayStart: DateMarker
+    let todayEnd: DateMarker
 
     if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
       classes.push('fc-disabled-day') // TODO: jQuery UI theme?
     } else {
-      classes.push('fc-' + dayIDs[date.day()])
+      classes.push('fc-' + dayIDs[date.getUTCDay()])
 
       if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
         classes.push('fc-other-month')
       }
 
-      today = view.calendar.getNow()
+      todayStart = startOfDay(view.calendar.getNow())
+      todayEnd = addDays(todayStart, 1)
 
-      if (date.isSame(today, 'day')) {
+      if (date < todayStart) {
+        classes.push('fc-past')
+      } else if (date >= todayEnd) {
+        classes.push('fc-future')
+      } else {
         classes.push('fc-today')
 
         if (noThemeHighlight !== true) {
           classes.push(view.calendar.theme.getClass('today'))
         }
-      } else if (date < today) {
-        classes.push('fc-past')
-      } else {
-        classes.push('fc-future')
       }
     }
 
@@ -758,49 +760,49 @@ export default abstract class DateComponent extends Component {
   }
 
 
-  // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
-  // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
-  // The timezones of the dates within `range` will be respected.
-  formatRange(range: { start: moment.Moment, end: moment.Moment }, isAllDay, formatStr, separator) {
-    let end = range.end
+  // Compute the number of the give units in the "current" range.
+  // Will return `0` if there's not a clean whole interval.
+  currentRangeAs(unit) {
+    const dateEnv = this._getCalendar().dateEnv
+    let range = this._getDateProfile().currentUnzonedRange
+    let res = null
 
-    if (isAllDay) {
-      end = end.clone().subtract(1) // convert to inclusive. last ms of previous day
+    if (unit === 'year') {
+      res = dateEnv.diffWholeYears(range.start, range.end)
+    } else if (unit === 'month') {
+      res = dateEnv.diffWholeMonths(range.start, range.end)
+    } else if (unit === 'week') {
+      res = dateEnv.diffWholeMonths(range.start, range.end)
+    } else if (unit === 'day') {
+      res = dateEnv.diffWholeDays(range.start, range.end)
     }
 
-    return formatRange(range.start, end, formatStr, separator, this.isRTL)
-  }
-
-
-  // Compute the number of the give units in the "current" range.
-  // Will return a floating-point number. Won't round.
-  currentRangeAs(unit) {
-    return this._getDateProfile().currentUnzonedRange.as(unit)
+    return res || 0
   }
 
 
   // Returns the date range of the full days the given range visually appears to occupy.
   // Returns a plain object with start/end, NOT an UnzonedRange!
-  computeDayRange(unzonedRange): { start: moment.Moment, end: moment.Moment } {
-    let calendar = this._getCalendar()
-    let startDay = calendar.msToUtcMoment(unzonedRange.startMs, true) // the beginning of the day the range starts
-    let end = calendar.msToUtcMoment(unzonedRange.endMs)
-    let endTimeMS = +end.time() // # of milliseconds into `endDay`
-    let endDay = end.clone().stripTime() // the beginning of the day the range exclusively ends
+  computeDayRange(unzonedRange): { start: DateMarker, end: DateMarker } {
+    const dateEnv = this._getCalendar().dateEnv
+    let startDay: DateMarker = dateEnv.startOfDay(unzonedRange.start) // the beginning of the day the range starts
+    let end: DateMarker = unzonedRange.end
+    let endDay: DateMarker = dateEnv.startOfDay(end)
+    let endTimeMS: number = end.valueOf() - endDay.valueOf() // # of milliseconds into `endDay`
 
     // If the end time is actually inclusively part of the next day and is equal to or
     // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
     // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
-    if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
-      endDay.add(1, 'days')
+    if (endTimeMS && endTimeMS >= asRoughMs(this.nextDayThreshold)) {
+      endDay = addDays(endDay, 1)
     }
 
     // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
     if (endDay <= startDay) {
-      endDay = startDay.clone().add(1, 'days')
+      endDay = addDays(startDay, 1)
     }
 
-    return { start: startDay, end: endDay }
+    return { start: startDay, end: endDay } // TODO: eventually use UnzonedRange?
   }
 
 
@@ -808,7 +810,7 @@ export default abstract class DateComponent extends Component {
   isMultiDayRange(unzonedRange) {
     let dayRange = this.computeDayRange(unzonedRange)
 
-    return dayRange.end.diff(dayRange.start, 'days') > 1
+    return diffDays(dayRange.start, dayRange.end) > 1
   }
 
 }
@@ -816,7 +818,7 @@ export default abstract class DateComponent extends Component {
 
 // legacy
 
-function convertEventsPayloadToLegacyArray(eventsPayload) {
+function convertEventsPayloadToLegacyArray(eventsPayload, calendar) {
   let eventDefId
   let eventInstances
   let legacyEvents = []
@@ -827,7 +829,7 @@ function convertEventsPayloadToLegacyArray(eventsPayload) {
 
     for (i = 0; i < eventInstances.length; i++) {
       legacyEvents.push(
-        eventInstances[i].toLegacy()
+        eventInstances[i].toLegacy(calendar)
       )
     }
   }

+ 38 - 33
src/component/DayTableMixin.ts

@@ -1,10 +1,12 @@
 import { htmlEscape } from '../util/html'
-import { dayIDs } from '../util/date'
 import { prependToElement, appendToElement } from '../util/dom-manip'
 import Mixin from '../common/Mixin'
+import { DateMarker, addDays, dayIDs } from '../datelib/util'
+import { diffDays } from '../datelib/env'
+import { createFormatter } from '../datelib/formatting'
 
 export interface DayTableInterface {
-  dayDates: any
+  dayDates: DateMarker[]
   daysPerRow: any
   rowCnt: any
   colCnt: any
@@ -27,7 +29,7 @@ Prerequisite: the object being mixed into needs to be a *Grid*
 export default class DayTableMixin extends Mixin implements DayTableInterface {
 
   breakOnWeeks: boolean // should create a new row for each week? not specified, so default is FALSY
-  dayDates: any // whole-day dates for each column. left to right
+  dayDates: DateMarker[] // whole-day dates for each column. left to right
   dayIndices: any // for each day from start, the offset
   daysPerRow: any
   rowCnt: any
@@ -39,32 +41,31 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   updateDayTable() {
     let t = (this as any)
     let view = t.view
-    let calendar = view.calendar
-    let date = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.startMs, true)
-    let end = calendar.msToUtcMoment(t.dateProfile.renderUnzonedRange.endMs, true)
+    let date: DateMarker = t.dateProfile.renderUnzonedRange.start
+    let end: DateMarker = t.dateProfile.renderUnzonedRange.end
     let dayIndex = -1
     let dayIndices = []
-    let dayDates = []
+    let dayDates: DateMarker[] = []
     let daysPerRow
     let firstDay
     let rowCnt
 
-    while (date.isBefore(end)) { // loop each day from start to end
+    while (date < end) { // loop each day from start to end
       if (view.isHiddenDay(date)) {
         dayIndices.push(dayIndex + 0.5) // mark that it's between indices
       } else {
         dayIndex++
         dayIndices.push(dayIndex)
-        dayDates.push(date.clone())
+        dayDates.push(date)
       }
-      date.add(1, 'days')
+      date = addDays(date, 1)
     }
 
     if (this.breakOnWeeks) {
       // count columns until the day-of-week repeats
-      firstDay = dayDates[0].day()
+      firstDay = dayDates[0].getUTCDate()
       for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
-        if (dayDates[daysPerRow].day() === firstDay) {
+        if (dayDates[daysPerRow].getUTCDate() === firstDay) {
           break
         }
       }
@@ -86,10 +87,11 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   // Computes and assigned the colCnt property and updates any options that may be computed from it
   updateDayTableCols() {
     this.colCnt = this.computeColCnt()
-    this.colHeadFormat =
+    this.colHeadFormat = createFormatter(
       (this as any).opt('columnHeaderFormat') ||
       (this as any).opt('columnFormat') || // deprecated
       this.computeColHeadFormat()
+    )
   }
 
 
@@ -99,18 +101,16 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   }
 
 
-  // Computes the ambiguously-timed moment for the given cell
-  getCellDate(row, col) {
-    return this.dayDates[
-        this.getCellDayIndex(row, col)
-      ].clone()
+  // Computes the DateMarker for the given cell
+  getCellDate(row, col): DateMarker {
+    return this.dayDates[this.getCellDayIndex(row, col)]
   }
 
 
   // Computes the ambiguously-timed date range for the given cell
   getCellRange(row, col) {
     let start = this.getCellDate(row, col)
-    let end = start.clone().add(1, 'days')
+    let end = addDays(start, 1)
 
     return { start: start, end: end }
   }
@@ -139,7 +139,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
   getDateDayIndex(date) {
     let dayIndices = this.dayIndices
-    let dayOffset = date.diff(this.dayDates[0], 'days')
+    let dayOffset = Math.floor(diffDays(this.dayDates[0], date))
 
     if (dayOffset < 0) {
       return dayIndices[0] - 1
@@ -160,11 +160,11 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     // if more than one week row, or if there are a lot of columns with not much space,
     // put just the day numbers will be in each cell
     if (this.rowCnt > 1 || this.colCnt > 10) {
-      return 'ddd' // "Sat"
+      return { weekday: 'short' } // "Sat"
     } else if (this.colCnt > 1) {
-      return (this as any).opt('dayOfMonthFormat') // "Sat 12/10"
+      return { weekday: 'short', month: 'numeric', day: 'numeric' } // "Sat 11/12"
     } else {
-      return 'dddd' // "Saturday"
+      return { weekday: 'long' } // "Saturday"
     }
   }
 
@@ -178,7 +178,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     let daysPerRow = this.daysPerRow
     let normalRange = (this as any).view.computeDayRange(unzonedRange) // make whole-day range, considering nextDayThreshold
     let rangeFirst = this.getDateDayIndex(normalRange.start) // inclusive first index
-    let rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')) // inclusive last index
+    let rangeLast = this.getDateDayIndex(addDays(normalRange.end, -1)) // inclusive last index
     let segs = []
     let row
     let rowFirst
@@ -223,7 +223,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     let daysPerRow = this.daysPerRow
     let normalRange = (this as any).view.computeDayRange(unzonedRange) // make whole-day range, considering nextDayThreshold
     let rangeFirst = this.getDateDayIndex(normalRange.start) // inclusive first index
-    let rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')) // inclusive last index
+    let rangeLast = this.getDateDayIndex(addDays(normalRange.end, -1)) // inclusive last index
     let segs = []
     let row
     let rowFirst
@@ -302,7 +302,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   renderHeadDateCellsHtml() {
     let htmls = []
     let col
-    let date
+    let date: DateMarker
 
     for (col = 0; col < this.colCnt; col++) {
       date = this.getCellDate(0, col)
@@ -315,9 +315,11 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
 
   // TODO: when internalApiVersion, accept an object for HTML attributes
   // (colspan should be no different)
-  renderHeadDateCellHtml(date, colspan, otherAttrs) {
+  renderHeadDateCellHtml(date: DateMarker, colspan, otherAttrs) {
     let t = (this as any)
     let view = t.view
+    let calendar = view.calendar
+    let dateEnv = calendar.dateEnv
     let isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let classNames = [
       'fc-day-header',
@@ -328,9 +330,11 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
     if (typeof t.opt('columnHeaderHtml') === 'function') {
       innerHtml = t.opt('columnHeaderHtml')(date)
     } else if (typeof t.opt('columnHeaderText') === 'function') {
-      innerHtml = htmlEscape(t.opt('columnHeaderText')(date))
+      innerHtml = htmlEscape(
+        t.opt('columnHeaderText')(date)
+      )
     } else {
-      innerHtml = htmlEscape(date.format(t.colHeadFormat))
+      innerHtml = htmlEscape(dateEnv.toFormat(date, t.colHeadFormat))
     }
 
     // if only one row of days, the classNames on the header can represent the specific days beneath
@@ -341,13 +345,13 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
         t.getDayClasses(date, true)
       )
     } else {
-      classNames.push('fc-' + dayIDs[date.day()]) // only add the day-of-week class
+      classNames.push('fc-' + dayIDs[date.getUTCDay()]) // only add the day-of-week class
     }
 
     return '' +
       '<th class="' + classNames.join(' ') + '"' +
         ((isDateValid && t.rowCnt) === 1 ?
-          ' data-date="' + date.format('YYYY-MM-DD') + '"' :
+          ' data-date="' + dateEnv.toIso(date, { omitTime: true }) + '"' :
           '') +
         (colspan > 1 ?
           ' colspan="' + colspan + '"' :
@@ -402,9 +406,10 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
   }
 
 
-  renderBgCellHtml(date, otherAttrs) {
+  renderBgCellHtml(date: DateMarker, otherAttrs) {
     let t = (this as any)
     let view = t.view
+    const dateEnv = view.calendar.dateEnv
     let isDateValid = t.dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
     let classes = t.getDayClasses(date)
 
@@ -412,7 +417,7 @@ export default class DayTableMixin extends Mixin implements DayTableInterface {
 
     return '<td class="' + classes.join(' ') + '"' +
       (isDateValid ?
-        ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
+        ' data-date="' + dateEnv.toIso(date, { omitTime: true }) + '"' : // if date has a time, won't format it
         '') +
       (otherAttrs ?
         ' ' + otherAttrs :

+ 10 - 7
src/component/InteractiveDateComponent.ts

@@ -1,9 +1,8 @@
-import * as moment from 'moment'
-import { diffByUnit, diffDayTime } from '../util/date'
 import { elementClosest } from '../util/dom-manip'
 import { getEvIsTouch, listenBySelector, listenToHoverBySelector } from '../util/dom-event'
 import DateComponent from './DateComponent'
 import GlobalEmitter from '../common/GlobalEmitter'
+import { Duration, createDuration } from '../datelib/duration'
 
 
 export default abstract class InteractiveDateComponent extends DateComponent {
@@ -323,11 +322,15 @@ export default abstract class InteractiveDateComponent extends DateComponent {
 
   // Diffs the two dates, returning a duration, based on granularity of the grid
   // TODO: port isTimeScale into this system?
-  diffDates(a, b): moment.Duration {
-    if (this.largeUnit) {
-      return diffByUnit(a, b, this.largeUnit)
-    } else {
-      return diffDayTime(a, b)
+  diffDates(a, b): Duration {
+    const dateEnv = this._getCalendar().dateEnv
+
+    if (!this.largeUnit) {
+      return dateEnv.diffDayAndTime(a, b) // returns a duration
+    } else if (this.largeUnit === 'year') {
+      return createDuration(dateEnv.diffWholeYears(a, b), 'year')
+    } else if (this.largeUnit === 'month') {
+      return createDuration(dateEnv.diffWholeMonths(a, b), 'month')
     }
   }
 

+ 4 - 4
src/component/interactions/DateSelecting.ts

@@ -137,10 +137,10 @@ export default class DateSelecting extends Interaction {
   // Assumes both footprints are non-open-ended.
   computeSelectionFootprint(footprint0, footprint1) {
     let ms = [
-      footprint0.unzonedRange.startMs,
-      footprint0.unzonedRange.endMs,
-      footprint1.unzonedRange.startMs,
-      footprint1.unzonedRange.endMs
+      footprint0.unzonedRange.start,
+      footprint0.unzonedRange.end,
+      footprint1.unzonedRange.start,
+      footprint1.unzonedRange.end
     ]
 
     ms.sort(compareNumbers)

+ 6 - 6
src/component/interactions/EventDragging.ts

@@ -263,7 +263,7 @@ export default class EventDragging extends Interaction {
     this.component.publiclyTrigger('eventDragStart', {
       context: seg.el,
       args: [
-        seg.footprint.getEventLegacy(),
+        seg.footprint.getEventLegacy(this.component._getCalendar()),
         ev,
         {}, // jqui dummy
         this.view
@@ -278,7 +278,7 @@ export default class EventDragging extends Interaction {
     this.component.publiclyTrigger('eventDragStop', {
       context: seg.el,
       args: [
-        seg.footprint.getEventLegacy(),
+        seg.footprint.getEventLegacy(this.component._getCalendar()),
         ev,
         {}, // jqui dummy
         this.view
@@ -300,8 +300,8 @@ export default class EventDragging extends Interaction {
 
 
   computeEventDateMutation(startFootprint, endFootprint) {
-    let date0 = startFootprint.unzonedRange.getStart()
-    let date1 = endFootprint.unzonedRange.getStart()
+    let date0 = startFootprint.unzonedRange.start
+    let date1 = endFootprint.unzonedRange.start
     let clearEnd = false
     let forceTimed = false
     let forceAllDay = false
@@ -313,13 +313,13 @@ export default class EventDragging extends Interaction {
 
       if (endFootprint.isAllDay) {
         forceAllDay = true
-        date0.stripTime()
+        date0 = this.view.calendar.dateEnv.startOfDay(date0)
       } else {
         forceTimed = true
       }
     }
 
-    dateDelta = this.component.diffDates(date1, date0)
+    dateDelta = this.component.diffDates(date0, date1)
 
     dateMutation = new EventDefDateMutation()
     dateMutation.clearEnd = clearEnd

+ 3 - 3
src/component/interactions/EventPointing.ts

@@ -29,7 +29,7 @@ export default class EventPointing extends Interaction {
   handleClick(seg, ev) {
     let res = this.component.publiclyTrigger('eventClick', { // can return `false` to cancel
       context: seg.el,
-      args: [ seg.footprint.getEventLegacy(), ev, this.view ]
+      args: [ seg.footprint.getEventLegacy(this.view.calendar), ev, this.view ]
     })
 
     if (res === false) {
@@ -53,7 +53,7 @@ export default class EventPointing extends Interaction {
 
       this.component.publiclyTrigger('eventMouseover', {
         context: seg.el,
-        args: [ seg.footprint.getEventLegacy(), ev, this.view ]
+        args: [ seg.footprint.getEventLegacy(this.view.calendar), ev, this.view ]
       })
     }
   }
@@ -73,7 +73,7 @@ export default class EventPointing extends Interaction {
       this.component.publiclyTrigger('eventMouseout', {
         context: seg.el,
         args: [
-          seg.footprint.getEventLegacy(),
+          seg.footprint.getEventLegacy(this.view.calendar),
           ev || {}, // if given no arg, make a mock mouse event
           this.view
         ]

+ 10 - 8
src/component/interactions/EventResizing.ts

@@ -162,7 +162,7 @@ export default class EventResizing extends Interaction {
     this.component.publiclyTrigger('eventResizeStart', {
       context: seg.el,
       args: [
-        seg.footprint.getEventLegacy(),
+        seg.footprint.getEventLegacy(this.view.calendar),
         ev,
         {}, // jqui dummy
         this.view
@@ -177,7 +177,7 @@ export default class EventResizing extends Interaction {
     this.component.publiclyTrigger('eventResizeStop', {
       context: seg.el,
       args: [
-        seg.footprint.getEventLegacy(),
+        seg.footprint.getEventLegacy(this.view.calendar),
         ev,
         {}, // jqui dummy
         this.view
@@ -188,15 +188,16 @@ export default class EventResizing extends Interaction {
 
   // Returns new date-information for an event segment being resized from its start
   computeEventStartResizeMutation(startFootprint, endFootprint, origEventFootprint) {
+    const dateEnv = this.component._getCalendar().dateEnv
     let origRange = origEventFootprint.componentFootprint.unzonedRange
     let startDelta = this.component.diffDates(
-      endFootprint.unzonedRange.getStart(),
-      startFootprint.unzonedRange.getStart()
+      startFootprint.unzonedRange.start,
+      endFootprint.unzonedRange.start
     )
     let dateMutation
     let eventDefMutation
 
-    if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
+    if (dateEnv.add(origRange.start, startDelta) < origRange.end) {
 
       dateMutation = new EventDefDateMutation()
       dateMutation.setStartDelta(startDelta)
@@ -213,15 +214,16 @@ export default class EventResizing extends Interaction {
 
   // Returns new date-information for an event segment being resized from its end
   computeEventEndResizeMutation(startFootprint, endFootprint, origEventFootprint) {
+    const dateEnv = this.component._getCalendar().dateEnv
     let origRange = origEventFootprint.componentFootprint.unzonedRange
     let endDelta = this.component.diffDates(
-      endFootprint.unzonedRange.getEnd(),
-      startFootprint.unzonedRange.getEnd()
+      startFootprint.unzonedRange.end,
+      endFootprint.unzonedRange.end
     )
     let dateMutation
     let eventDefMutation
 
-    if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
+    if (dateEnv.add(origRange.end, endDelta) > origRange.start) {
 
       dateMutation = new EventDefDateMutation()
       dateMutation.setEndDelta(endDelta)

+ 12 - 17
src/component/interactions/ExternalDropping.ts

@@ -1,13 +1,13 @@
-import * as moment from 'moment'
 import { assignTo } from '../../util/object'
 import { elementMatches } from '../../util/dom-manip'
 import { disableCursor, enableCursor } from '../../util/misc'
-import momentExt from '../../moment-ext'
 import HitDragListener from '../../common/HitDragListener'
 import SingleEventDef from '../../models/event/SingleEventDef'
 import EventInstanceGroup from '../../models/event/EventInstanceGroup'
 import EventSource from '../../models/event-source/EventSource'
 import Interaction from './Interaction'
+import { startOfDay } from '../../datelib/util'
+import { createDuration } from '../../datelib/duration'
 
 
 export default class ExternalDropping extends Interaction {
@@ -54,8 +54,8 @@ export default class ExternalDropping extends Interaction {
     if (stick == null) { stick = ExternalDropping.getEmbeddedElData(el, 'stick', true) }
 
     // massage into correct data types
-    startTime = startTime != null ? moment.duration(startTime) : null
-    duration = duration != null ? moment.duration(duration) : null
+    startTime = startTime != null ? createDuration(startTime) : null
+    duration = duration != null ? createDuration(duration) : null
     stick = Boolean(stick)
 
     return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }
@@ -205,33 +205,28 @@ export default class ExternalDropping extends Interaction {
   // Assumes both footprints are non-open-ended.
   computeExternalDrop(componentFootprint, meta) {
     let calendar = this.view.calendar
-    let start = momentExt.utc(componentFootprint.unzonedRange.startMs).stripZone()
+    const dateEnv = calendar.dateEnv
+    let start = componentFootprint.unzonedRange.start
     let end
     let eventDef
 
     if (componentFootprint.isAllDay) {
+      start = startOfDay(start)
+
       // if dropped on an all-day span, and element's metadata specified a time, set it
       if (meta.startTime) {
-        start.time(meta.startTime)
-      } else {
-        start.stripTime()
+        start = dateEnv.add(start, meta.startTime)
       }
     }
 
     if (meta.duration) {
-      end = start.clone().add(meta.duration)
-    }
-
-    start = calendar.applyTimezone(start)
-
-    if (end) {
-      end = calendar.applyTimezone(end)
+      end = dateEnv.add(start, meta.duration)
     }
 
     eventDef = SingleEventDef.parse(
       assignTo({}, meta.eventProps, {
-        start: start,
-        end: end
+        start: dateEnv.toDate(start), // inefficient to convert back
+        end: dateEnv.toDate(end) // inefficient to convert back
       }),
       new EventSource(calendar)
     )

+ 47 - 24
src/component/renderers/EventRenderer.ts

@@ -1,9 +1,12 @@
+import View from '../../View'
+import { DateMarker } from '../../datelib/util'
+import { createFormatter, DateFormatter } from '../../datelib/formatting'
 import { htmlToElements } from '../../util/dom-manip'
 import { compareByFieldSpecs } from '../../util/misc'
 
 export default class EventRenderer {
 
-  view: any
+  view: View
   component: any
   fillRenderer: any // might remain null
 
@@ -11,9 +14,9 @@ export default class EventRenderer {
   bgSegs: any
 
   // derived from options
-  eventTimeFormat: any
-  displayEventTime: any
-  displayEventEnd: any
+  eventTimeFormat: DateFormatter
+  displayEventTime: boolean
+  displayEventEnd: boolean
 
 
   constructor(component, fillRenderer) { // fillRenderer is optional
@@ -33,10 +36,11 @@ export default class EventRenderer {
     let displayEventTime
     let displayEventEnd
 
-    this.eventTimeFormat =
+    this.eventTimeFormat = createFormatter(
       this.opt('eventTimeFormat') ||
       this.opt('timeFormat') || // deprecated
       this.computeEventTimeFormat()
+    )
 
     displayEventTime = this.opt('displayEventTime')
     if (displayEventTime == null) {
@@ -244,7 +248,7 @@ export default class EventRenderer {
   // Given an event and the default element used for rendering, returns the element that should actually be used.
   // Basically runs events and elements through the eventRender hook.
   filterEventRenderEl(eventFootprint, el) {
-    let legacy = eventFootprint.getEventLegacy()
+    let legacy = eventFootprint.getEventLegacy(this.view.calendar)
 
     let custom = this.view.publiclyTrigger('eventRender', {
       context: legacy,
@@ -264,22 +268,36 @@ export default class EventRenderer {
   // Compute the text that should be displayed on an event's element.
   // `range` can be the Event object itself, or something range-like, with at least a `start`.
   // If event times are disabled, or the event has no time, will return a blank string.
-  // If not specified, formatStr will default to the eventTimeFormat setting,
+  // If not specified, formatter will default to the eventTimeFormat setting,
   // and displayEnd will default to the displayEventEnd setting.
-  getTimeText(eventFootprint, formatStr?, displayEnd?) {
+  getTimeText(eventFootprint, formatter?, displayEnd?) {
+    let eventDateProfile = eventFootprint.eventInstance.dateProfile
+
     return this._getTimeText(
-      eventFootprint.eventInstance.dateProfile.start,
-      eventFootprint.eventInstance.dateProfile.end,
+      eventDateProfile.unzonedRange.start,
+      eventDateProfile.unzonedRange.end,
       eventFootprint.componentFootprint.isAllDay,
-      formatStr,
-      displayEnd
+      formatter,
+      displayEnd,
+      eventDateProfile.forcedStartTimeZoneOffset,
+      eventDateProfile.forcedEndTimeZoneOffset
     )
   }
 
 
-  _getTimeText(start, end, isAllDay, formatStr?, displayEnd?) {
-    if (formatStr == null) {
-      formatStr = this.eventTimeFormat
+  _getTimeText(
+    start: DateMarker,
+    end: DateMarker,
+    isAllDay,
+    formatter?,
+    displayEnd?,
+    forcedStartTimeZoneOffset?: number,
+    forcedEndTimeZoneOffset?: number
+) {
+    const dateEnv = this.view.calendar.dateEnv
+
+    if (formatter == null) {
+      formatter = this.eventTimeFormat
     }
 
     if (displayEnd == null) {
@@ -288,13 +306,14 @@ export default class EventRenderer {
 
     if (this.displayEventTime && !isAllDay) {
       if (displayEnd && end) {
-        return this.view.formatRange(
-          { start: start, end: end },
-          false, // allDay
-          formatStr
-        )
+        return dateEnv.toRangeFormat(start, end, formatter, {
+          forcedStartTimeZoneOffset,
+          forcedEndTimeZoneOffset
+        })
       } else {
-        return start.format(formatStr)
+        return dateEnv.toFormat(start, formatter, {
+          forcedTimeZoneOffset: forcedStartTimeZoneOffset
+        })
       }
     }
 
@@ -303,7 +322,11 @@ export default class EventRenderer {
 
 
   computeEventTimeFormat() {
-    return this.opt('smallTimeFormat')
+    return {
+      hour: 'numeric',
+      minute: '2-digit',
+      // TODO: remove :00
+    }
   }
 
 
@@ -433,8 +456,8 @@ export default class EventRenderer {
     let r1 = cf1.unzonedRange
     let r2 = cf2.unzonedRange
 
-    return r1.startMs - r2.startMs || // earlier events go first
-      (r2.endMs - r2.startMs) - (r1.endMs - r1.startMs) || // tie? longer events go first
+    return r1.start.valueOf() - r2.start.valueOf() || // earlier events go first
+      (r2.end.valueOf() - r2.start.valueOf()) - (r1.end.valueOf() - r1.start.valueOf()) || // tie? longer events go first
       cf2.isAllDay - cf1.isAllDay || // tie? put all-day events first (booleans cast to 0/1)
       compareByFieldSpecs(
         f1.eventDef,

+ 0 - 441
src/date-formatting.ts

@@ -1,441 +0,0 @@
-import { Moment } from 'moment'
-import {
-  default as momentExt,
-  newMomentProto,
-  oldMomentProto,
-  oldMomentFormat
-} from './moment-ext'
-
-
-// Plugin
-// -------------------------------------------------------------------------------------------------
-
-newMomentProto.format = function() {
-
-  if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
-    return formatDate(this, arguments[0]) // our extended formatting
-  }
-  if (this._ambigTime) {
-    return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD')
-  }
-  if (this._ambigZone) {
-    return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss')
-  }
-  if (this._fullCalendar) { // enhanced non-ambig moment?
-    // moment.format() doesn't ensure english, but we want to.
-    return oldMomentFormat(englishMoment(this))
-  }
-
-  return oldMomentProto.format.apply(this, arguments)
-}
-
-newMomentProto.toISOString = function() {
-
-  if (this._ambigTime) {
-    return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD')
-  }
-  if (this._ambigZone) {
-    return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss')
-  }
-  if (this._fullCalendar) { // enhanced non-ambig moment?
-    // depending on browser, moment might not output english. ensure english.
-    // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
-    return oldMomentProto.toISOString.apply(englishMoment(this), arguments)
-  }
-
-  return oldMomentProto.toISOString.apply(this, arguments)
-}
-
-function englishMoment(mom) {
-  if (mom.locale() !== 'en') {
-    return mom.clone().locale('en')
-  }
-  return mom
-}
-
-
-// Config
-// ---------------------------------------------------------------------------------------------------------------------
-
-/*
-Inserted between chunks in the fake ("intermediate") formatting string.
-Important that it passes as whitespace (\s) because moment often identifies non-standalone months
-via a regexp with an \s.
-*/
-let PART_SEPARATOR = '\u000b' // vertical tab
-
-/*
-Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text,
-but rather, a "special" token that has custom rendering (see specialTokens map).
-*/
-let SPECIAL_TOKEN_MARKER = '\u001f' // information separator 1
-
-/*
-Inserted at the beginning and end of a span of text that must have non-zero numeric characters.
-Handling of these markers is done in a post-processing step at the very end of text rendering.
-*/
-let MAYBE_MARKER = '\u001e' // information separator 2
-let MAYBE_REGEXP = new RegExp(MAYBE_MARKER + '([^' + MAYBE_MARKER + ']*)' + MAYBE_MARKER, 'g') // must be global
-
-/*
-Addition formatting tokens we want recognized
-*/
-let specialTokens = {
-  t: function(date) { // "a" or "p"
-    return oldMomentFormat(date, 'a').charAt(0)
-  },
-  T: function(date) { // "A" or "P"
-    return oldMomentFormat(date, 'A').charAt(0)
-  }
-}
-
-/*
-The first characters of formatting tokens for units that are 1 day or larger.
-`value` is for ranking relative size (lower means bigger).
-`unit` is a normalized unit, used for comparing moments.
-*/
-let largeTokenMap = {
-  Y: { value: 1, unit: 'year' },
-  M: { value: 2, unit: 'month' },
-  W: { value: 3, unit: 'week' }, // ISO week
-  w: { value: 3, unit: 'week' }, // local week
-  D: { value: 4, unit: 'day' }, // day of month
-  d: { value: 4, unit: 'day' } // day of week
-}
-
-
-// Single Date Formatting
-// ---------------------------------------------------------------------------------------------------------------------
-
-/*
-Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
-*/
-export function formatDate(date, formatStr) {
-  return renderFakeFormatString(
-    getParsedFormatString(formatStr).fakeFormatString,
-    date
-  )
-}
-
-
-// Date Range Formatting
-// -------------------------------------------------------------------------------------------------
-// TODO: make it work with timezone offset
-
-/*
-Using a formatting string meant for a single date, generate a range string, like
-"Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
-If the dates are the same as far as the format string is concerned, just return a single
-rendering of one date, without any separator.
-*/
-export function formatRange(date1: Moment, date2: Moment, formatStr, separator, isRTL) {
-  let localeData
-
-  date1 = momentExt.parseZone(date1)
-  date2 = momentExt.parseZone(date2)
-
-  localeData = date1.localeData()
-
-  // Expand localized format strings, like "LL" -> "MMMM D YYYY".
-  // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
-  // or non-zero areas in Moment's localized format strings.
-  formatStr = localeData.longDateFormat(formatStr) || formatStr
-
-  return renderParsedFormat(
-    getParsedFormatString(formatStr),
-    date1,
-    date2,
-    separator || ' - ',
-    isRTL
-  )
-}
-
-/*
-Renders a range with an already-parsed format string.
-*/
-function renderParsedFormat(parsedFormat, date1, date2, separator, isRTL) {
-  let sameUnits = parsedFormat.sameUnits
-  let unzonedDate1 = date1.clone().stripZone() // for same-unit comparisons
-  let unzonedDate2 = date2.clone().stripZone() // "
-
-  let renderedParts1 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date1)
-  let renderedParts2 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date2)
-
-  let leftI
-  let leftStr = ''
-  let rightI
-  let rightStr = ''
-  let middleI
-  let middleStr1 = ''
-  let middleStr2 = ''
-  let middleStr = ''
-
-  // Start at the leftmost side of the formatting string and continue until you hit a token
-  // that is not the same between dates.
-  for (
-    leftI = 0;
-    leftI < sameUnits.length && (!sameUnits[leftI] || unzonedDate1.isSame(unzonedDate2, sameUnits[leftI]));
-    leftI++
-  ) {
-    leftStr += renderedParts1[leftI]
-  }
-
-  // Similarly, start at the rightmost side of the formatting string and move left
-  for (
-    rightI = sameUnits.length - 1;
-    rightI > leftI && (!sameUnits[rightI] || unzonedDate1.isSame(unzonedDate2, sameUnits[rightI]));
-    rightI--
-  ) {
-    // If current chunk is on the boundary of unique date-content, and is a special-case
-    // date-formatting postfix character, then don't consume it. Consider it unique date-content.
-    // TODO: make configurable
-    if (rightI - 1 === leftI && renderedParts1[rightI] === '.') {
-      break
-    }
-
-    rightStr = renderedParts1[rightI] + rightStr
-  }
-
-  // The area in the middle is different for both of the dates.
-  // Collect them distinctly so we can jam them together later.
-  for (middleI = leftI; middleI <= rightI; middleI++) {
-    middleStr1 += renderedParts1[middleI]
-    middleStr2 += renderedParts2[middleI]
-  }
-
-  if (middleStr1 || middleStr2) {
-    if (isRTL) {
-      middleStr = middleStr2 + separator + middleStr1
-    } else {
-      middleStr = middleStr1 + separator + middleStr2
-    }
-  }
-
-  return processMaybeMarkers(
-    leftStr + middleStr + rightStr
-  )
-}
-
-
-// Format String Parsing
-// ---------------------------------------------------------------------------------------------------------------------
-
-let parsedFormatStrCache = {}
-
-/*
-Returns a parsed format string, leveraging a cache.
-*/
-function getParsedFormatString(formatStr) {
-  return parsedFormatStrCache[formatStr] ||
-    (parsedFormatStrCache[formatStr] = parseFormatString(formatStr))
-}
-
-/*
-Parses a format string into the following:
-- fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed.
-- sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"),
-  that indicates how similar a range's start & end must be in order to share the same formatted text.
-  If not a token, then the value is null.
-  Always a flat array (not nested liked "chunks").
-*/
-function parseFormatString(formatStr) {
-  let chunks = chunkFormatString(formatStr)
-
-  return {
-    fakeFormatString: buildFakeFormatString(chunks),
-    sameUnits: buildSameUnits(chunks)
-  }
-}
-
-/*
-Break the formatting string into an array of chunks.
-A 'maybe' chunk will have nested chunks.
-*/
-function chunkFormatString(formatStr) {
-  let chunks = []
-  let match
-
-  // TODO: more descrimination
-  // \4 is a backreference to the first character of a multi-character set.
-  let chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g
-
-  while ((match = chunker.exec(formatStr))) {
-    if (match[1]) { // a literal string inside [ ... ]
-      chunks.push.apply(chunks, // append
-        splitStringLiteral(match[1])
-      )
-    } else if (match[2]) { // non-zero formatting inside ( ... )
-      chunks.push({ maybe: chunkFormatString(match[2]) })
-    } else if (match[3]) { // a formatting token
-      chunks.push({ token: match[3] })
-    } else if (match[5]) { // an unenclosed literal string
-      chunks.push.apply(chunks, // append
-        splitStringLiteral(match[5])
-      )
-    }
-  }
-
-  return chunks
-}
-
-/*
-Potentially splits a literal-text string into multiple parts. For special cases.
-*/
-function splitStringLiteral(s) {
-  if (s === '. ') {
-    return [ '.', ' ' ] // for locales with periods bound to the end of each year/month/date
-  } else {
-    return [ s ]
-  }
-}
-
-/*
-Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control
-characters that will eventually be given to moment for formatting, and then post-processed.
-*/
-function buildFakeFormatString(chunks) {
-  let parts = []
-  let i
-  let chunk
-
-  for (i = 0; i < chunks.length; i++) {
-    chunk = chunks[i]
-
-    if (typeof chunk === 'string') {
-      parts.push('[' + chunk + ']')
-    } else if (chunk.token) {
-      if (chunk.token in specialTokens) {
-        parts.push(
-          SPECIAL_TOKEN_MARKER + // useful during post-processing
-          '[' + chunk.token + ']' // preserve as literal text
-        )
-      } else {
-        parts.push(chunk.token) // unprotected text implies a format string
-      }
-    } else if (chunk.maybe) {
-      parts.push(
-        MAYBE_MARKER + // useful during post-processing
-        buildFakeFormatString(chunk.maybe) +
-        MAYBE_MARKER
-      )
-    }
-  }
-
-  return parts.join(PART_SEPARATOR)
-}
-
-/*
-Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate
-in which regard two dates must be similar in order to share range formatting text.
-The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat.
-*/
-function buildSameUnits(chunks) {
-  let units = []
-  let i
-  let chunk
-  let tokenInfo
-
-  for (i = 0; i < chunks.length; i++) {
-    chunk = chunks[i]
-
-    if (chunk.token) {
-      tokenInfo = largeTokenMap[chunk.token.charAt(0)]
-      units.push(tokenInfo ? tokenInfo.unit : 'second') // default to a very strict same-second
-    } else if (chunk.maybe) {
-      units.push.apply(units, // append
-        buildSameUnits(chunk.maybe)
-      )
-    } else {
-      units.push(null)
-    }
-  }
-
-  return units
-}
-
-
-// Rendering to text
-// ---------------------------------------------------------------------------------------------------------------------
-
-/*
-Formats a date with a fake format string, post-processes the control characters, then returns.
-*/
-function renderFakeFormatString(fakeFormatString, date) {
-  return processMaybeMarkers(
-    renderFakeFormatStringParts(fakeFormatString, date).join('')
-  )
-}
-
-/*
-Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers.
-*/
-function renderFakeFormatStringParts(fakeFormatString, date) {
-  let parts = []
-  let fakeRender = oldMomentFormat(date, fakeFormatString)
-  let fakeParts = fakeRender.split(PART_SEPARATOR)
-  let i
-  let fakePart
-
-  for (i = 0; i < fakeParts.length; i++) {
-    fakePart = fakeParts[i]
-
-    if (fakePart.charAt(0) === SPECIAL_TOKEN_MARKER) {
-      parts.push(
-        // the literal string IS the token's name.
-        // call special token's registered function.
-        specialTokens[fakePart.substring(1)](date)
-      )
-    } else {
-      parts.push(fakePart)
-    }
-  }
-
-  return parts
-}
-
-/*
-Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string.
-*/
-function processMaybeMarkers(s) {
-  return s.replace(MAYBE_REGEXP, function(m0, m1) { // regex assumed to have 'g' flag
-    if (m1.match(/[1-9]/)) { // any non-zero numeric characters?
-      return m1
-    } else {
-      return ''
-    }
-  })
-}
-
-
-// Misc Utils
-// -------------------------------------------------------------------------------------------------
-
-/*
-Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
-*/
-export function queryMostGranularFormatUnit(formatStr) {
-  let chunks = chunkFormatString(formatStr)
-  let i
-  let chunk
-  let candidate
-  let best
-
-  for (i = 0; i < chunks.length; i++) {
-    chunk = chunks[i]
-
-    if (chunk.token) {
-      candidate = largeTokenMap[chunk.token.charAt(0)]
-      if (candidate) {
-        if (!best || candidate.value > best.value) {
-          best = candidate
-        }
-      }
-    }
-  }
-
-  if (best) {
-    return best.unit
-  }
-
-  return null
-}

+ 144 - 2
src/datelib/duration.ts

@@ -4,6 +4,8 @@ export interface DurationObjInput {
   year?: number
   months?: number
   month?: number
+  weeks?: number
+  week?: number
   days?: number
   day?: number
   hours?: number
@@ -26,11 +28,33 @@ export interface Duration {
 
 let re = /^(?:(\d+)\.)?(\d\d):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/
 
-export function createDuration(input) {
+export function createDuration(input, unit?: string) {
   if (typeof input === 'string') {
     return parseString(input)
   } else if (typeof input === 'object') {
     return normalizeObject(input)
+  } else if (typeof input === 'number') {
+    return {
+      year: (unit === 'year' || unit === 'years') ? input : 0,
+      month: (unit === 'month' || unit === 'months') ? input : 0,
+      day: (unit === 'day' || unit === 'days') ? input : 0,
+      time:
+        ((unit === 'hour' || unit === 'hours') ? input * 60 * 60 * 1000 : 0) +
+        ((unit === 'minute' || unit === 'minutes') ? input * 60 * 1000 : 0) +
+        ((unit === 'seconds' || unit === 'second') ? input * 1000 : 0) +
+        ((!unit || unit === 'millisecond' || unit === 'milliseconds') ? input : 0)
+    }
+  } else {
+    return null
+  }
+}
+
+export function addDurations(d0: Duration, d1: Duration) {
+  return {
+    year: d0.year + d1.year,
+    month: d0.month + d1.month,
+    day: d0.day + d1.day,
+    time: d0.time + d1.time
   }
 }
 
@@ -48,13 +72,16 @@ function parseString(s: string): Duration {
         (parseInt(m[5], 10) || 0) // ms
     }
   }
+  return null
 }
 
 function normalizeObject(obj: DurationObjInput): Duration {
   return {
     year: obj.years || obj.year || 0,
     month: obj.months || obj.month || 0,
-    day: obj.days || obj.day || 0,
+    day:
+      (obj.days || obj.day || 0) +
+      getWeeksFromInput(obj) * 7,
     time:
       (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours
       (obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes
@@ -63,9 +90,124 @@ function normalizeObject(obj: DurationObjInput): Duration {
   }
 }
 
+export function getWeeksFromInput(obj: DurationObjInput) {
+  return obj.weeks || obj.week || 0
+}
+
 export function durationsEqual(d0: Duration, d1: Duration): boolean {
   return d0.year === d1.year &&
     d0.month === d1.month &&
     d0.day === d1.day &&
     d0.time === d1.time
 }
+
+export function diffDurations(d0: Duration, d1: Duration): Duration {
+  return {
+    year: d1.year - d0.year,
+    month: d1.month - d0.month,
+    day: d1.day - d0.day,
+    time: d1.time - d0.time
+  }
+}
+
+export function wholeDivideDurationByDuration(numerator: Duration, denominator: Duration): number {
+  let res0 = null
+  let res1 = null
+
+  if (denominator.year) {
+    res0 = numerator.year / denominator.year
+  }
+
+  if (denominator.month) {
+    res1 = numerator.month / denominator.month
+
+    if (res0 === null || res0 === res1) {
+      res0 = res1
+    } else {
+      return null
+    }
+  }
+
+  if (denominator.day) {
+    res1 = numerator.day / denominator.day
+
+    if (res0 === null || res0 === res1) {
+      res0 = res1
+    } else {
+      return null
+    }
+  }
+
+  if (denominator.time) {
+    res1 = numerator.time / denominator.time
+
+    if (res0 === null || res0 === res1) {
+      res0 = res1
+    } else {
+      return null
+    }
+  }
+
+  return res0
+}
+
+export function isSingleDay(dur: Duration) {
+  return dur.year === 0 && dur.month === 0 && dur.day === 1 && dur.time === 0
+}
+
+
+export function computeGreatestUnit(dur: Duration) { // can return null?
+  if (dur.time % 1000 !== 0) {
+    return 'millisecond'
+  }
+  if (dur.time % 1000 * 60 !== 0) {
+    return 'second'
+  }
+  if (dur.time % 1000 * 60 * 60 !== 0) {
+    return 'minute'
+  }
+  if (dur.time) { // correct???
+    return 'hour'
+  }
+  if (dur.day) {
+    return 'day'
+  }
+  if (dur.month) {
+    return 'month'
+  }
+  if (dur.year) {
+    return 'year'
+  }
+}
+
+
+export function multiplyDuration(dur: Duration, n: number) {
+  return {
+    year: dur.year * n,
+    month: dur.month * n,
+    day: dur.day * n,
+    time: dur.time * n
+  }
+}
+
+
+const MS_IN_DAY = 864e5
+
+export function asRoughDays(dur: Duration) { // TODO: use asRoughMs
+  return dur.year * 365 + dur.month * 30 + dur.day + dur.time / MS_IN_DAY
+}
+
+export function asRoughMs(dur: Duration) {
+  return dur.year * (365 * MS_IN_DAY) +
+    dur.month * (30 * MS_IN_DAY) +
+    dur.day * MS_IN_DAY +
+    dur.time
+}
+
+export function asRoughMinutes(dur: Duration) {
+  return asRoughMs(dur) / 1000 / 60
+}
+
+export function asRoughSeconds(dur: Duration) {
+  return asRoughMs(dur) / 1000
+}

+ 201 - 24
src/datelib/env.ts

@@ -1,4 +1,4 @@
-import { DateMarker, arrayToUtcDate, dateToUtcArray, arrayToLocalDate, dateToLocalArray  } from './util'
+import { DateMarker, arrayToUtcDate, dateToUtcArray, arrayToLocalDate, dateToLocalArray, startOfHour, startOfMinute, startOfSecond, addMs  } from './util'
 import { CalendarSystem, createCalendarSystem } from './calendar-system'
 import { namedTimeZoneOffsetGenerator, getNamedTimeZoneOffsetGenerator } from './timezone'
 import { getLocale } from './locale'
@@ -11,12 +11,16 @@ export interface DateEnvSettings {
   timeZone: string
   timeZoneImpl?: string
   calendarSystem: string
-  locale: string
+  locale: string // TODO: accept a list
   weekNumberCalculation?: any
   firstDay?: any
 }
 
 
+
+export type DateInput = Date | number[] | number | string
+
+
 const MS_IN_DAY = 864e5
 
 // TODO: locale: 'auto'
@@ -31,6 +35,7 @@ export class DateEnv {
   locale: string
   weekMeta: any
   weekNumberFunc: any
+  simpleNumberFormat: Intl.NumberFormat
 
   constructor(settings: DateEnvSettings) {
     this.timeZone = settings.timeZone
@@ -39,6 +44,8 @@ export class DateEnv {
     this.locale = settings.locale
     this.weekMeta = assignTo({}, getLocale(settings.locale).week)
 
+    this.simpleNumberFormat = new Intl.NumberFormat(settings.locale)
+
     if (settings.weekNumberCalculation === 'ISO') {
       this.weekMeta.dow = 1
       this.weekMeta.doy = 4
@@ -65,6 +72,56 @@ export class DateEnv {
     ])
   }
 
+  getMonth(marker: DateMarker): number {
+   return this.calendarSystem.getMarkerMonth(marker)
+  }
+
+  subtract(marker: DateMarker, dur: Duration): DateMarker {
+    let { calendarSystem } = this
+
+    return calendarSystem.arrayToMarker([
+      calendarSystem.getMarkerYear(marker) - dur.year,
+      calendarSystem.getMarkerMonth(marker) - dur.month,
+      calendarSystem.getMarkerDay(marker) - dur.day,
+      marker.getUTCHours(),
+      marker.getUTCMinutes(),
+      marker.getUTCSeconds(),
+      marker.getUTCMilliseconds() - dur.time
+    ])
+  }
+
+  addYears(marker: DateMarker, n: number): DateMarker {
+    let { calendarSystem } = this
+
+    return calendarSystem.arrayToMarker([
+      calendarSystem.getMarkerYear(marker) + n,
+      calendarSystem.getMarkerMonth(marker),
+      calendarSystem.getMarkerDay(marker),
+      marker.getUTCHours(),
+      marker.getUTCMinutes(),
+      marker.getUTCSeconds(),
+      marker.getUTCMilliseconds()
+    ])
+  }
+
+  startOf(marker: DateMarker, unit: string) {
+    if (unit === 'year') {
+      return this.startOfYear(marker)
+    } else if (unit === 'month') {
+      return this.startOfMonth(marker)
+    } else if (unit === 'week') {
+      return this.startOfWeek(marker)
+    } else if (unit === 'day') {
+      return this.startOfDay(marker)
+    } else if (unit === 'hour') {
+      return startOfHour(marker)
+    } else if (unit === 'minute') {
+      return startOfMinute(marker)
+    } else if (unit === 'second') {
+      return startOfSecond(marker)
+    }
+  }
+
   startOfYear(marker: DateMarker): DateMarker {
     let { calendarSystem } = this
 
@@ -99,6 +156,19 @@ export class DateEnv {
     }
   }
 
+  // TODO: make i18n friendly
+  // use weekNumberTitle?
+  formatWeek(marker: DateMarker, includeLabel: boolean = false): string {
+    let w = this.computeWeekNumber(marker)
+    let s = this.simpleNumberFormat.format(w)
+
+    if (includeLabel) {
+      s = 'Wk ' + s
+    }
+
+    return s
+  }
+
   toDate(marker: DateMarker): Date {
     if (this.timeZone === 'UTC' || !this.canComputeTimeZoneOffset()) {
       return new Date(marker.valueOf())
@@ -135,21 +205,27 @@ export class DateEnv {
     }
   }
 
-  toRangeFormat(start: DateMarker, end: DateMarker, formatter: DateFormatter, extraOptions: any = {}) {
+  toRangeFormat(start: DateMarker, end: DateMarker, formatter: DateFormatter, dateOptions: any = {}) {
+
+    // yuck
+    if (dateOptions.isExclusive) {
+      end = addMs(end, -1)
+    }
+
     return formatter.format(
       {
         marker: start,
-        timeZoneOffset: extraOptions.forcedStartTimeZoneOffset != null ?
-          extraOptions.forcedStartTimeZoneOffset :
+        timeZoneOffset: dateOptions.forcedStartTimeZoneOffset != null ?
+          dateOptions.forcedStartTimeZoneOffset :
           this.computeTimeZoneOffset(start)
       },
       {
         marker: end,
-        timeZoneOffset: extraOptions.forcedEndTimeZoneOffset != null ?
-          extraOptions.forcedEndTimeZoneOffset :
+        timeZoneOffset: dateOptions.forcedEndTimeZoneOffset != null ?
+          dateOptions.forcedEndTimeZoneOffset :
           this.computeTimeZoneOffset(end)
       },
-      this
+      this // yuck
     )
   }
 
@@ -171,24 +247,25 @@ export class DateEnv {
       marker,
       extraOptions.forcedTimeZoneOffset != null ?
         extraOptions.forcedTimeZoneOffset :
-        this.computeTimeZoneOffset(marker)
+        this.computeTimeZoneOffset(marker),
+      extraOptions.omitTime
     )
   }
 
-  createMarker(input) {
+  createMarker(input: DateInput) {
     return this.createMarkerMeta(input).marker
   }
 
   // returns an object that wraps the marker!
-  createMarkerMeta(input) {
+  createMarkerMeta(input: DateInput) {
     if (typeof input === 'string') {
       return this.parse(input)
     } else if (typeof input === 'number') {
-      return { marker: this.timestampToMarker(input), hasTime: false, forcedTimeZoneOffset: null }
+      return { marker: this.timestampToMarker(input), isTimeUnspecified: false, forcedTimeZoneOffset: null }
     } else if (isNativeDate(input)) {
-      return { marker: this.dateToMarker(input), hasTime: false, forcedTimeZoneOffset: null }
+      return { marker: this.dateToMarker(input as Date), isTimeUnspecified: false, forcedTimeZoneOffset: null }
     } else if (Array.isArray(input)) {
-      return { marker: arrayToUtcDate(input), hasTime: false, forcedTimeZoneOffset: null }
+      return { marker: arrayToUtcDate(input), isTimeUnspecified: false, forcedTimeZoneOffset: null }
     }
     return null
   }
@@ -206,7 +283,7 @@ export class DateEnv {
       }
     }
 
-    return { marker, hasTime: parts.hasTime, forcedTimeZoneOffset } // TODO: timeNotSpecified
+    return { marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTimeZoneOffset }
   }
 
   dateToMarker(date: Date) {
@@ -223,6 +300,63 @@ export class DateEnv {
     }
   }
 
+  computeGreatestDenominator(m0: DateMarker, m1: DateMarker) {
+    let n = this.diffWholeYears(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'year', value: n }
+    }
+
+    n = this.diffWholeMonths(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'month', value: n }
+    }
+
+    n = this.diffWholeWeeks(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'week', value: n / 7 }
+    }
+
+    n = this.diffWholeDays(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'day', value: n }
+    }
+
+    n = diffHours(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'hour', value: n }
+    }
+
+    n = diffMinutes(m0, m1)
+
+    if (n !== null) {
+      return  { unit: 'minute', value: n }
+    }
+
+    n = diffSeconds(m0, m1)
+
+    if (n !== null) {
+      return { unit: 'second', value: n }
+    }
+
+    return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() }
+  }
+
+  divideRangeByWholeDuration(m0: DateMarker, m1: DateMarker, d: Duration) {
+    let cnt = 0
+
+    while (m0 < m1) { // not optimal
+      m0 = this.add(m0, d)
+      cnt++
+    }
+
+    return cnt
+  }
+
   diffWholeYears(m0: DateMarker, m1: DateMarker): number {
     let { calendarSystem } = this
 
@@ -249,17 +383,17 @@ export class DateEnv {
       m0.getUTCHours() === m1.getUTCHours() &&
       calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)
     ) {
-      return calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0) +
-       (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12
+      return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
+          (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12
     }
     return null
   }
 
   diffWholeWeeks(m0: DateMarker, m1: DateMarker): number {
-    let days = this.diffWholeDays(m0, m1)
+    let d = this.diffWholeDays(m0, m1)
 
-    if (days !== null && days % 7 === 0) {
-      return days / 7
+    if (d !== null && d % 7 === 0) {
+      return d / 7
     }
 
     return null
@@ -272,7 +406,7 @@ export class DateEnv {
       m0.getUTCMinutes() === m1.getUTCMinutes() &&
       m0.getUTCHours() === m1.getUTCHours()
     ) {
-      return diffDays(m0, m1)
+      return Math.round(diffDays(m0, m1))
     }
     return null
   }
@@ -316,8 +450,9 @@ function weekOfYear(marker, dow, doy) {
 function weekOfGivenYear(marker, year, dow, doy) {
   let firstWeekStart = arrayToUtcDate([ year, 0, 1 + firstWeekOffset(year, dow, doy) ])
   let dayStart = startOfDay(marker)
+  let days = Math.round(diffDays(firstWeekStart, dayStart))
 
-  return Math.floor(diffDays(firstWeekStart, dayStart) / 7) + 1 // zero-indexed
+  return Math.floor(days / 7) + 1 // zero-indexed
 }
 
 
@@ -330,8 +465,12 @@ function startOfDay(marker: DateMarker): DateMarker {
 }
 
 
-function diffDays(m0, m1) { // will round
-  return Math.round((m1.valueOf() - m0.valueOf()) / MS_IN_DAY)
+export function diffDays(m0, m1) { // will give float
+  return (m1.valueOf() - m0.valueOf()) / MS_IN_DAY
+}
+
+export function diffWeeks(m0, m1) { // will give float
+  return Math.round(diffDays(m0, m1)) / 7
 }
 
 
@@ -348,3 +487,41 @@ function firstWeekOffset(year, dow, doy) {
 function isNativeDate(input) {
   return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date
 }
+
+
+
+const MS_IN_HOUR = 1000 * 60 * 60
+const MS_IN_MINUTE = 1000 * 60
+
+
+function diffHours(m0, m1) {
+  let ms = m1.valueOf() - m0.valueOf()
+
+  if (ms % MS_IN_HOUR === 0) {
+    return ms / MS_IN_HOUR
+  }
+
+  return null
+}
+
+
+function diffMinutes(m0, m1) {
+  let ms = m1.valueOf() - m0.valueOf()
+
+  if (ms % MS_IN_MINUTE === 0) {
+    return ms / MS_IN_MINUTE
+  }
+
+  return null
+}
+
+
+function diffSeconds(m0, m1) {
+  let ms = m1.valueOf() - m0.valueOf()
+
+  if (ms % 1000 === 0) {
+    return ms / 1000
+  }
+
+  return null
+}

+ 8 - 7
src/datelib/formatting.ts

@@ -361,15 +361,16 @@ function pad(n) {
 }
 
 
-export function buildIsoString(marker: DateMarker, timeZoneOffset?: number) {
-  let s = marker.toISOString().replace('.000', '')
+export function buildIsoString(marker: DateMarker, timeZoneOffset?: number, stripZeroTime: boolean = false) {
+  let s = marker.toISOString()
 
-  if (timeZoneOffset !== 0) {
-    s = s.replace('Z', '')
+  s = s.replace('.000', '')
+  s = s.replace('Z', '')
 
-    if (timeZoneOffset != null) {
-      s += formatIsoTimeZoneOffset(timeZoneOffset)
-    }
+  if (timeZoneOffset != null) { // provided?
+    s += formatIsoTimeZoneOffset(timeZoneOffset)
+  } else if (stripZeroTime) {
+    s = s.replace('T00:00:00', '')
   }
 
   return s

+ 6 - 7
src/datelib/parsing.ts

@@ -4,13 +4,15 @@ const ISO_TZO_RE = /(?:(Z)|([-+])(\d\d)(?::(\d\d))?)$/
 
 export function parse(str) {
   let timeZoneOffset = null
-  let hasTime = false
+  let isTimeUnspecified = false
   let m = ISO_START.exec(str)
 
   if (m) {
-    hasTime = Boolean(m[1])
+    isTimeUnspecified = !m[1]
 
-    if (hasTime) {
+    if (isTimeUnspecified) {
+      str += 'T00:00:00Z'
+    } else {
       str = str.replace(ISO_TZO_RE, function(whole, z, sign, minutes, seconds) {
         if (z) {
           timeZoneOffset = 0
@@ -22,15 +24,12 @@ export function parse(str) {
         }
         return ''
       }) + 'Z' // otherwise will parse in local
-
-    } else {
-      str += 'T00:00:00Z'
     }
   }
 
   return {
     marker: new Date(str),
-    hasTime,
+    isTimeUnspecified,
     timeZoneOffset
   }
 }

+ 107 - 0
src/datelib/util.ts

@@ -1,3 +1,4 @@
+import { Duration } from './duration'
 
 export type DateMarker = Date
 
@@ -6,6 +7,10 @@ export function nowMarker(): DateMarker {
 }
 
 
+export const dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]
+export const unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ] // descending
+
+
 // export function markersEqual(m0: DateMarker, m1)
 
 
@@ -50,3 +55,105 @@ export function dateToUtcArray(date) {
 export function arrayToUtcDate(arr) {
   return new Date(Date.UTC.apply(Date, arr))
 }
+
+
+export function addWeeks(m: DateMarker, n: number) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate() + n * 7,
+    m.getUTCHours(),
+    m.getUTCMinutes(),
+    m.getUTCSeconds(),
+    m.getUTCMilliseconds()
+  ))
+}
+
+
+export function addDays(m: DateMarker, n: number) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate() + n,
+    m.getUTCHours(),
+    m.getUTCMinutes(),
+    m.getUTCSeconds(),
+    m.getUTCMilliseconds()
+  ))
+}
+
+export function addMs(m: DateMarker, n: number) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours(),
+    m.getUTCMinutes(),
+    m.getUTCSeconds(),
+    m.getUTCMilliseconds() + n
+  ))
+}
+
+export function startOfDay(m: DateMarker) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate()
+  ))
+}
+
+export function startOfHour(m: DateMarker) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours()
+  ))
+}
+
+export function startOfMinute(m: DateMarker) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours(),
+    m.getUTCMinutes()
+  ))
+}
+
+export function startOfSecond(m: DateMarker) {
+  return new Date(Date.UTC(
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours(),
+    m.getUTCMinutes(),
+    m.getUTCSeconds()
+  ))
+}
+
+
+const MS_IN_HOUR = 1000 * 60 * 60
+const MS_IN_MINUTE = 1000 * 60
+
+export function computeGreatestDurationDenominator(dur: Duration, considerWeeks: boolean = false) {
+  if (dur.year) {
+    return { unit: 'year', value: dur.year }
+  } else if (dur.month) {
+    return { unit: 'month', value: dur.month }
+  } else if (considerWeeks && dur.day && dur.day % 7 === 0) {
+    return { unit: 'week', value: dur.day / 7 }
+  } else if (dur.day) {
+    return { unit: 'day', value: dur.day }
+  } else if (dur.time) {
+    if (dur.time % MS_IN_HOUR === 0) {
+      return { unit: 'hour', value: dur.time / MS_IN_HOUR }
+    } else if (dur.time % MS_IN_MINUTE === 0) {
+      return { unit: 'minute', value: dur.time / MS_IN_MINUTE }
+    } else if (dur.time % 1000 === 0) {
+      return { unit: 'second', value: dur.time / 1000 }
+    } else {
+      return { unit: 'millisecond', value: dur.time }
+    }
+  }
+}

+ 5 - 19
src/exports.ts

@@ -43,14 +43,6 @@ export {
   assignTo
 } from './util/object'
 
-export {
-  computeGreatestUnit,
-  divideRangeByDuration,
-  divideDurationByDuration,
-  multiplyDuration,
-  durationHasTime
-} from './util/date'
-
 export {
   findElements,
   findChildren,
@@ -79,17 +71,9 @@ export {
 } from './util/dom-geom'
 
 export {
-  formatDate,
-  formatRange,
-  queryMostGranularFormatUnit
-} from './date-formatting'
-
-export {
-  datepickerLocale,
   locale
 } from './locale'
 
-export { default as moment } from './moment-ext'
 export { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
 export { default as ListenerMixin, ListenerInterface } from './common/ListenerMixin'
 export { default as Model } from './common/Model'
@@ -138,7 +122,9 @@ export { default as BasicView } from './basic/BasicView'
 export { default as MonthView } from './basic/MonthView'
 export { default as ListView } from './list/ListView'
 
+export { DateMarker, addDays, startOfDay, addMs } from './datelib/util'
 export { DateEnv } from './datelib/env'
-export { createDuration, durationsEqual } from './datelib/duration'
-export { nowMarker } from './datelib/util' // might not need
-export { createFormatter } from './datelib/formatting'
+export {
+  wholeDivideDurationByDuration, isSingleDay, createDuration, multiplyDuration,
+  asRoughMinutes, asRoughSeconds
+} from './datelib/duration'

+ 6 - 3
src/list/ListEventRenderer.ts

@@ -33,8 +33,8 @@ export default class ListEventRenderer extends EventRenderer {
     } else if (view.isMultiDayRange(componentFootprint.unzonedRange)) {
       if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
         timeHtml = htmlEscape(this._getTimeText(
-          calendar.msToMoment(seg.startMs),
-          calendar.msToMoment(seg.endMs),
+          seg.start,
+          seg.end,
           componentFootprint.isAllDay
         ))
       } else { // inner segment that lasts the whole day
@@ -73,7 +73,10 @@ export default class ListEventRenderer extends EventRenderer {
 
   // like "4:00am"
   computeEventTimeFormat() {
-    return this.opt('mediumTimeFormat')
+    return {
+      hour: 'numeric',
+      minute: '2-digit'
+    }
   }
 
 }

+ 24 - 16
src/list/ListView.ts

@@ -6,6 +6,8 @@ import View from '../View'
 import Scroller from '../common/Scroller'
 import ListEventRenderer from './ListEventRenderer'
 import ListEventPointing from './ListEventPointing'
+import { addDays, DateMarker } from '../datelib/util'
+import { createFormatter } from '../datelib/formatting'
 
 /*
 Responsible for the scroller, and forwarding event-related actions into the "grid".
@@ -21,8 +23,8 @@ export default class ListView extends View {
   scroller: Scroller
   contentEl: HTMLElement
 
-  dayDates: any // localized ambig-time moment array
-  dayRanges: any // UnzonedRange[], of start-end of each day
+  dayDates: DateMarker[]
+  dayRanges: UnzonedRange[] // start/end of each day
 
 
   constructor(calendar, viewSpec) {
@@ -74,21 +76,21 @@ export default class ListView extends View {
 
   renderDates(dateProfile) {
     let calendar = this.calendar
-    let dayStart = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs, true)
-    let viewEnd = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.endMs, true)
+    let dayStart = calendar.dateEnv.startOfDay(dateProfile.renderUnzonedRange.start)
+    let viewEnd = dateProfile.renderUnzonedRange.end
     let dayDates = []
     let dayRanges = []
 
     while (dayStart < viewEnd) {
 
-      dayDates.push(dayStart.clone())
+      dayDates.push(dayStart)
 
       dayRanges.push(new UnzonedRange(
         dayStart,
-        dayStart.clone().add(1, 'day')
+        addDays(dayStart, 1)
       ))
 
-      dayStart.add(1, 'day')
+      dayStart = addDays(dayStart, 1)
     }
 
     this.dayDates = dayDates
@@ -100,6 +102,7 @@ export default class ListView extends View {
 
   // slices by day
   componentFootprintToSegs(footprint) {
+    const dateEnv = this.calendar.dateEnv
     let dayRanges = this.dayRanges
     let dayIndex
     let segRange
@@ -111,8 +114,8 @@ export default class ListView extends View {
 
       if (segRange) {
         seg = {
-          startMs: segRange.startMs,
-          endMs: segRange.endMs,
+          start: segRange.start,
+          end: segRange.end,
           isStart: segRange.isStart,
           isEnd: segRange.isEnd,
           dayIndex: dayIndex
@@ -125,9 +128,13 @@ export default class ListView extends View {
         if (
           !seg.isEnd && !footprint.isAllDay &&
           dayIndex + 1 < dayRanges.length &&
-          footprint.unzonedRange.endMs < dayRanges[dayIndex + 1].startMs + this.nextDayThreshold
+          footprint.unzonedRange.end <
+            dateEnv.add(
+              dayRanges[dayIndex + 1].start,
+              this.nextDayThreshold
+            )
         ) {
-          seg.endMs = footprint.unzonedRange.endMs
+          seg.end = footprint.unzonedRange.end
           seg.isEnd = true
           break
         }
@@ -198,12 +205,13 @@ export default class ListView extends View {
 
   // generates the HTML for the day headers that live amongst the event rows
   buildDayHeaderRow(dayDate) {
-    let mainFormat = this.opt('listDayFormat')
-    let altFormat = this.opt('listDayAltFormat')
+    const dateEnv = this.calendar.dateEnv
+    let mainFormat = createFormatter(this.opt('listDayFormat')) // TODO: cache
+    let altFormat = createFormatter(this.opt('listDayAltFormat')) // TODO: cache
 
     return createElement('tr', {
       className: 'fc-list-heading',
-      'data-date': dayDate.format('YYYY-MM-DD')
+      'data-date': dateEnv.toIso(dayDate, { omitTime: true })
     }, '<td class="' + (
       this.calendar.theme.getClass('tableListHeading') ||
       this.calendar.theme.getClass('widgetHeader')
@@ -212,14 +220,14 @@ export default class ListView extends View {
         this.buildGotoAnchorHtml(
           dayDate,
           { 'class': 'fc-list-heading-main' },
-          htmlEscape(dayDate.format(mainFormat)) // inner HTML
+          htmlEscape(dateEnv.toFormat(dayDate, mainFormat)) // inner HTML
         ) :
         '') +
       (altFormat ?
         this.buildGotoAnchorHtml(
           dayDate,
           { 'class': 'fc-list-heading-alt' },
-          htmlEscape(dayDate.format(altFormat)) // inner HTML
+          htmlEscape(dateEnv.toFormat(dayDate, altFormat)) // inner HTML
         ) :
         '') +
     '</td>') as HTMLTableRowElement

+ 6 - 6
src/list/config.ts

@@ -6,7 +6,7 @@ defineView('list', {
   buttonTextKey: 'list', // what to lookup in locale files
   defaults: {
     buttonText: 'list', // text to display for English
-    listDayFormat: 'LL', // like "January 1, 2016"
+    listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' }, // like "January 1, 2016"
     noEventsMessage: 'No events to display'
   }
 })
@@ -15,7 +15,7 @@ defineView('listDay', {
   type: 'list',
   duration: { days: 1 },
   defaults: {
-    listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header
+    listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header
   }
 })
 
@@ -23,8 +23,8 @@ defineView('listWeek', {
   type: 'list',
   duration: { weeks: 1 },
   defaults: {
-    listDayFormat: 'dddd', // day-of-week is more important
-    listDayAltFormat: 'LL'
+    listDayFormat: { weekday: 'long' }, // day-of-week is more important
+    listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' }
   }
 })
 
@@ -32,7 +32,7 @@ defineView('listMonth', {
   type: 'list',
   duration: { month: 1 },
   defaults: {
-    listDayAltFormat: 'dddd' // day-of-week is nice-to-have
+    listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
   }
 })
 
@@ -40,6 +40,6 @@ defineView('listYear', {
   type: 'list',
   duration: { year: 1 },
   defaults: {
-    listDayAltFormat: 'dddd' // day-of-week is nice-to-have
+    listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
   }
 })

+ 2 - 180
src/locale.ts

@@ -1,174 +1,13 @@
-import * as moment from 'moment'
 import * as exportHooks from './exports'
-import { mergeOptions, globalDefaults, englishDefaults } from './options'
-import { stripHtmlEntities } from './util/html'
+import { mergeOptions, globalDefaults } from './options'
 
 export const localeOptionHash = {};
 (exportHooks as any).locales = localeOptionHash
 
 
-// NOTE: can't guarantee any of these computations will run because not every locale has datepicker
-// configs, so make sure there are English fallbacks for these in the defaults file.
-const dpComputableOptions = {
-
-  buttonText: function(dpOptions) {
-    return {
-      // the translations sometimes wrongly contain HTML entities
-      prev: stripHtmlEntities(dpOptions.prevText),
-      next: stripHtmlEntities(dpOptions.nextText),
-      today: stripHtmlEntities(dpOptions.currentText)
-    }
-  },
-
-  // Produces format strings like "MMMM YYYY" -> "September 2014"
-  monthYearFormat: function(dpOptions) {
-    return dpOptions.showMonthAfterYear ?
-      'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
-      'MMMM YYYY[' + dpOptions.yearSuffix + ']'
-  }
-
-}
-
-
-const momComputableOptions = {
-
-  // Produces format strings like "ddd M/D" -> "Fri 9/15"
-  dayOfMonthFormat: function(momOptions, fcOptions) {
-    let format = momOptions.longDateFormat('l') // for the format like "M/D/YYYY"
-
-    // strip the year off the edge, as well as other misc non-whitespace chars
-    format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '')
-
-    if (fcOptions.isRTL) {
-      format += ' ddd' // for RTL, add day-of-week to end
-    } else {
-      format = 'ddd ' + format // for LTR, add day-of-week to beginning
-    }
-    return format
-  },
-
-  // Produces format strings like "h:mma" -> "6:00pm"
-  mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
-    return momOptions.longDateFormat('LT')
-      .replace(/\s*a$/i, 'a') // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
-  },
-
-  // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
-  smallTimeFormat: function(momOptions) {
-    return momOptions.longDateFormat('LT')
-      .replace(':mm', '(:mm)')
-      .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
-      .replace(/\s*a$/i, 'a') // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
-  },
-
-  // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
-  extraSmallTimeFormat: function(momOptions) {
-    return momOptions.longDateFormat('LT')
-      .replace(':mm', '(:mm)')
-      .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales
-      .replace(/\s*a$/i, 't') // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
-  },
-
-  // Produces format strings like "ha" / "H" -> "6pm" / "18"
-  hourFormat: function(momOptions) {
-    return momOptions.longDateFormat('LT')
-      .replace(':mm', '')
-      .replace(/(\Wmm)$/, '') // like above, but for foreign locales
-      .replace(/\s*a$/i, 'a') // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
-  },
-
-  // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
-  noMeridiemTimeFormat: function(momOptions) {
-    return momOptions.longDateFormat('LT')
-      .replace(/\s*a$/i, '') // remove trailing AM/PM
-  }
-
-}
-
-
-// options that should be computed off live calendar options (considers override options)
-// TODO: best place for this? related to locale?
-// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it
-const instanceComputableOptions = {
-
-  // Produces format strings for results like "Mo 16"
-  smallDayDateFormat: function(options) {
-    return options.isRTL ?
-      'D dd' :
-      'dd D'
-  },
-
-  // Produces format strings for results like "Wk 5"
-  weekFormat: function(options) {
-    return options.isRTL ?
-      'w[ ' + options.weekNumberTitle + ']' :
-      '[' + options.weekNumberTitle + ' ]w'
-  },
-
-  // Produces format strings for results like "Wk5"
-  smallWeekFormat: function(options) {
-    return options.isRTL ?
-      'w[' + options.weekNumberTitle + ']' :
-      '[' + options.weekNumberTitle + ']w'
-  }
-
-}
-
-
-// TODO: make these computable properties in optionsManager
-export function populateInstanceComputableOptions(options) {
-  for (let name in instanceComputableOptions) {
-    let func = instanceComputableOptions[name]
-    if (options[name] == null) {
-      options[name] = func(options)
-    }
-  }
-}
-
-
-// Initialize jQuery UI datepicker translations while using some of the translations
-// Will set this as the default locales for datepicker.
-export function datepickerLocale(localeCode, dpLocaleCode, dpOptions) {
-
-  // get the FullCalendar internal option hash for this locale. create if necessary
-  let fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {})
-
-  // transfer some simple options from datepicker to fc
-  fcOptions.isRTL = dpOptions.isRTL
-  fcOptions.weekNumberTitle = dpOptions.weekHeader
-
-  // compute some more complex options from datepicker
-  for (let name in dpComputableOptions) {
-    let func = dpComputableOptions[name]
-    fcOptions[name] = func(dpOptions)
-  }
-
-  let jqDatePicker = window['jQuery'] && window['jQuery'].datepicker
-
-  // is jQuery UI Datepicker is on the page?
-  if (jqDatePicker) {
-
-    // Register the locale data.
-    // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker
-    // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt".
-    // Make an alias so the locale can be referenced either way.
-    jqDatePicker.regional[dpLocaleCode] =
-      jqDatePicker.regional[localeCode] = // alias
-        dpOptions
-
-    // Alias 'en' to the default locale data. Do this every time.
-    jqDatePicker.regional.en = jqDatePicker.regional['']
-
-    // Set as Datepicker's global defaults.
-    jqDatePicker.setDefaults(dpOptions)
-  }
-}
-
-
 // Sets FullCalendar-specific translations. Will set the locales as the global default.
 export function locale(localeCode, newFcOptions) {
   let fcOptions
-  let momOptions
 
   // get the FullCalendar internal option hash for this locale. create if necessary
   fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {})
@@ -178,28 +17,11 @@ export function locale(localeCode, newFcOptions) {
     fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ])
   }
 
-  // compute locale options that weren't defined.
-  // always do this. newFcOptions can be undefined when initializing from i18n file,
-  // so no way to tell if this is an initialization or a default-setting.
-  momOptions = getMomentLocaleData(localeCode) // will fall back to en
-  for (let name in momComputableOptions) {
-    let func = momComputableOptions[name]
-    if (fcOptions[name] == null) {
-      fcOptions[name] = (func)(momOptions, fcOptions)
-    }
-  }
-
   // set it as the default locale for FullCalendar
   globalDefaults.locale = localeCode
 }
 
 
-// Returns moment's internal locale data. If doesn't exist, returns English.
-export function getMomentLocaleData(localeCode) {
-  return moment.localeData(localeCode) || moment.localeData('en')
-}
-
-
 // Initialize English by forcing computation of moment-derived options.
 // Also, sets it as the default.
-locale('en', englishDefaults)
+locale('en', {})

+ 0 - 2
src/main.ts

@@ -1,8 +1,6 @@
 import * as exportHooks from './exports'
 
 // for intentional side-effects
-import './moment-ext'
-import './date-formatting'
 import './models/event-source/config'
 import './theme/config'
 import './basic/config'

+ 2 - 2
src/models/ComponentFootprint.ts

@@ -19,8 +19,8 @@ export default class ComponentFootprint {
   */
   toLegacy(calendar) {
     return {
-      start: calendar.msToMoment(this.unzonedRange.startMs, this.isAllDay),
-      end: calendar.msToMoment(this.unzonedRange.endMs, this.isAllDay)
+      start: calendar.dateEnv.toDate(this.unzonedRange.start),
+      end: calendar.dateEnv.toDate(this.unzonedRange.end)
     }
   }
 

+ 3 - 3
src/models/EventManager.ts

@@ -34,15 +34,15 @@ export default class EventManager {
   }
 
 
-  requestEvents(start, end, timezone, force, callback) {
+  requestEvents(start, end, dateEnv, force, callback) {
     if (
       force ||
       !this.currentPeriod ||
       !this.currentPeriod.isWithinRange(start, end) ||
-      timezone !== this.currentPeriod.timezone
+      dateEnv !== this.currentPeriod.dateEnv
     ) {
       this.setPeriod( // will change this.currentPeriod
-        new EventPeriod(start, end, timezone)
+        new EventPeriod(start, end, dateEnv)
       )
     }
 

+ 9 - 12
src/models/EventPeriod.ts

@@ -1,10 +1,10 @@
-import * as moment from 'moment'
 import { removeExact, removeMatching } from '../util/array'
 import { isEmptyObject } from '../util/object'
 import { default as EmitterMixin, EmitterInterface } from '../common/EmitterMixin'
 import UnzonedRange from './UnzonedRange'
 import EventInstanceGroup from './event/EventInstanceGroup'
-
+import { DateEnv } from '../datelib/env'
+import { DateMarker } from '../datelib/util'
 
 export default class EventPeriod {
 
@@ -15,9 +15,9 @@ export default class EventPeriod {
   triggerWith: EmitterInterface['triggerWith']
   hasHandlers: EmitterInterface['hasHandlers']
 
-  start: moment.Moment
-  end: moment.Moment
-  timezone: any
+  start: DateMarker
+  end: DateMarker
+  dateEnv: DateEnv
 
   unzonedRange: UnzonedRange
 
@@ -33,15 +33,12 @@ export default class EventPeriod {
   eventInstanceGroupsById: any
 
 
-  constructor(start, end, timezone) {
+  constructor(start: DateMarker, end: DateMarker, dateEnv: DateEnv) {
     this.start = start
     this.end = end
-    this.timezone = timezone
+    this.dateEnv = dateEnv
 
-    this.unzonedRange = new UnzonedRange(
-      start.clone().stripZone(),
-      end.clone().stripZone()
-    )
+    this.unzonedRange = new UnzonedRange(start, end)
 
     this.requestsByUid = {}
     this.eventDefsByUid = {}
@@ -77,7 +74,7 @@ export default class EventPeriod {
     this.requestsByUid[source.uid] = request
     this.pendingCnt += 1
 
-    source.fetch(this.start, this.end, this.timezone, (eventDefs) => {
+    source.fetch(this.start, this.end, this.dateEnv, (eventDefs) => {
       if (request.status !== 'cancelled') {
         request.status = 'completed'
         request.eventDefs = eventDefs

+ 53 - 96
src/models/UnzonedRange.ts

@@ -1,32 +1,23 @@
-import * as moment from 'moment'
-import momentExt from '../moment-ext'
+import { DateMarker } from '../datelib/util'
 
 export default class UnzonedRange {
 
-  startMs: number // if null, no start constraint
-  endMs: number // if null, no end constraint
+  start: DateMarker // if null, no start constraint
+  end: DateMarker // if null, no end constraint
 
   // TODO: move these into footprint.
-  // Especially, doesn't make sense for null startMs/endMs.
+  // Especially, doesn't make sense for null start/end
   isStart: boolean = true
   isEnd: boolean = true
 
-  constructor(startInput?, endInput?) {
+  constructor(start?: DateMarker, end?: DateMarker) {
 
-    if (moment.isMoment(startInput)) {
-      startInput = (startInput.clone() as any).stripZone()
+    if (start) {
+      this.start = start
     }
 
-    if (moment.isMoment(endInput)) {
-      endInput = (endInput.clone() as any).stripZone()
-    }
-
-    if (startInput) {
-      this.startMs = startInput.valueOf()
-    }
-
-    if (endInput) {
-      this.endMs = endInput.valueOf()
+    if (end) {
+      this.end = end
     }
   }
 
@@ -36,9 +27,9 @@ export default class UnzonedRange {
   Will return a new array result.
   Only works for non-open-ended ranges.
   */
-  static invertRanges(ranges, constraintRange) {
+  static invertRanges(ranges: UnzonedRange[], constraintRange: UnzonedRange) {
     let invertedRanges = []
-    let startMs = constraintRange.startMs // the end of the previous range. the start of the new range
+    let start = constraintRange.start // the end of the previous range. the start of the new range
     let i
     let dateRange
 
@@ -49,21 +40,21 @@ export default class UnzonedRange {
       dateRange = ranges[i]
 
       // add the span of time before the event (if there is any)
-      if (dateRange.startMs > startMs) { // compare millisecond time (skip any ambig logic)
+      if (dateRange.start > start) { // compare millisecond time (skip any ambig logic)
         invertedRanges.push(
-          new UnzonedRange(startMs, dateRange.startMs)
+          new UnzonedRange(start, dateRange.start)
         )
       }
 
-      if (dateRange.endMs > startMs) {
-        startMs = dateRange.endMs
+      if (dateRange.end > start) {
+        start = dateRange.end
       }
     }
 
     // add the span of time after the last event (if there is any)
-    if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic)
+    if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic)
       invertedRanges.push(
-        new UnzonedRange(startMs, constraintRange.endMs)
+        new UnzonedRange(start, constraintRange.end)
       )
     }
 
@@ -71,84 +62,80 @@ export default class UnzonedRange {
   }
 
 
-  intersect(otherRange) {
-    let startMs = this.startMs
-    let endMs = this.endMs
+  intersect(otherRange: UnzonedRange) {
+    let start = this.start
+    let end = this.end
     let newRange = null
 
-    if (otherRange.startMs != null) {
-      if (startMs == null) {
-        startMs = otherRange.startMs
+    if (otherRange.start != null) {
+      if (start === null) {
+        start = otherRange.start
       } else {
-        startMs = Math.max(startMs, otherRange.startMs)
+        start = new Date(Math.max(start.valueOf(), otherRange.start.valueOf()))
       }
     }
 
-    if (otherRange.endMs != null) {
-      if (endMs == null) {
-        endMs = otherRange.endMs
+    if (otherRange.end != null) {
+      if (end == null) {
+        end = otherRange.end
       } else {
-        endMs = Math.min(endMs, otherRange.endMs)
+        end = new Date(Math.min(end.valueOf(), otherRange.end.valueOf()))
       }
     }
 
-    if (startMs == null || endMs == null || startMs < endMs) {
-      newRange = new UnzonedRange(startMs, endMs)
-      newRange.isStart = this.isStart && startMs === this.startMs
-      newRange.isEnd = this.isEnd && endMs === this.endMs
+    if (start == null || end == null || start < end) {
+      newRange = new UnzonedRange(start, end)
+      newRange.isStart = this.isStart && start.valueOf() === this.start.valueOf()
+      newRange.isEnd = this.isEnd && end.valueOf() === this.end.valueOf()
     }
 
     return newRange
   }
 
 
-  intersectsWith(otherRange) {
-    return (this.endMs == null || otherRange.startMs == null || this.endMs > otherRange.startMs) &&
-      (this.startMs == null || otherRange.endMs == null || this.startMs < otherRange.endMs)
+  intersectsWith(otherRange: UnzonedRange) {
+    return (this.end == null || otherRange.start == null || this.end > otherRange.start) &&
+      (this.start == null || otherRange.end == null || this.start < otherRange.end)
   }
 
 
-  containsRange(innerRange) {
-    return (this.startMs == null || (innerRange.startMs != null && innerRange.startMs >= this.startMs)) &&
-      (this.endMs == null || (innerRange.endMs != null && innerRange.endMs <= this.endMs))
+  containsRange(innerRange: UnzonedRange) {
+    return (this.start == null || (innerRange.start != null && innerRange.start >= this.start)) &&
+      (this.end == null || (innerRange.end != null && innerRange.end <= this.end))
   }
 
 
-  // `date` can be a moment, a Date, or a millisecond time.
-  containsDate(date) {
-    let ms = date.valueOf()
-
-    return (this.startMs == null || ms >= this.startMs) &&
-      (this.endMs == null || ms < this.endMs)
+  // `date` can be a Date, or a millisecond time.
+  containsDate(date: Date) {
+    return (this.start == null || date >= this.start) &&
+      (this.end == null || date < this.end)
   }
 
 
   // If the given date is not within the given range, move it inside.
   // (If it's past the end, make it one millisecond before the end).
-  // `date` can be a moment, a Date, or a millisecond time.
-  // Returns a MS-time.
-  constrainDate(date) {
-    let ms = date.valueOf()
+  constrainDate(date: Date): Date {
 
-    if (this.startMs != null && ms < this.startMs) {
-      ms = this.startMs
+    if (this.start != null && date < this.start) {
+      return this.start
     }
 
-    if (this.endMs != null && ms >= this.endMs) {
-      ms = this.endMs - 1
+    if (this.end != null && date >= this.end) {
+      return new Date(this.end.valueOf() - 1)
     }
 
-    return ms
+    return date
   }
 
 
   equals(otherRange) {
-    return this.startMs === otherRange.startMs && this.endMs === otherRange.endMs
+    return (this.start == null ? null : this.start.valueOf()) === (otherRange.start == null ? null : otherRange.start.valueOf()) &&
+      (this.end == null ? null : this.end.valueOf()) === (otherRange.end == null ? null : otherRange.end.valueOf())
   }
 
 
   clone() {
-    let range = new UnzonedRange(this.startMs, this.endMs)
+    let range = new UnzonedRange(this.start, this.end)
 
     range.isStart = this.isStart
     range.isEnd = this.isEnd
@@ -156,42 +143,12 @@ export default class UnzonedRange {
     return range
   }
 
-
-  // Returns an ambig-zoned moment from startMs.
-  // BEWARE: returned moment is not localized.
-  // Formatting and start-of-week will be default.
-  getStart() {
-    if (this.startMs != null) {
-      return momentExt.utc(this.startMs).stripZone()
-    }
-    return null
-  }
-
-  // Returns an ambig-zoned moment from startMs.
-  // BEWARE: returned moment is not localized.
-  // Formatting and start-of-week will be default.
-  getEnd() {
-    if (this.endMs != null) {
-      return momentExt.utc(this.endMs).stripZone()
-    }
-    return null
-  }
-
-
-  as(unit) {
-    return moment.utc(this.endMs).diff(
-      moment.utc(this.startMs),
-      unit,
-      true
-    )
-  }
-
 }
 
 
 /*
 Only works for non-open-ended ranges.
 */
-function compareUnzonedRanges(range1, range2) {
-  return range1.startMs - range2.startMs // earlier ranges go first
+function compareUnzonedRanges(range1: UnzonedRange, range2: UnzonedRange) {
+  return range1.start.valueOf() - range2.start.valueOf() // earlier ranges go first
 }

+ 2 - 20
src/models/event-source/ArrayEventSource.ts

@@ -1,13 +1,11 @@
 import { removeMatching } from '../../util/array'
 import EventSource from './EventSource'
-import SingleEventDef from '../event/SingleEventDef'
 
 
 export default class ArrayEventSource extends EventSource {
 
   rawEventDefs: any // unparsed
   eventDefs: any
-  currentTimezone: any
 
 
   constructor(calendar) {
@@ -40,24 +38,8 @@ export default class ArrayEventSource extends EventSource {
   }
 
 
-  fetch(start, end, timezone, onSuccess, onFailure) {
-    let eventDefs = this.eventDefs
-    let i
-
-    if (
-      this.currentTimezone != null &&
-      this.currentTimezone !== timezone
-    ) {
-      for (i = 0; i < eventDefs.length; i++) {
-        if (eventDefs[i] instanceof SingleEventDef) {
-          eventDefs[i].rezone()
-        }
-      }
-    }
-
-    this.currentTimezone = timezone
-
-    onSuccess(eventDefs)
+  fetch(start, end, dateEnv, onSuccess, onFailure) {
+    onSuccess(this.eventDefs)
   }
 
 

+ 3 - 1
src/models/event-source/EventSource.ts

@@ -5,6 +5,8 @@ import {
 import Class from '../../common/Class'
 import Calendar from '../../Calendar'
 import EventDefParser from '../event/EventDefParser'
+import { DateMarker } from '../../datelib/util'
+import { DateEnv } from '../../datelib/env'
 
 
 export default class EventSource extends Class {
@@ -70,7 +72,7 @@ export default class EventSource extends Class {
   }
 
 
-  fetch(start, end, timezone, onSuccess, onFailure) {
+  fetch(start: DateMarker, end: DateMarker, dateEnv: DateEnv, onSuccess, onFailure) {
     // subclasses must implement. must call the `onSuccess` or `onFailure` func.
   }
 

+ 9 - 3
src/models/event-source/FuncEventSource.ts

@@ -1,6 +1,7 @@
 import { unpromisify } from '../../util/promise'
 import EventSource from './EventSource'
-
+import { DateMarker } from '../../datelib/util'
+import { DateEnv } from '../../datelib/env'
 
 export default class FuncEventSource extends EventSource {
 
@@ -25,11 +26,16 @@ export default class FuncEventSource extends EventSource {
   }
 
 
-  fetch(start, end, timezone, onSuccess, onFailure) {
+  fetch(start: DateMarker, end: DateMarker, dateEnv: DateEnv, onSuccess, onFailure) {
     this.calendar.pushLoading()
 
     unpromisify( // allow the func to return a promise
-      this.func.bind(this.calendar, start.clone(), end.clone(), timezone),
+      this.func.bind(
+        this.calendar,
+        dateEnv.toDate(start),
+        dateEnv.toDate(end),
+        dateEnv.timeZone
+      ),
       (rawEventDefs) => {
         this.calendar.popLoading()
         onSuccess(this.parseEventDefs(rawEventDefs))

+ 9 - 7
src/models/event-source/JsonFeedEventSource.ts

@@ -2,6 +2,8 @@ import * as request from 'superagent'
 import { assignTo } from '../../util/object'
 import { applyAll } from '../../util/misc'
 import EventSource from './EventSource'
+import { DateMarker } from '../../datelib/util'
+import { DateEnv } from '../../datelib/env'
 
 
 export default class JsonFeedEventSource extends EventSource {
@@ -32,9 +34,9 @@ export default class JsonFeedEventSource extends EventSource {
   }
 
 
-  fetch(start, end, timezone, onSuccess, onFailure) {
+  fetch(start: DateMarker, end: DateMarker, dateEnv: DateEnv, onSuccess, onFailure) {
     let ajaxSettings = this.ajaxSettings
-    let requestParams = this.buildRequestParams(start, end, timezone)
+    let requestParams = this.buildRequestParams(start, end, dateEnv)
     let theRequest
 
     this.calendar.pushLoading()
@@ -75,7 +77,7 @@ export default class JsonFeedEventSource extends EventSource {
   }
 
 
-  buildRequestParams(start, end, timezone) {
+  buildRequestParams(start: DateMarker, end: DateMarker, dateEnv: DateEnv) {
     let calendar = this.calendar
     let ajaxSettings = this.ajaxSettings
     let startParam
@@ -110,11 +112,11 @@ export default class JsonFeedEventSource extends EventSource {
 
     assignTo(params, customRequestParams)
 
-    params[startParam] = start.format()
-    params[endParam] = end.format()
+    params[startParam] = dateEnv.toIso(start)
+    params[endParam] = dateEnv.toIso(end)
 
-    if (timezone && timezone !== 'local') {
-      params[timezoneParam] = timezone
+    if (dateEnv.timeZone !== 'local') {
+      params[timezoneParam] = dateEnv.timeZone
     }
 
     return params

+ 39 - 59
src/models/event/EventDateProfile.ts

@@ -1,20 +1,28 @@
-import { Moment } from 'moment'
 import UnzonedRange from '../UnzonedRange'
+import Calendar from '../../Calendar'
+import { DateMarker } from '../../datelib/util'
 
 /*
 Meant to be immutable
 */
 export default class EventDateProfile {
 
-  start: Moment
-  end: Moment
   unzonedRange: any
-
-
-  constructor(start, end, calendar) {
-    this.start = start
-    this.end = end || null
-    this.unzonedRange = this.buildUnzonedRange(calendar)
+  hasEnd: boolean
+  isAllDay: boolean
+  forcedStartTimeZoneOffset: number
+  forcedEndTimeZoneOffset: number
+
+
+  constructor(startMarker: DateMarker, endMarker: DateMarker, isAllDay: boolean, calendar: Calendar, forcedStartTimeZoneOffset?: number, forcedEndTimeZoneOffset?: number) {
+    this.unzonedRange = new UnzonedRange(
+      startMarker,
+      endMarker || calendar.getDefaultEventEnd(isAllDay, startMarker)
+    )
+    this.hasEnd = Boolean(endMarker)
+    this.isAllDay = isAllDay
+    this.forcedStartTimeZoneOffset = forcedStartTimeZoneOffset
+    this.forcedEndTimeZoneOffset = forcedEndTimeZoneOffset
   }
 
 
@@ -30,19 +38,17 @@ export default class EventDateProfile {
     }
 
     let calendar = source.calendar
-    let start = calendar.moment(startInput)
-    let end = endInput ? calendar.moment(endInput) : null
+    let startMeta = calendar.dateEnv.createMarkerMeta(startInput)
+    let startMarker = startMeta.marker
+    let endMeta = endInput ? calendar.dateEnv.createMarkerMeta(endInput) : null
+    let endMarker = endMeta ? endMeta.marker : null
     let forcedAllDay = rawProps.allDay
     let forceEventDuration = calendar.opt('forceEventDuration')
 
-    if (!start.isValid()) {
+    if (!startMarker) {
       return false
     }
 
-    if (end && (!end.isValid() || !end.isAfter(start))) {
-      end = null
-    }
-
     if (forcedAllDay == null) {
       forcedAllDay = source.allDayDefault
       if (forcedAllDay == null) {
@@ -51,24 +57,28 @@ export default class EventDateProfile {
     }
 
     if (forcedAllDay === true) {
-      start.stripTime()
-      if (end) {
-        end.stripTime()
-      }
-    } else if (forcedAllDay === false) {
-      if (!start.hasTime()) {
-        start.time(0)
-      }
-      if (end && !end.hasTime()) {
-        end.time(0)
+      startMarker = calendar.dateEnv.startOfDay(startMarker)
+
+      if (endMarker) {
+        endMarker = calendar.dateEnv.startOfDay(endMarker)
       }
     }
 
-    if (!end && forceEventDuration) {
-      end = calendar.getDefaultEventEnd(!start.hasTime(), start)
+    if (!endMarker && forceEventDuration) {
+      endMarker = calendar.getDefaultEventEnd(
+        startMeta.isTimeUnspecified,
+        startMarker
+      )
     }
 
-    return new EventDateProfile(start, end, calendar)
+    return new EventDateProfile(
+      startMarker,
+      endMarker,
+      startMeta.isTimeUnspecified && (!endMeta || endMeta.isTimeUnspecified),
+      calendar,
+      startMeta.forcedTimeZoneOffset,
+      endMeta ? endMeta.forcedTimeZoneOffset : null
+    )
   }
 
 
@@ -76,34 +86,4 @@ export default class EventDateProfile {
     return propName === 'start' || propName === 'date' || propName === 'end' || propName === 'allDay'
   }
 
-
-  isAllDay() { // why recompute this every time?
-    return !(this.start.hasTime() || (this.end && this.end.hasTime()))
-  }
-
-
-  /*
-  Needs a Calendar object
-  */
-  buildUnzonedRange(calendar) {
-    let startMs = this.start.clone().stripZone().valueOf()
-    let endMs = this.getEnd(calendar).stripZone().valueOf()
-
-    return new UnzonedRange(startMs, endMs)
-  }
-
-
-  /*
-  Needs a Calendar object
-  */
-  getEnd(calendar) {
-    return this.end ?
-      this.end.clone() :
-      // derive the end from the start and allDay. compute allDay if necessary
-      calendar.getDefaultEventEnd(
-        this.isAllDay(),
-        this.start
-      )
-  }
-
 }

+ 47 - 65
src/models/event/EventDefDateMutation.ts

@@ -1,6 +1,5 @@
-import { diffByUnit, diffDay, diffDayTime } from '../../util/date'
 import EventDateProfile from './EventDateProfile'
-
+import { diffDurations, Duration } from '../../datelib/duration'
 
 export default class EventDefDateMutation {
 
@@ -10,40 +9,43 @@ export default class EventDefDateMutation {
 
   // Durations. if 0-ms duration, will be null instead.
   // Callers should not set this directly.
-  dateDelta: any
-  startDelta: any
-  endDelta: any
+  dateDelta: Duration
+  startDelta: Duration
+  endDelta: Duration
 
 
-  static createFromDiff(dateProfile0, dateProfile1, largeUnit) {
+  static createFromDiff(dateProfile0, dateProfile1, largeUnit, calendar) {
+    const dateEnv = calendar.dateEnv
     let clearEnd = dateProfile0.end && !dateProfile1.end
-    let forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay()
-    let forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay()
+    let forceTimed = dateProfile0.isAllDay && !dateProfile1.isAllDay
+    let forceAllDay = !dateProfile0.isAllDay && dateProfile1.isAllDay
     let dateDelta
     let endDiff
     let endDelta
     let mutation
 
     // subtracts the dates in the appropriate way, returning a duration
-    function subtractDates(date1, date0) { // date1 - date0
-      if (largeUnit) {
-        return diffByUnit(date1, date0, largeUnit) // poorly named
-      } else if (dateProfile1.isAllDay()) {
-        return diffDay(date1, date0) // poorly named
+    function diffDates(date0, date1) {
+      if (largeUnit === 'year') {
+        return dateEnv.diffWholeYears(date0, date1)
+      } else if (largeUnit === 'month') {
+        return dateEnv.diffWholeMonths(date0, date1)
+      } else if (dateProfile1.isAllDay) {
+        return dateEnv.diffWholeDays(date0, date1)
       } else {
-        return diffDayTime(date1, date0) // poorly named
+        return dateEnv.diffDayAndTime(date0, date1)
       }
     }
 
-    dateDelta = subtractDates(dateProfile1.start, dateProfile0.start)
+    dateDelta = diffDates(dateProfile0.start, dateProfile1.start)
 
     if (dateProfile1.end) {
       // use unzonedRanges because dateProfile0.end might be null
-      endDiff = subtractDates(
-        dateProfile1.unzonedRange.getEnd(),
-        dateProfile0.unzonedRange.getEnd()
+      endDiff = diffDates(
+        dateProfile0.unzonedRange.end,
+        dateProfile1.unzonedRange.end
       )
-      endDelta = endDiff.subtract(dateDelta)
+      endDelta = diffDurations(dateDelta, endDiff)
     }
 
     mutation = new EventDefDateMutation()
@@ -61,74 +63,54 @@ export default class EventDefDateMutation {
   returns an undo function.
   */
   buildNewDateProfile(eventDateProfile, calendar) {
-    let start = eventDateProfile.start.clone()
-    let end = null
-    let shouldRezone = false
-
-    if (eventDateProfile.end && !this.clearEnd) {
-      end = eventDateProfile.end.clone()
-    } else if (this.endDelta && !end) {
-      end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start)
+    const dateEnv = calendar.dateEnv
+    let isAllDay = eventDateProfile.isAllDay
+    let startMarker = eventDateProfile.unzonedRange.start
+    let endMarker = null
+
+    if (this.forceAllDay) {
+      isAllDay = true
+    } else if (this.forceTimed) {
+      isAllDay = false
     }
 
-    if (this.forceTimed) {
-      shouldRezone = true
-
-      if (!start.hasTime()) {
-        start.time(0)
-      }
-
-      if (end && !end.hasTime()) {
-        end.time(0)
-      }
-    } else if (this.forceAllDay) {
+    if (eventDateProfile.hasEnd && !this.clearEnd) {
+      endMarker = eventDateProfile.unzonedRange.end
+    } else if (this.endDelta && !endMarker) { // won't always be null?
+      endMarker = calendar.getDefaultEventEnd(isAllDay, startMarker)
+    }
 
-      if (start.hasTime()) {
-        start.stripTime()
-      }
+    if (this.forceAllDay) {
+      startMarker = dateEnv.startOfDay(startMarker)
 
-      if (end && end.hasTime()) {
-        end.stripTime()
+      if (endMarker) {
+        endMarker = dateEnv.startOfDay(endMarker)
       }
     }
 
     if (this.dateDelta) {
-      shouldRezone = true
+      startMarker = dateEnv.add(startMarker, this.dateDelta)
 
-      start.add(this.dateDelta)
-
-      if (end) {
-        end.add(this.dateDelta)
+      if (endMarker) {
+        endMarker = dateEnv.add(endMarker, this.dateDelta)
       }
     }
 
     // do this before adding startDelta to start, so we can work off of start
     if (this.endDelta) {
-      shouldRezone = true
-
-      end.add(this.endDelta)
+      endMarker = dateEnv.add(endMarker, this.endDelta)
     }
 
     if (this.startDelta) {
-      shouldRezone = true
-
-      start.add(this.startDelta)
-    }
-
-    if (shouldRezone) {
-      start = calendar.applyTimezone(start)
-
-      if (end) {
-        end = calendar.applyTimezone(end)
-      }
+      startMarker = dateEnv.add(startMarker, this.startDelta)
     }
 
     // TODO: okay to access calendar option?
-    if (!end && calendar.opt('forceEventDuration')) {
-      end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start)
+    if (!endMarker && calendar.opt('forceEventDuration')) {
+      endMarker = calendar.getDefaultEventEnd(isAllDay, startMarker)
     }
 
-    return new EventDateProfile(start, end, calendar)
+    return new EventDateProfile(startMarker, endMarker, isAllDay, calendar)
   }
 
 

+ 3 - 2
src/models/event/EventDefMutation.ts

@@ -19,7 +19,7 @@ export default class EventDefMutation {
   miscProps: any
 
 
-  static createFromRawProps(eventInstance, rawProps, largeUnit) {
+  static createFromRawProps(eventInstance, rawProps, largeUnit, calendar) {
     let eventDef = eventInstance.def
     let dateProps: any = {}
     let standardProps: any = {}
@@ -48,7 +48,8 @@ export default class EventDefMutation {
       dateMutation = EventDefDateMutation.createFromDiff(
         eventInstance.dateProfile,
         dateProfile,
-        largeUnit
+        largeUnit,
+        calendar
       )
     }
 

+ 11 - 6
src/models/event/EventDefParser.ts

@@ -1,16 +1,21 @@
-import * as moment from 'moment'
-import { isTimeString } from '../../util/date'
 import SingleEventDef from './SingleEventDef'
 import RecurringEventDef from './RecurringEventDef'
+import { createDuration } from '../../datelib/duration'
 
 
 export default {
 
   parse: function(eventInput, source) {
-    if (
-      isTimeString(eventInput.start) || moment.isDuration(eventInput.start) ||
-      isTimeString(eventInput.end) || moment.isDuration(eventInput.end)
-    ) {
+    let startTime, endTime
+
+    if (typeof eventInput.start !== 'number') { // because numbers should be parsed as dates
+      startTime = createDuration(eventInput.start)
+    }
+    if (typeof eventInput.end !== 'number') {
+      endTime = createDuration(eventInput.end)
+    }
+
+    if (startTime && endTime) { // inefficient to compute and then throw away
       return RecurringEventDef.parse(eventInput, source)
     } else {
       return SingleEventDef.parse(eventInput, source)

+ 2 - 2
src/models/event/EventFootprint.ts

@@ -16,8 +16,8 @@ export default class EventFootprint {
   }
 
 
-  getEventLegacy() {
-    return (this.eventInstance || this.eventDef).toLegacy()
+  getEventLegacy(calendar) {
+    return (this.eventInstance || this.eventDef).toLegacy(calendar)
   }
 
 }

+ 6 - 3
src/models/event/EventInstance.ts

@@ -11,12 +11,15 @@ export default class EventInstance {
   }
 
 
-  toLegacy() {
+  toLegacy(calendar) {
+    const dateEnv = calendar.dateEnv
     let dateProfile = this.dateProfile
     let obj = this.def.toLegacy()
 
-    obj.start = dateProfile.start.clone()
-    obj.end = dateProfile.end ? dateProfile.end.clone() : null
+    obj.start = dateEnv.toDate(dateProfile.unzonedRange.start)
+    obj.end = dateProfile.hasEnd ?
+      dateEnv.toDate(dateProfile.unzonedRange.end) :
+      null
 
     return obj
   }

+ 23 - 23
src/models/event/RecurringEventDef.ts

@@ -1,14 +1,16 @@
-import * as moment from 'moment'
 import { assignTo } from '../../util/object'
 import EventDef from './EventDef'
 import EventInstance from './EventInstance'
 import EventDateProfile from './EventDateProfile'
+import { createDuration, Duration } from '../../datelib/duration'
+import { DateMarker } from '../../datelib/util'
 
+const ONE_DAY = createDuration({ days: 1 })
 
 export default class RecurringEventDef extends EventDef {
 
-  startTime: any // duration
-  endTime: any // duration, or null
+  startTime: Duration // duration
+  endTime: Duration // duration, or null
   dowHash: any // object hash, or null
 
 
@@ -19,41 +21,39 @@ export default class RecurringEventDef extends EventDef {
 
   buildInstances(unzonedRange) {
     let calendar = this.source.calendar
-    let unzonedDate = unzonedRange.getStart()
-    let unzonedEnd = unzonedRange.getEnd()
-    let zonedDayStart
-    let instanceStart
-    let instanceEnd
+    const dateEnv = calendar.dateEnv
+    let dateMarker: DateMarker = dateEnv.startOfDay(unzonedRange.start)
+    let endMarker: DateMarker = unzonedRange.end
+    let instanceStart: DateMarker
+    let instanceEnd: DateMarker
     let instances = []
 
-    while (unzonedDate.isBefore(unzonedEnd)) {
+    while (dateMarker < endMarker) {
 
       // if everyday, or this particular day-of-week
-      if (!this.dowHash || this.dowHash[unzonedDate.day()]) {
-
-        zonedDayStart = calendar.applyTimezone(unzonedDate)
-        instanceStart = zonedDayStart.clone()
-        instanceEnd = null
+      if (!this.dowHash || this.dowHash[dateMarker.getUTCDay()]) {
 
         if (this.startTime) {
-          instanceStart.time(this.startTime)
+          instanceStart = dateEnv.add(dateMarker, this.startTime)
         } else {
-          instanceStart.stripTime()
+          instanceStart = null
         }
 
         if (this.endTime) {
-          instanceEnd = zonedDayStart.clone().time(this.endTime)
+          instanceEnd = dateEnv.add(dateMarker, this.endTime)
+        } else {
+          instanceEnd = null
         }
 
         instances.push(
           new EventInstance(
             this, // definition
-            new EventDateProfile(instanceStart, instanceEnd, calendar)
+            new EventDateProfile(instanceStart, instanceEnd, !(this.startTime || this.endTime), calendar)
           )
         )
       }
 
-      unzonedDate.add(1, 'days')
+      dateMarker = dateEnv.add(dateMarker, ONE_DAY) // wish we didnt have to recreate each time
     }
 
     return instances
@@ -76,11 +76,11 @@ export default class RecurringEventDef extends EventDef {
     let def = super.clone()
 
     if (def.startTime) {
-      def.startTime = moment.duration(this.startTime)
+      def.startTime = createDuration(this.startTime)
     }
 
     if (def.endTime) {
-      def.endTime = moment.duration(this.endTime)
+      def.endTime = createDuration(this.endTime)
     }
 
     if (this.dowHash) {
@@ -101,11 +101,11 @@ RecurringEventDef.prototype.applyProps = function(rawProps) {
   let superSuccess = EventDef.prototype.applyProps.call(this, rawProps)
 
   if (rawProps.start) {
-    this.startTime = moment.duration(rawProps.start)
+    this.startTime = createDuration(rawProps.start)
   }
 
   if (rawProps.end) {
-    this.endTime = moment.duration(rawProps.end)
+    this.endTime = createDuration(rawProps.end)
   }
 
   if (rawProps.dow) {

+ 1 - 13
src/models/event/SingleEventDef.ts

@@ -25,7 +25,7 @@ export default class SingleEventDef extends EventDef {
 
 
   isAllDay() {
-    return this.dateProfile.isAllDay()
+    return this.dateProfile.isAllDay
   }
 
 
@@ -38,18 +38,6 @@ export default class SingleEventDef extends EventDef {
   }
 
 
-  rezone() {
-    let calendar = this.source.calendar
-    let dateProfile = this.dateProfile
-
-    this.dateProfile = new EventDateProfile(
-      calendar.moment(dateProfile.start),
-      dateProfile.end ? calendar.moment(dateProfile.end) : null,
-      calendar
-    )
-  }
-
-
   /*
   NOTE: if super-method fails, should still attempt to apply
   */

+ 0 - 308
src/moment-ext.ts

@@ -1,308 +0,0 @@
-import * as moment from 'moment'
-import { isNativeDate } from './util/date'
-import { assignTo } from './util/object'
-
-/*
-GENERAL NOTE on moments throughout the *entire rest* of the codebase:
-All moments are assumed to be ambiguously-zoned unless otherwise noted,
-with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
-Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
-*/
-
-declare module 'moment' {
-  interface Moment {
-    hasTime(): boolean
-    time(): moment.Duration
-    stripZone()
-    stripTime()
-  }
-}
-
-let ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/
-let ambigTimeOrZoneRegex =
-  /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/
-let newMomentProto: any = moment.fn // where we will attach our new methods
-let oldMomentProto = assignTo({}, newMomentProto) // copy of original moment methods
-
-// tell momentjs to transfer these properties upon clone
-let momentProperties = (moment as any).momentProperties
-momentProperties.push('_fullCalendar')
-momentProperties.push('_ambigTime')
-momentProperties.push('_ambigZone')
-
-/*
-Call this if you want Moment's original format method to be used
-*/
-function oldMomentFormat(mom, formatStr?) {
-  return oldMomentProto.format.call(mom, formatStr) // oldMomentProto defined in moment-ext.js
-}
-
-export { newMomentProto, oldMomentProto, oldMomentFormat }
-
-
-// Creating
-// -------------------------------------------------------------------------------------------------
-
-// Creates a new moment, similar to the vanilla moment(...) constructor, but with
-// extra features (ambiguous time, enhanced formatting). When given an existing moment,
-// it will function as a clone (and retain the zone of the moment). Anything else will
-// result in a moment in the local zone.
-const momentExt: any = function() {
-  return makeMoment(arguments)
-}
-
-export default momentExt
-
-// Sames as momentExt, but forces the resulting moment to be in the UTC timezone.
-momentExt.utc = function() {
-  let mom = makeMoment(arguments, true)
-
-  // Force it into UTC because makeMoment doesn't guarantee it
-  // (if given a pre-existing moment for example)
-  if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
-    mom.utc()
-  }
-
-  return mom
-}
-
-// Same as momentExt, but when given an ISO8601 string, the timezone offset is preserved.
-// ISO8601 strings with no timezone offset will become ambiguously zoned.
-momentExt.parseZone = function() {
-  return makeMoment(arguments, true, true)
-}
-
-// Builds an enhanced moment from args. When given an existing moment, it clones. When given a
-// native Date, or called with no arguments (the current time), the resulting moment will be local.
-// Anything else needs to be "parsed" (a string or an array), and will be affected by:
-//    parseAsUTC - if there is no zone information, should we parse the input in UTC?
-//    parseZone - if there is zone information, should we force the zone of the moment?
-function makeMoment(args, parseAsUTC= false, parseZone= false) {
-  let input = args[0]
-  let isSingleString = args.length === 1 && typeof input === 'string'
-  let isAmbigTime
-  let isAmbigZone
-  let ambigMatch
-  let mom
-
-  if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
-    mom = moment.apply(null, args)
-  } else { // "parsing" is required
-    isAmbigTime = false
-    isAmbigZone = false
-
-    if (isSingleString) {
-      if (ambigDateOfMonthRegex.test(input)) {
-        // accept strings like '2014-05', but convert to the first of the month
-        input += '-01'
-        args = [ input ] // for when we pass it on to moment's constructor
-        isAmbigTime = true
-        isAmbigZone = true
-      } else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
-        isAmbigTime = !ambigMatch[5] // no time part?
-        isAmbigZone = true
-      }
-    } else if (Array.isArray(input)) {
-      // arrays have no timezone information, so assume ambiguous zone
-      isAmbigZone = true
-    }
-    // otherwise, probably a string with a format
-
-    if (parseAsUTC || isAmbigTime) {
-      mom = moment.utc.apply(moment, args)
-    } else {
-      mom = moment.apply(null, args)
-    }
-
-    if (isAmbigTime) {
-      mom._ambigTime = true
-      mom._ambigZone = true // ambiguous time always means ambiguous zone
-    } else if (parseZone) { // let's record the inputted zone somehow
-      if (isAmbigZone) {
-        mom._ambigZone = true
-      } else if (isSingleString) {
-        mom.utcOffset(input) // if not a valid zone, will assign UTC
-      }
-    }
-  }
-
-  mom._fullCalendar = true // flag for extended functionality
-
-  return mom
-}
-
-
-// Week Number
-// -------------------------------------------------------------------------------------------------
-
-
-// Returns the week number, considering the locale's custom week number calcuation
-// `weeks` is an alias for `week`
-newMomentProto.week = newMomentProto.weeks = function(input) {
-  let weekCalc = this._locale._fullCalendar_weekCalc
-
-  if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
-    return weekCalc(this)
-  } else if (weekCalc === 'ISO') {
-    return oldMomentProto.isoWeek.apply(this, arguments) // ISO getter/setter
-  }
-
-  return oldMomentProto.week.apply(this, arguments) // local getter/setter
-}
-
-
-// Time-of-day
-// -------------------------------------------------------------------------------------------------
-
-// GETTER
-// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
-// If the moment has an ambiguous time, a duration of 00:00 will be returned.
-//
-// SETTER
-// You can supply a Duration, a Moment, or a Duration-like argument.
-// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
-newMomentProto.time = function(time) {
-
-  // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
-  // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
-  if (!this._fullCalendar) {
-    return oldMomentProto.time.apply(this, arguments)
-  }
-
-  if (time == null) { // getter
-    return moment.duration({
-      hours: this.hours(),
-      minutes: this.minutes(),
-      seconds: this.seconds(),
-      milliseconds: this.milliseconds()
-    })
-  } else { // setter
-
-    this._ambigTime = false // mark that the moment now has a time
-
-    if (!moment.isDuration(time) && !moment.isMoment(time)) {
-      time = moment.duration(time)
-    }
-
-    // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
-    // Only for Duration times, not Moment times.
-    let dayHours = 0
-    if (moment.isDuration(time)) {
-      dayHours = Math.floor(time.asDays()) * 24
-    }
-
-    // We need to set the individual fields.
-    // Can't use startOf('day') then add duration. In case of DST at start of day.
-    return this.hours(dayHours + time.hours())
-      .minutes(time.minutes())
-      .seconds(time.seconds())
-      .milliseconds(time.milliseconds())
-  }
-}
-
-// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
-// but preserving its YMD. A moment with a stripped time will display no time
-// nor timezone offset when .format() is called.
-newMomentProto.stripTime = function() {
-
-  if (!this._ambigTime) {
-
-    this.utc(true) // keepLocalTime=true (for keeping *date* value)
-
-    // set time to zero
-    this.set({
-      hours: 0,
-      minutes: 0,
-      seconds: 0,
-      ms: 0
-    })
-
-    // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
-    // which clears all ambig flags.
-    this._ambigTime = true
-    this._ambigZone = true // if ambiguous time, also ambiguous timezone offset
-  }
-
-  return this // for chaining
-}
-
-// Returns if the moment has a non-ambiguous time (boolean)
-newMomentProto.hasTime = function() {
-  return !this._ambigTime
-}
-
-
-// Timezone
-// -------------------------------------------------------------------------------------------------
-
-// Converts the moment to UTC, stripping out its timezone offset, but preserving its
-// YMD and time-of-day. A moment with a stripped timezone offset will display no
-// timezone offset when .format() is called.
-newMomentProto.stripZone = function() {
-  let wasAmbigTime
-
-  if (!this._ambigZone) {
-
-    wasAmbigTime = this._ambigTime
-
-    this.utc(true) // keepLocalTime=true (for keeping date and time values)
-
-    // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
-    this._ambigTime = wasAmbigTime || false
-
-    // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
-    // which clears the ambig flags.
-    this._ambigZone = true
-  }
-
-  return this // for chaining
-}
-
-// Returns of the moment has a non-ambiguous timezone offset (boolean)
-newMomentProto.hasZone = function() {
-  return !this._ambigZone
-}
-
-
-// implicitly marks a zone
-newMomentProto.local = function(keepLocalTime) {
-
-  // for when converting from ambiguously-zoned to local,
-  // keep the time values when converting from UTC -> local
-  oldMomentProto.local.call(this, this._ambigZone || keepLocalTime)
-
-  // ensure non-ambiguous
-  // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
-  this._ambigTime = false
-  this._ambigZone = false
-
-  return this // for chaining
-}
-
-
-// implicitly marks a zone
-newMomentProto.utc = function(keepLocalTime) {
-
-  oldMomentProto.utc.call(this, keepLocalTime)
-
-  // ensure non-ambiguous
-  // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
-  this._ambigTime = false
-  this._ambigZone = false
-
-  return this
-}
-
-
-// implicitly marks a zone (will probably get called upon .utc() and .local())
-newMomentProto.utcOffset = function(tzo) {
-
-  if (tzo != null) { // setter
-    // these assignments needs to happen before the original zone method is called.
-    // I forget why, something to do with a browser crash.
-    this._ambigTime = false
-    this._ambigZone = false
-  }
-
-  return oldMomentProto.utcOffset.apply(this, arguments)
-}

+ 2 - 8
src/options.ts

@@ -4,10 +4,9 @@ import { mergeProps } from './util/object'
 export const globalDefaults = {
 
   titleRangeSeparator: ' \u2013 ', // en dash
-  monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option
 
   defaultTimedEventDuration: '02:00:00',
-  defaultAllDayEventDuration: { days: 1 },
+  defaultAllDayEventDuration: { day: 1 },
   forceEventDuration: false,
   nextDayThreshold: '09:00:00', // 9am
 
@@ -87,7 +86,7 @@ export const globalDefaults = {
   eventLimit: false,
   eventLimitText: 'more',
   eventLimitClick: 'popover',
-  dayPopoverFormat: 'LL',
+  dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
 
   handleWindowResize: true,
   windowResizeDelay: 100, // milliseconds before an updateSize happens
@@ -97,11 +96,6 @@ export const globalDefaults = {
 }
 
 
-export const englishDefaults = { // used by locale.js
-  dayPopoverFormat: 'dddd, MMMM D'
-}
-
-
 export const rtlDefaults = { // right-to-left defaults
   header: { // TODO: smarter solution (first/center/last ?)
     left: 'next,prev today',

+ 26 - 26
src/types/input-types.ts

@@ -3,16 +3,16 @@ Huge thanks to these people:
 https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/fullcalendar/index.d.ts
 */
 
-import * as moment from 'moment'
 import View from '../View'
 import EventSource from '../models/event-source/EventSource'
+import { Duration } from '../datelib/duration'
+import { DateInput } from '../datelib/env'
 
-export type MomentInput = moment.Moment | Date | object | string | number
-export type DurationInput = moment.Duration | object | string | number
+export type DurationInput = Duration | object | string | number
 
 export interface RangeInput {
-  start?: MomentInput
-  end?: MomentInput
+  start?: DateInput
+  end?: DateInput
 }
 
 export type ConstraintInput = RangeInput | BusinessHoursInput | 'businessHours'
@@ -43,7 +43,7 @@ export interface EventObjectInput extends EventOptionsBase, RangeInput {
   [customField: string]: any // non-standard fields
 }
 
-export type EventSourceFunction = (start: moment.Moment, end: moment.Moment, timezone: string, callback: ((events: EventObjectInput[]) => void)) => void
+export type EventSourceFunction = (start: Date, end: Date, timezone: string, callback: ((events: EventObjectInput[]) => void)) => void
 export type EventSourceSimpleInput = EventObjectInput[] | EventSourceFunction | string
 
 export interface EventSourceExtendedInput extends EventOptionsBase {
@@ -103,21 +103,21 @@ export interface ButtonTextCompoundInput {
 }
 
 export interface BusinessHoursInput {
-  start?: MomentInput
-  end?: MomentInput
+  start?: DateInput
+  end?: DateInput
   dow?: number[]
 }
 
 export interface EventSegment {
   event: EventObjectInput
-  start: moment.Moment
-  end: moment.Moment
+  start: Date
+  end: Date
   isStart: boolean
   isEnd: boolean
 }
 
 export interface CellInfo {
-  date: moment.Moment
+  date: Date
   dayEl: HTMLElement
   moreEl: HTMLElement
   segs: EventSegment[]
@@ -125,8 +125,8 @@ export interface CellInfo {
 }
 
 export interface DropInfo {
-  start: moment.Moment
-  end: moment.Moment
+  start: Date
+  end: Date
 }
 
 export interface OptionsInputBase {
@@ -145,7 +145,7 @@ export interface OptionsInputBase {
   fixedWeekCount?: boolean
   weekNumbers?: boolean
   weekNumbersWithinDays?: boolean
-  weekNumberCalculation?: 'local' | 'ISO' | ((m: moment.Moment) => number)
+  weekNumberCalculation?: 'local' | 'ISO' | ((m: Date) => number)
   businessHours?: boolean | BusinessHoursInput | BusinessHoursInput[]
   showNonCurrentDates?: boolean
   height?: number | 'auto' | 'parent' | (() => number)
@@ -156,7 +156,7 @@ export interface OptionsInputBase {
   eventLimit?: boolean | number
   eventLimitClick?: 'popover' | 'week' | 'day' | string | ((cellinfo: CellInfo, jsevent: Event) => void)
   timezone?: string | boolean
-  now?: MomentInput | (() => MomentInput)
+  now?: DateInput | (() => DateInput)
   defaultView?: string
   allDaySlot?: boolean
   allDayText?: string
@@ -171,9 +171,9 @@ export interface OptionsInputBase {
   listDayFormat?: string | boolean
   listDayAltFormat?: string | boolean
   noEventsMessage?: string
-  defaultDate?: MomentInput
+  defaultDate?: DateInput
   nowIndicator?: boolean
-  visibleRange?: ((currentDate: moment.Moment) => RangeInput) | RangeInput
+  visibleRange?: ((currentDate: Date) => RangeInput) | RangeInput
   validRange?: RangeInput
   dateIncrement?: DurationInput
   dateAlignment?: string
@@ -183,8 +183,8 @@ export interface OptionsInputBase {
   timeFormat?: string
   columnHeader?: boolean
   columnHeaderFormat?: string
-  columnHeaderText?: string | ((date: MomentInput) => string)
-  columnHeaderHtml?: string | ((date: MomentInput) => string)
+  columnHeaderText?: string | ((date: DateInput) => string)
+  columnHeaderHtml?: string | ((date: DateInput) => string)
   titleFormat?: string
   monthNames?: string[]
   monthNamesShort?: string[]
@@ -196,7 +196,7 @@ export interface OptionsInputBase {
   eventLimitText?: string | ((eventCnt: number) => string)
   dayPopoverFormat?: string
   navLinks?: boolean
-  navLinkDayClick?: string | ((date: moment.Moment, jsEvent: Event) => void)
+  navLinkDayClick?: string | ((date: Date, jsEvent: Event) => void)
   navLinkWeekClick?: string | ((weekStart: any, jsEvent: Event) => void)
   selectable?: boolean
   selectHelper?: boolean
@@ -233,13 +233,13 @@ export interface OptionsInputBase {
 
   viewRender?(view: View, element: HTMLElement): void
   viewDestroy?(view: View, element: HTMLElement): void
-  dayRender?(date: moment.Moment, cell: HTMLElement): void
+  dayRender?(date: DateInput, cell: HTMLElement): void
   windowResize?(view: View): void
-  dayClick?(date: moment.Moment, jsEvent: MouseEvent, view: View, resourceObj?): void // resourceObj for Scheduler
+  dayClick?(date: DateInput, jsEvent: MouseEvent, view: View, resourceObj?): void // resourceObj for Scheduler
   eventClick?(event: EventObjectInput, jsEvent: MouseEvent, view: View): boolean | void
   eventMouseover?(event: EventObjectInput, jsEvent: MouseEvent, view: View): void
   eventMouseout?(event: EventObjectInput, jsEvent: MouseEvent, view: View): void
-  select?(start: moment.Moment, end: moment.Moment, jsEvent: MouseEvent, view: View, resource?: any): void
+  select?(start: DateInput, end: DateInput, jsEvent: MouseEvent, view: View, resource?: any): void
   unselect?(view: View, jsEvent: Event): void
   eventDataTransform?(eventData: any): EventObjectInput
   loading?(isLoading: boolean, view: View): void
@@ -249,11 +249,11 @@ export interface OptionsInputBase {
   eventDestroy?(event: EventObjectInput, element: HTMLElement, view: View): void
   eventDragStart?(event: EventObjectInput, jsEvent: MouseEvent, ui: any, view: View): void
   eventDragStop?(event: EventObjectInput, jsEvent: MouseEvent, ui: any, view: View): void
-  eventDrop?(event: EventObjectInput, delta: moment.Duration, revertFunc: Function, jsEvent: Event, ui: any, view: View): void
+  eventDrop?(event: EventObjectInput, delta: Duration, revertFunc: Function, jsEvent: Event, ui: any, view: View): void
   eventResizeStart?(event: EventObjectInput, jsEvent: MouseEvent, ui: any, view: View): void
   eventResizeStop?(event: EventObjectInput, jsEvent: MouseEvent, ui: any, view: View): void
-  eventResize?(event: EventObjectInput, delta: moment.Duration, revertFunc: Function, jsEvent: Event, ui: any, view: View): void
-  drop?(date: moment.Moment, jsEvent: MouseEvent, ui: any): void
+  eventResize?(event: EventObjectInput, delta: Duration, revertFunc: Function, jsEvent: Event, ui: any, view: View): void
+  drop?(date: DateInput, jsEvent: MouseEvent, ui: any): void
   eventReceive?(event: EventObjectInput): void
 }
 

+ 0 - 150
src/util/date.ts

@@ -1,150 +0,0 @@
-import * as moment from 'moment'
-import { isInt } from '../util/misc'
-
-export const dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]
-export const unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ] // descending
-
-
-// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
-// Moments will have their timezones normalized.
-export function diffDayTime(a, b) {
-  return moment.duration({
-    days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
-    ms: a.time() - b.time() // time-of-day from day start. disregards timezone
-  })
-}
-
-
-// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
-export function diffDay(a, b) {
-  return moment.duration({
-    days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
-  })
-}
-
-
-// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
-export function diffByUnit(a, b, unit) {
-  return moment.duration(
-    Math.round(a.diff(b, unit, true)), // returnFloat=true
-    unit
-  )
-}
-
-
-// Computes the unit name of the largest whole-unit period of time.
-// For example, 48 hours will be "days" whereas 49 hours will be "hours".
-// Accepts start/end, a range object, or an original duration object.
-export function computeGreatestUnit(start, end?) {
-  let i
-  let unit
-  let val
-
-  for (i = 0; i < unitsDesc.length; i++) {
-    unit = unitsDesc[i]
-    val = computeRangeAs(unit, start, end)
-
-    if (val >= 1 && isInt(val)) {
-      break
-    }
-  }
-
-  return unit // will be "milliseconds" if nothing else matches
-}
-
-
-// like computeGreatestUnit, but has special abilities to interpret the source input for clues
-export function computeDurationGreatestUnit(duration, durationInput) {
-  let unit = computeGreatestUnit(duration)
-
-  // prevent days:7 from being interpreted as a week
-  if (unit === 'week' && typeof durationInput === 'object' && durationInput && durationInput.days) { // non-null object
-    unit = 'day'
-  }
-
-  return unit
-}
-
-
-// Computes the number of units (like "hours") in the given range.
-// Range can be a {start,end} object, separate start/end args, or a Duration.
-// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
-// of month-diffing logic (which tends to vary from version to version).
-function computeRangeAs(unit, start, end) {
-
-  if (end != null) { // given start, end
-    return end.diff(start, unit, true)
-  } else if (moment.isDuration(start)) { // given duration
-    return start.as(unit)
-  } else { // given { start, end } range object
-    return start.end.diff(start.start, unit, true)
-  }
-}
-
-
-// Intelligently divides a range (specified by a start/end params) by a duration
-export function divideRangeByDuration(start, end, dur) {
-  let months
-
-  if (durationHasTime(dur)) {
-    return (end - start) / dur
-  }
-  months = dur.asMonths()
-  if (Math.abs(months) >= 1 && isInt(months)) {
-    return end.diff(start, 'months', true) / months
-  }
-  return end.diff(start, 'days', true) / dur.asDays()
-}
-
-
-// Intelligently divides one duration by another
-export function divideDurationByDuration(dur1, dur2) {
-  let months1
-  let months2
-
-  if (durationHasTime(dur1) || durationHasTime(dur2)) {
-    return dur1 / dur2
-  }
-  months1 = dur1.asMonths()
-  months2 = dur2.asMonths()
-  if (
-    Math.abs(months1) >= 1 && isInt(months1) &&
-    Math.abs(months2) >= 1 && isInt(months2)
-  ) {
-    return months1 / months2
-  }
-  return dur1.asDays() / dur2.asDays()
-}
-
-
-// Intelligently multiplies a duration by a number
-export function multiplyDuration(dur, n) {
-  let months
-
-  if (durationHasTime(dur)) {
-    return moment.duration(dur * n)
-  }
-  months = dur.asMonths()
-  if (Math.abs(months) >= 1 && isInt(months)) {
-    return moment.duration({ months: months * n })
-  }
-  return moment.duration({ days: dur.asDays() * n })
-}
-
-
-// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
-export function durationHasTime(dur) {
-  return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds())
-}
-
-
-export function isNativeDate(input) {
-  return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date
-}
-
-
-// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
-export function isTimeString(str) {
-  return typeof str === 'string' &&
-    /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str)
-}

+ 0 - 5
src/util/html.ts

@@ -9,11 +9,6 @@ export function htmlEscape(s) {
 }
 
 
-export function stripHtmlEntities(text) {
-  return text.replace(/&.*?;/g, '')
-}
-
-
 // Given a hash of CSS properties, returns a string of CSS.
 // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
 export function cssToStr(cssProps) {