Ver código fonte

cleaned up datelib

Adam Shaw 7 anos atrás
pai
commit
d2ac7bd213

+ 2 - 5
src/datelib/calendar-system.ts

@@ -1,5 +1,4 @@
-import { DateMarker, arrayToUtcDate, dateToUtcArray } from './util'
-
+import { DateMarker, arrayToUtcDate, dateToUtcArray } from './marker'
 
 export interface CalendarSystem {
   getMarkerYear(d: DateMarker): number;
@@ -12,12 +11,10 @@ export interface CalendarSystem {
 
 let calendarSystemClassMap = {}
 
-
 export function registerCalendarSystem(name, theClass) {
   calendarSystemClassMap[name] = theClass
 }
 
-
 export function createCalendarSystem(name) {
   return new calendarSystemClassMap[name]()
 }
@@ -47,4 +44,4 @@ class GregorianCalendarSystem implements CalendarSystem {
 
 }
 
-registerCalendarSystem('gregorian', GregorianCalendarSystem)
+registerCalendarSystem('gregory', GregorianCalendarSystem)

+ 95 - 85
src/datelib/duration.ts

@@ -1,5 +1,5 @@
 
-export interface DurationObjInput {
+export interface DurationInput {
   years?: number
   year?: number
   months?: number
@@ -26,7 +26,10 @@ export interface Duration {
   time: number
 }
 
-let re = /^(?:(\d+)\.)?(\d\d):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/
+
+// Parsing and Creation
+
+const PARSE_RE = /^(?:(\d+)\.)?(\d\d):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/
 
 export function createDuration(input, unit?: string) {
   if (typeof input === 'string') {
@@ -34,48 +37,30 @@ export function createDuration(input, unit?: string) {
   } 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)
-    }
+    return normalizeObject({ [unit || 'millisecond']: input })
   } 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
-  }
-}
-
 function parseString(s: string): Duration {
-  let m = re.exec(s)
+  let m = PARSE_RE.exec(s)
   if (m) {
     return {
       year: 0,
       month: 0,
-      day: m[1] ? parseInt(m[1], 10) : 0, // todo: do this for others
+      day: m[1] ? parseInt(m[1], 10) : 0,
       time:
-        (parseInt(m[2], 10) || 0) * 60 * 60 * 1000 + // hours
-        (parseInt(m[3], 10) || 0) * 60 * 1000 + // minutes
-        (parseInt(m[4], 10) || 0) * 1000 + // seconds
-        (parseInt(m[5], 10) || 0) // ms
+        (m[2] ? parseInt(m[2], 10) : 0) * 60 * 60 * 1000 + // hours
+        (m[3] ? parseInt(m[3], 10) : 0) * 60 * 1000 + // minutes
+        (m[4] ? parseInt(m[4], 10) : 0) * 1000 + // seconds
+        (m[5] ? parseInt(m[5], 10) : 0) // ms
     }
   }
   return null
 }
 
-function normalizeObject(obj: DurationObjInput): Duration {
+function normalizeObject(obj: DurationInput): Duration {
   return {
     year: obj.years || obj.year || 0,
     month: obj.months || obj.month || 0,
@@ -90,10 +75,13 @@ function normalizeObject(obj: DurationObjInput): Duration {
   }
 }
 
-export function getWeeksFromInput(obj: DurationObjInput) {
+export function getWeeksFromInput(obj: DurationInput) {
   return obj.weeks || obj.week || 0
 }
 
+
+// Equality
+
 export function durationsEqual(d0: Duration, d1: Duration): boolean {
   return d0.year === d1.year &&
     d0.month === d1.month &&
@@ -101,6 +89,22 @@ export function durationsEqual(d0: Duration, d1: Duration): boolean {
     d0.time === d1.time
 }
 
+export function isSingleDay(dur: Duration) {
+  return dur.year === 0 && dur.month === 0 && dur.day === 1 && dur.time === 0
+}
+
+
+// Simple Math
+
+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
+  }
+}
+
 export function diffDurations(d0: Duration, d1: Duration): Duration {
   return {
     year: d1.year - d0.year,
@@ -110,7 +114,46 @@ export function diffDurations(d0: Duration, d1: Duration): Duration {
   }
 }
 
-export function wholeDivideDurationByDuration(numerator: Duration, denominator: Duration): number {
+export function multiplyDuration(d: Duration, n: number) {
+  return {
+    year: d.year * n,
+    month: d.month * n,
+    day: d.day * n,
+    time: d.time * n
+  }
+}
+
+
+// Conversions
+// "Rough" because they are based on average-case Gregorian months/years
+
+export function asRoughDays(dur: Duration) {
+  return asRoughMs(dur) / 864e5
+}
+
+export function asRoughHours(dur: Duration) {
+  return asRoughMs(dur) / (1000 * 60 * 60)
+}
+
+export function asRoughMinutes(dur: Duration) {
+  return asRoughMs(dur) / (1000 * 60)
+}
+
+export function asRoughSeconds(dur: Duration) {
+  return asRoughMs(dur) / 1000
+}
+
+export function asRoughMs(dur: Duration) {
+  return dur.year * (365 * 864e5) +
+    dur.month * (30 * 864e5) +
+    dur.day * 864e5 +
+    dur.time
+}
+
+
+// Advanced Math
+
+export function wholeDivideDurations(numerator: Duration, denominator: Duration): number {
   let res0 = null
   let res1 = null
 
@@ -121,7 +164,7 @@ export function wholeDivideDurationByDuration(numerator: Duration, denominator:
   if (denominator.month) {
     res1 = numerator.month / denominator.month
 
-    if (res0 === null || res0 === res1) {
+    if (res0 === null || res0 === res1) { // must match any previous division results
       res0 = res1
     } else {
       return null
@@ -131,7 +174,7 @@ export function wholeDivideDurationByDuration(numerator: Duration, denominator:
   if (denominator.day) {
     res1 = numerator.day / denominator.day
 
-    if (res0 === null || res0 === res1) {
+    if (res0 === null || res0 === res1) { // must match any previous division results
       res0 = res1
     } else {
       return null
@@ -141,7 +184,7 @@ export function wholeDivideDurationByDuration(numerator: Duration, denominator:
   if (denominator.time) {
     res1 = numerator.time / denominator.time
 
-    if (res0 === null || res0 === res1) {
+    if (res0 === null || res0 === res1) { // must match any previous division results
       res0 = res1
     } else {
       return null
@@ -151,63 +194,30 @@ export function wholeDivideDurationByDuration(numerator: Duration, denominator:
   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'
+export function greatestDurationDenominator(dur: Duration, considerWeeks: boolean = false) {
+  let time = dur.time
+  if (time) {
+    if (time % 1000 !== 0) {
+      return { unit: 'millisecond', value: time }
+    }
+    if (time % (1000 * 60) ! == 0) {
+      return { unit: 'second', value: time / 1000 }
+    }
+    if (time % (1000 * 60 * 60) !== 0) {
+      return { unit: 'minute', value: time / (1000 * 60) }
+    }
+    if (time) {
+      return { unit: 'hour', value: time / (1000 * 60 * 60) }
+    }
   }
   if (dur.day) {
-    return 'day'
+    return { unit: 'day', value: dur.day }
   }
   if (dur.month) {
-    return 'month'
+    return { unit: 'month', value: dur.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
+    return { unit: 'year', value: dur.year }
   }
-}
-
-
-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
+  return { unit: 'millisecond', value: 0 }
 }

+ 244 - 362
src/datelib/env.ts

@@ -1,56 +1,63 @@
-import { DateMarker, arrayToUtcDate, dateToUtcArray, arrayToLocalDate, dateToLocalArray, startOfHour, startOfMinute, startOfSecond, addMs  } from './util'
+import {
+  DateMarker,
+  addDays, addMs,
+  diffHours, diffMinutes, diffSeconds, diffWholeWeeks, diffWholeDays,
+  startOfDay, startOfHour, startOfMinute, startOfSecond,
+  weekOfYear, arrayToUtcDate, dateToUtcArray, dateToLocalArray, arrayToLocalDate, timeAsMs
+} from './marker'
 import { CalendarSystem, createCalendarSystem } from './calendar-system'
-import { namedTimeZoneOffsetGenerator, getNamedTimeZoneOffsetGenerator } from './timezone'
-import { getLocale } from './locale'
+import { Locale } from './locale'
+import { NamedTimeZoneImpl, createNamedTimeZoneImpl } from './timezone'
 import { Duration } from './duration'
 import { DateFormatter, buildIsoString } from './formatting'
 import { parse } from './parsing'
-import { assignTo } from '../util/object'
+import { isInt } from '../util/misc'
 
 export interface DateEnvSettings {
   timeZone: string
   timeZoneImpl?: string
   calendarSystem: string
-  locale: string // TODO: accept a list
+  locale: Locale
   weekNumberCalculation?: any
   firstDay?: any
 }
 
-
-
-export type DateInput = Date | number[] | number | string
-
-
-const MS_IN_DAY = 864e5
-
-// TODO: locale: 'auto'
-// TODO: separate locale object from dateenv. but keep separate from locale name
+export type DateInput = Date | string | number | number[]
 
 
 export class DateEnv {
 
   timeZone: string
-  namedTimeZoneOffsetGenerator: namedTimeZoneOffsetGenerator
+  namedTimeZoneImpl: NamedTimeZoneImpl
+  canComputeOffset: boolean
+
   calendarSystem: CalendarSystem
-  locale: string
-  weekMeta: any
+  locale: Locale
+  weekDow: number
+  weekDoy: number
   weekNumberFunc: any
-  simpleNumberFormat: Intl.NumberFormat
+
 
   constructor(settings: DateEnvSettings) {
-    this.timeZone = settings.timeZone
-    this.namedTimeZoneOffsetGenerator = getNamedTimeZoneOffsetGenerator(settings.timeZoneImpl)
+    let timeZone = this.timeZone = settings.timeZone
+    let isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC'
+
+    if (settings.timeZoneImpl && isNamedTimeZone) {
+      this.namedTimeZoneImpl = createNamedTimeZoneImpl(settings.timeZoneImpl, timeZone)
+    }
+
+    this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl)
+
     this.calendarSystem = createCalendarSystem(settings.calendarSystem)
     this.locale = settings.locale
-    this.weekMeta = assignTo({}, getLocale(settings.locale).week)
-
-    this.simpleNumberFormat = new Intl.NumberFormat(settings.locale)
+    this.weekDow = settings.locale.week.dow
+    this.weekDoy = settings.locale.week.dow
 
     if (settings.weekNumberCalculation === 'ISO') {
-      this.weekMeta.dow = 1
-      this.weekMeta.doy = 4
+      this.weekDow = 1
+      this.weekDoy = 4
     } else if (typeof settings.firstDay === 'number') {
-      this.weekMeta.dow = settings.firstDay
+      this.weekDow = settings.firstDay
     }
 
     if (typeof settings.weekNumberCalculation === 'function') {
@@ -58,249 +65,137 @@ export class DateEnv {
     }
   }
 
-  add(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
-    ])
-  }
+  // Creating / Parsing
 
-  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)
+  createMarker(input: DateInput): DateMarker {
+    let meta = this.createMarkerMeta(input)
+    if (meta === null) {
+      return null
     }
+    return meta.marker
   }
 
-  startOfYear(marker: DateMarker): DateMarker {
-    let { calendarSystem } = this
-
-    return calendarSystem.arrayToMarker([
-      calendarSystem.getMarkerYear(marker) // might not work, might go to ms
-    ])
+  createNowMarker(): DateMarker {
+    return this.timestampToMarker(new Date().valueOf())
   }
 
-  startOfMonth(marker: DateMarker): DateMarker {
-    let { calendarSystem } = this
+  createMarkerMeta(input: DateInput) {
 
-    return calendarSystem.arrayToMarker([
-      calendarSystem.getMarkerYear(marker),
-      calendarSystem.getMarkerMonth(marker)
-    ])
-  }
+    if (typeof input === 'string') {
+      return this.parse(input)
+    }
 
-  startOfWeek(marker: DateMarker): DateMarker {
-    return arrayToUtcDate([
-      marker.getUTCFullYear(),
-      marker.getUTCMonth(),
-      marker.getUTCDate() - (marker.getUTCDay() - this.weekMeta.dow + 7) % 7
-    ])
-  }
+    let marker = null
 
-  computeWeekNumber(marker: DateMarker): number {
-    if (this.weekNumberFunc) {
-      return this.weekNumberFunc(this.toDate(marker))
-    } else {
-      let { dow, doy } = this.weekMeta
-      return weekOfYear(marker, dow, doy)
+    if (typeof input === 'number') {
+      marker = this.timestampToMarker(input)
+    } else if (input instanceof Date) {
+      marker = this.timestampToMarker(input.valueOf())
+    } else if (Array.isArray(input)) {
+      marker = arrayToUtcDate(input)
     }
-  }
 
-  // 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
+    if (marker === null) {
+      return null
     }
 
-    return s
+    return { marker, isTimeUnspecified: false, forcedTimeZoneOffset: null }
   }
 
-  toDate(marker: DateMarker): Date {
-    if (this.timeZone === 'UTC' || !this.canComputeTimeZoneOffset()) {
-      return new Date(marker.valueOf())
-    } else {
-      let arr = dateToUtcArray(marker)
+  parse(s: string) {
+    let parts = parse(s)
+    let marker = parts.marker
+    let forcedTimeZoneOffset = null
 
-      if (this.timeZone === 'local') {
-        return arrayToLocalDate(arr)
+    if (parts.timeZoneOffset !== null) {
+      if (this.canComputeOffset) {
+        marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000)
       } else {
-        return new Date(
-          marker.valueOf() -
-          this.namedTimeZoneOffsetGenerator(this.timeZone, arr)
-        )
+        forcedTimeZoneOffset = parts.timeZoneOffset
       }
     }
-  }
 
-  canComputeTimeZoneOffset() {
-    return this.timeZone === 'UTC' || this.timeZone === 'local' || this.namedTimeZoneOffsetGenerator
+    return { marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTimeZoneOffset }
   }
 
-  // will return undefined if cant compute it
-  computeTimeZoneOffset(marker: DateMarker) {
-    if (this.timeZone === 'UTC') {
-      return 0
-    } else {
-      let arr = dateToUtcArray(marker)
 
-      if (this.timeZone === 'local') {
-        return arrayToLocalDate(arr).getTimezoneOffset()
-      } else if (this.namedTimeZoneOffsetGenerator) {
-        return this.namedTimeZoneOffsetGenerator(this.timeZone, arr)
-      }
-    }
+  // Accessors
+
+  getYear(marker: DateMarker): number {
+    return this.calendarSystem.getMarkerYear(marker)
   }
 
-  toRangeFormat(start: DateMarker, end: DateMarker, formatter: DateFormatter, dateOptions: any = {}) {
+  getMonth(marker: DateMarker): number {
+    return this.calendarSystem.getMarkerMonth(marker)
+  }
 
-    // yuck
-    if (dateOptions.isExclusive) {
-      end = addMs(end, -1)
-    }
 
-    return formatter.format(
-      {
-        marker: start,
-        timeZoneOffset: dateOptions.forcedStartTimeZoneOffset != null ?
-          dateOptions.forcedStartTimeZoneOffset :
-          this.computeTimeZoneOffset(start)
-      },
-      {
-        marker: end,
-        timeZoneOffset: dateOptions.forcedEndTimeZoneOffset != null ?
-          dateOptions.forcedEndTimeZoneOffset :
-          this.computeTimeZoneOffset(end)
-      },
-      this // yuck
-    )
+  // Adding / Subtracting
+
+  add(marker: DateMarker, dur: Duration): DateMarker {
+    let a = this.calendarSystem.markerToArray(marker)
+    a[0] += dur.year
+    a[1] += dur.month
+    a[2] += dur.day
+    a[6] += dur.time
+    return this.calendarSystem.arrayToMarker(a)
   }
 
-  toFormat(marker: DateMarker, formatter: DateFormatter, extraOptions: any = {}) {
-    return formatter.format(
-      {
-        marker: marker,
-        timeZoneOffset: extraOptions.forcedTimeZoneOffset != null ?
-          extraOptions.forcedTimeZoneOffset :
-          this.computeTimeZoneOffset(marker)
-      },
-      null,
-      this
-    )
+  subtract(marker: DateMarker, dur: Duration): DateMarker {
+    let a = this.calendarSystem.markerToArray(marker)
+    a[0] -= dur.year
+    a[1] -= dur.month
+    a[2] -= dur.day
+    a[6] -= dur.time
+    return this.calendarSystem.arrayToMarker(a)
   }
 
-  toIso(marker: DateMarker, extraOptions: any = {}) {
-    return buildIsoString(
-      marker,
-      extraOptions.forcedTimeZoneOffset != null ?
-        extraOptions.forcedTimeZoneOffset :
-        this.computeTimeZoneOffset(marker),
-      extraOptions.omitTime
-    )
+  addYears(marker: DateMarker, n: number) {
+    let a = this.calendarSystem.markerToArray(marker)
+    a[0] += n
+    return this.calendarSystem.arrayToMarker(a)
   }
 
-  createMarker(input: DateInput) {
-    return this.createMarkerMeta(input).marker
+  addMonths(marker: DateMarker, n: number) {
+    let a = this.calendarSystem.markerToArray(marker)
+    a[1] += n
+    return this.calendarSystem.arrayToMarker(a)
   }
 
-  // returns an object that wraps the marker!
-  createMarkerMeta(input: DateInput) {
-    if (typeof input === 'string') {
-      return this.parse(input)
-    } else if (typeof input === 'number') {
-      return { marker: this.timestampToMarker(input), isTimeUnspecified: false, forcedTimeZoneOffset: null }
-    } else if (isNativeDate(input)) {
-      return { marker: this.dateToMarker(input as Date), isTimeUnspecified: false, forcedTimeZoneOffset: null }
-    } else if (Array.isArray(input)) {
-      return { marker: arrayToUtcDate(input), isTimeUnspecified: false, forcedTimeZoneOffset: null }
+
+  // Diffing Whole Units
+
+  diffWholeYears(m0: DateMarker, m1: DateMarker): number {
+    let { calendarSystem } = this
+
+    if (
+      timeAsMs(m0) === timeAsMs(m1) &&
+      calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) &&
+      calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)
+    ) {
+      return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)
     }
     return null
   }
 
-  parse(str: string) {
-    let parts = parse(str)
-    let marker = parts.marker
-    let forcedTimeZoneOffset = null
+  diffWholeMonths(m0: DateMarker, m1: DateMarker): number {
+    let { calendarSystem } = this
 
-    if (parts.timeZoneOffset != null) {
-      if (this.canComputeTimeZoneOffset()) { // can get rid of this now?
-        marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000)
-      } else {
-        forcedTimeZoneOffset = parts.timeZoneOffset
-      }
+    if (
+      timeAsMs(m0) === timeAsMs(m1) &&
+      calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)
+    ) {
+      return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
+          (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12
     }
-
-    return { marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTimeZoneOffset }
+    return null
   }
 
-  dateToMarker(date: Date) {
-    return this.timestampToMarker(date.valueOf())
-  }
 
-  timestampToMarker(ms: number) {
-    if (this.timeZone === 'UTC') {
-      return new Date(ms)
-    } else if (this.timeZone === 'local') {
-      return arrayToUtcDate(dateToLocalArray(new Date(ms)))
-    } else {
-      throw 'need tz system!!!'
-    }
-  }
+  // Range / Duration
 
-  computeGreatestDenominator(m0: DateMarker, m1: DateMarker) {
+  greatestWholeUnit(m0: DateMarker, m1: DateMarker) {
     let n = this.diffWholeYears(m0, m1)
 
     if (n !== null) {
@@ -313,13 +208,13 @@ export class DateEnv {
       return { unit: 'month', value: n }
     }
 
-    n = this.diffWholeWeeks(m0, m1)
+    n = diffWholeWeeks(m0, m1)
 
     if (n !== null) {
-      return { unit: 'week', value: n / 7 }
+      return { unit: 'week', value: n }
     }
 
-    n = this.diffWholeDays(m0, m1)
+    n = diffWholeDays(m0, m1)
 
     if (n !== null) {
       return { unit: 'day', value: n }
@@ -327,29 +222,29 @@ export class DateEnv {
 
     n = diffHours(m0, m1)
 
-    if (n !== null) {
+    if (isInt(n)) {
       return { unit: 'hour', value: n }
     }
 
     n = diffMinutes(m0, m1)
 
-    if (n !== null) {
+    if (isInt(n)) {
       return  { unit: 'minute', value: n }
     }
 
     n = diffSeconds(m0, m1)
 
-    if (n !== null) {
+    if (isInt(n)) {
       return { unit: 'second', value: n }
     }
 
     return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() }
   }
 
-  divideRangeByWholeDuration(m0: DateMarker, m1: DateMarker, d: Duration) {
+  countDurationsBetween(m0: DateMarker, m1: DateMarker, d: Duration) {
     let cnt = 0
 
-    while (m0 < m1) { // not optimal
+    while (m0 < m1) { // better way to do this without iterating?
       m0 = this.add(m0, d)
       cnt++
     }
@@ -357,171 +252,158 @@ export class DateEnv {
     return cnt
   }
 
-  diffWholeYears(m0: DateMarker, m1: DateMarker): number {
-    let { calendarSystem } = this
 
-    if (
-      m0.getUTCMilliseconds() === m1.getUTCMilliseconds() && // TODO: util for time
-      m0.getUTCSeconds() === m1.getUTCSeconds() &&
-      m0.getUTCMinutes() === m1.getUTCMinutes() &&
-      m0.getUTCHours() === m1.getUTCHours() &&
-      calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) &&
-      calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)
-    ) {
-      return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)
-    }
-    return null
-  }
-
-  diffWholeMonths(m0: DateMarker, m1: DateMarker): number {
-    let { calendarSystem } = this
+  // Start-Of
 
-    if (
-      m0.getUTCMilliseconds() === m1.getUTCMilliseconds() &&
-      m0.getUTCSeconds() === m1.getUTCSeconds() &&
-      m0.getUTCMinutes() === m1.getUTCMinutes() &&
-      m0.getUTCHours() === m1.getUTCHours() &&
-      calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)
-    ) {
-      return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
-          (calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12
+  startOf(m: DateMarker, unit: string) {
+    if (unit === 'year') {
+      return this.startOfYear(m)
+    } else if (unit === 'month') {
+      return this.startOfMonth(m)
+    } else if (unit === 'week') {
+      return this.startOfWeek(m)
+    } else if (unit === 'day') {
+      return startOfDay(m)
+    } else if (unit === 'hour') {
+      return startOfHour(m)
+    } else if (unit === 'minute') {
+      return startOfMinute(m)
+    } else if (unit === 'second') {
+      return startOfSecond(m)
     }
-    return null
   }
 
-  diffWholeWeeks(m0: DateMarker, m1: DateMarker): number {
-    let d = this.diffWholeDays(m0, m1)
-
-    if (d !== null && d % 7 === 0) {
-      return d / 7
-    }
-
-    return null
+  startOfYear(m: DateMarker): DateMarker {
+    return this.calendarSystem.arrayToMarker([
+      this.calendarSystem.getMarkerYear(m)
+    ])
   }
 
-  diffWholeDays(m0: DateMarker, m1: DateMarker): number {
-    if (
-      m0.getUTCMilliseconds() === m1.getUTCMilliseconds() &&
-      m0.getUTCSeconds() === m1.getUTCSeconds() &&
-      m0.getUTCMinutes() === m1.getUTCMinutes() &&
-      m0.getUTCHours() === m1.getUTCHours()
-    ) {
-      return Math.round(diffDays(m0, m1))
-    }
-    return null
+  startOfMonth(m: DateMarker): DateMarker {
+    return this.calendarSystem.arrayToMarker([
+      this.calendarSystem.getMarkerYear(m),
+      this.calendarSystem.getMarkerMonth(m)
+    ])
   }
 
-  diffDayAndTime(m0: DateMarker, m1: DateMarker): Duration {
-    let m0day = this.startOfDay(m0)
-    let m1day = this.startOfDay(m1)
-
-    return {
-      year: 0,
-      month: 0,
-      day: Math.round((m1day.valueOf() - m0day.valueOf()) / MS_IN_DAY),
-      time: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf())
-    }
+  startOfWeek(m: DateMarker): DateMarker {
+    return addDays(m, -((m.getUTCDay() - this.weekDow + 7) % 7))
   }
 
-  startOfDay: (marker: DateMarker) => DateMarker
 
-}
+  // Week Number
 
-DateEnv.prototype.startOfDay = startOfDay
-
-
-function weekOfYear(marker, dow, doy) {
-  let y = marker.getUTCFullYear()
-  let w = weekOfGivenYear(marker, y, dow, doy)
-
-  if (w < 1) {
-    return weekOfGivenYear(marker, y - 1, dow, doy)
+  computeWeekNumber(marker: DateMarker): number {
+    if (this.weekNumberFunc) {
+      return this.weekNumberFunc(this.toDate(marker))
+    } else {
+      return weekOfYear(marker, this.weekDow, this.weekDoy)
+    }
   }
 
-  let nextW = weekOfGivenYear(marker, y + 1, dow, doy)
-  if (nextW >= 1) {
-    return Math.min(w, nextW)
+  format(marker: DateMarker, formatter: DateFormatter, dateOptions: any = {}) {
+    return formatter.format(
+      {
+        marker: marker,
+        timeZoneOffset: dateOptions.forcedTimeZoneOffset != null ?
+          dateOptions.forcedTimeZoneOffset :
+          this.offsetForMarker(marker)
+      },
+      this
+    )
   }
 
-  return w
-}
-
-
-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(days / 7) + 1 // zero-indexed
-}
-
-
-function startOfDay(marker: DateMarker): DateMarker {
-  return arrayToUtcDate([
-    marker.getUTCFullYear(),
-    marker.getUTCMonth(),
-    marker.getUTCDate()
-  ])
-}
-
-
-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
-}
-
-
-// start-of-first-week - start-of-year
-function firstWeekOffset(year, dow, doy) {
-  var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
-      fwd = 7 + dow - doy,
-      // first-week day local weekday -- which local weekday is fwd
-      fwdlw = (7 + arrayToUtcDate([ year, 0, fwd ]).getUTCDay() - dow) % 7;
-  return -fwdlw + fwd - 1;
-}
+  formatRange(start: DateMarker, end: DateMarker, formatter: DateFormatter, dateOptions: any = {}) {
 
+    if (dateOptions.isEndExclusive) {
+      end = addMs(end, -1)
+    }
 
-function isNativeDate(input) {
-  return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date
-}
+    return formatter.formatRange(
+      {
+        marker: start,
+        timeZoneOffset: dateOptions.forcedStartTimeZoneOffset != null ?
+          dateOptions.forcedStartTimeZoneOffset :
+          this.offsetForMarker(start)
+      },
+      {
+        marker: end,
+        timeZoneOffset: dateOptions.forcedEndTimeZoneOffset != null ?
+          dateOptions.forcedEndTimeZoneOffset :
+          this.offsetForMarker(end)
+      },
+      this
+    )
+  }
 
+  formatIso(marker: DateMarker, extraOptions: any = {}) {
+    return buildIsoString(
+      marker,
+      extraOptions.forcedTimeZoneOffset != null ?
+        extraOptions.forcedTimeZoneOffset :
+        this.offsetForMarker(marker),
+      extraOptions.omitTime
+    )
+  }
 
+  formatWeek(marker: DateMarker, fit?: 'numeric' | 'narrow' | 'short'): string {
+    let { locale } = this
+    let parts = []
 
-const MS_IN_HOUR = 1000 * 60 * 60
-const MS_IN_MINUTE = 1000 * 60
+    if (fit === 'narrow') {
+      parts.push(locale.options.weekHeader)
+    } else if (fit === 'short') {
+      parts.push(locale.options.weekHeader, ' ')
+    }
+    // otherwise, considered 'numeric'
 
+    parts.push(this.computeWeekNumber(marker))
 
-function diffHours(m0, m1) {
-  let ms = m1.valueOf() - m0.valueOf()
+    if (locale.options.isRTL) {
+      parts.reverse()
+    }
 
-  if (ms % MS_IN_HOUR === 0) {
-    return ms / MS_IN_HOUR
+    return parts.join('')
   }
 
-  return null
-}
-
 
-function diffMinutes(m0, m1) {
-  let ms = m1.valueOf() - m0.valueOf()
+  // TimeZone
 
-  if (ms % MS_IN_MINUTE === 0) {
-    return ms / MS_IN_MINUTE
+  timestampToMarker(ms: number) {
+    if (this.timeZone === 'local') {
+      return arrayToUtcDate(dateToLocalArray(new Date(ms)))
+    } else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
+      return new Date(ms)
+    } else {
+      return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms))
+    }
   }
 
-  return null
-}
+  offsetForMarker(m: DateMarker) {
+    if (this.timeZone === 'local') {
+      return arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset()
+    } else if (this.timeZone === 'UTC') {
+      return m.getTimezoneOffset()
+    } else if (this.namedTimeZoneImpl) {
+      return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m))
+    }
+    return null
+  }
 
 
-function diffSeconds(m0, m1) {
-  let ms = m1.valueOf() - m0.valueOf()
+  // Conversion
 
-  if (ms % 1000 === 0) {
-    return ms / 1000
+  toDate(m: DateMarker): Date {
+    if (this.timeZone === 'local') {
+      return arrayToLocalDate(dateToUtcArray(m))
+    } else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
+      return new Date(m.valueOf()) // make sure it's a copy
+    } else {
+      return new Date(
+        m.valueOf() -
+        this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m))
+      )
+    }
   }
 
-  return null
 }

+ 34 - 0
src/datelib/formatting-cmd.ts

@@ -0,0 +1,34 @@
+import {
+  DateFormatter, DateFormattingContext, ZonedMarker,
+  VerboseFormattingArg, createVerboseFormattingArg
+} from './formatting'
+
+export type CmdFormatterFunc = (cmd: string, arg: VerboseFormattingArg) => string
+
+
+let soleCmdFunc: CmdFormatterFunc = null
+
+export function registerCmdFormatter(name, input: CmdFormatterFunc) {
+  if (!soleCmdFunc) {
+    soleCmdFunc = input
+  }
+}
+
+
+export class CmdFormatter implements DateFormatter {
+
+  cmdStr: string
+
+  constructor(cmdStr: string) {
+    this.cmdStr = cmdStr
+  }
+
+  format(date: ZonedMarker, context: DateFormattingContext) {
+    return soleCmdFunc(this.cmdStr, createVerboseFormattingArg(date, null, context))
+  }
+
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext) {
+    return soleCmdFunc(this.cmdStr, createVerboseFormattingArg(start, end, context))
+  }
+
+}

+ 25 - 0
src/datelib/formatting-func.ts

@@ -0,0 +1,25 @@
+import {
+  DateFormatter, DateFormattingContext, ZonedMarker,
+  VerboseFormattingArg, createVerboseFormattingArg
+} from './formatting'
+
+export type FuncFormatterFunc = (arg: VerboseFormattingArg) => string
+
+
+export class FuncFormatter implements DateFormatter {
+
+  func: FuncFormatterFunc
+
+  constructor(func: FuncFormatterFunc) {
+    this.func = func
+  }
+
+  format(date: ZonedMarker, context: DateFormattingContext) {
+    return this.func(createVerboseFormattingArg(date, null, context))
+  }
+
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext) {
+    return this.func(createVerboseFormattingArg(start, end, context))
+  }
+
+}

+ 173 - 0
src/datelib/formatting-native.ts

@@ -0,0 +1,173 @@
+import { DateMarker, timeAsMs } from './marker'
+import { CalendarSystem } from './calendar-system'
+import { DateFormatter, DateFormattingContext, ZonedMarker, formatTimeZoneOffset } from './formatting'
+
+
+const DEFAULT_SEPARATOR = ' - '
+
+
+export class NativeFormatter implements DateFormatter {
+
+  standardSettings: any
+  extendedSettings: any
+
+  constructor(formatSettings) {
+    let groups = separateExtendedSettings(formatSettings)
+    this.standardSettings = groups.standard
+    this.standardSettings.timeZone = 'UTC'
+    this.extendedSettings = groups.extended
+  }
+
+  format(date: ZonedMarker, context: DateFormattingContext) {
+    return formatZonedMarker(date, context, this.standardSettings)
+  }
+
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext) {
+    let { standardSettings } = this
+
+    let diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem)
+    if (!diffSeverity) {
+      return formatZonedMarker(start, context, standardSettings)
+    }
+
+    let biggestUnitForPartial = diffSeverity
+    if (
+      biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time
+      (standardSettings.year === 'numeric' || standardSettings.year === '2-digit') &&
+      (standardSettings.month === 'numeric' || standardSettings.month === '2-digit') &&
+      (standardSettings.day === 'numeric' || standardSettings.day === '2-digit')
+    ) {
+      biggestUnitForPartial = 1 // make it look like the dates are only different in terms of time
+    }
+
+    let full0 = formatZonedMarker(start, context, standardSettings)
+    let full1 = formatZonedMarker(end, context, standardSettings)
+    let partialFormatSettings = computePartialFormattingOptions(standardSettings, biggestUnitForPartial)
+    let partial0 = formatZonedMarker(start, context, partialFormatSettings)
+    let partial1 = formatZonedMarker(end, context, partialFormatSettings)
+    let insertion = findCommonInsertion(full0, partial0, full1, partial1)
+    let separator = this.extendedSettings.separator || DEFAULT_SEPARATOR
+
+    if (insertion) {
+      return insertion.before + partial0 + separator + partial1 + insertion.after
+    }
+
+    return full0 + separator + full1
+  }
+
+}
+
+function separateExtendedSettings(settings) {
+  let standardSettings = {}
+  let extendedSettings = {}
+
+  for (let name in settings) {
+    if (name === 'separator') {
+      extendedSettings[name] = settings[name]
+    } else {
+      standardSettings[name] = settings[name]
+    }
+  }
+
+  return { standard: standardSettings, extended: extendedSettings }
+}
+
+
+// General Formatting Utils
+
+function formatZonedMarker(date: ZonedMarker, context: DateFormattingContext, standardSettings) {
+  let s = date.marker.toLocaleString(context.locale, standardSettings)
+
+  if (standardSettings.timeZoneName && date.timeZoneOffset != null && context.timeZone !== 'UTC') {
+    s = s.replace(/UTC|GMT/, formatTimeZoneOffset(date.timeZoneOffset))
+  }
+
+  return s
+}
+
+
+// Range Formatting Utils
+
+const SEVERITIES_FOR_PARTS = {
+  year: 4,
+  month: 3,
+  day: 2,
+  weekday: 2,
+  hour: 1,
+  minute: 1,
+  second: 1
+}
+
+// 0 = exactly the same
+// 1 = different by time
+// 2 = different by day
+// 3 = different by month
+// 4 = different by year
+function computeMarkerDiffSeverity(d0: DateMarker, d1: DateMarker, ca: CalendarSystem) {
+  if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) {
+    return 4
+  }
+  if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) {
+    return 3
+  }
+  if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) {
+    return 2
+  }
+  if (timeAsMs(d0) !== timeAsMs(d1)) {
+    return 1
+  }
+  return 0
+}
+
+function computePartialFormattingOptions(options, biggestUnit) {
+  let partialOptions = {}
+
+  for (let name in options) {
+    if (
+      name === 'timeZone' ||
+      SEVERITIES_FOR_PARTS[name] <= biggestUnit // if undefined, will always be false
+    ) {
+      partialOptions[name] = options[name]
+    }
+  }
+
+  return partialOptions
+}
+
+function findCommonInsertion(full0, partial0, full1, partial1) {
+
+  let i0 = 0
+  while (i0 < full0.length) {
+
+    let found0 = full0.indexOf(partial0, i0)
+    if (found0 === -1) {
+      break
+    }
+
+    let before0 = full0.substr(0, found0)
+    i0 = found0 + partial0.length
+    let after0 = full0.substr(i0)
+
+    let i1 = 0
+    while (i1 < full1.length) {
+
+      let found1 = full1.indexOf(partial1, i1)
+      if (found1 === -1) {
+        break
+      }
+
+      let before1 = full1.substr(0, found1)
+      i1 = found1 + partial1.length
+      let after1 = full1.substr(i1)
+
+      if (before0 === before1 && after0 === after1) {
+        return {
+          before: before0,
+          after: after0
+        }
+      }
+    }
+  }
+
+  return null
+}

+ 65 - 329
src/datelib/formatting.ts

@@ -1,205 +1,17 @@
-import { DateMarker } from './util'
+import { DateMarker } from './marker'
 import { CalendarSystem } from './calendar-system'
-import { assignTo } from '../util/object'
-import { DateEnv } from './env'
-
+import { Locale } from './locale'
+import { NativeFormatter } from './formatting-native'
+import { CmdFormatter } from './formatting-cmd'
+import { FuncFormatter } from './formatting-func'
 
 export interface ZonedMarker {
   marker: DateMarker,
   timeZoneOffset: number
 }
 
-export interface ExpandedZonedMarker extends ZonedMarker {
-  arr: number[]
-}
-
-export interface DateFormatter {
-  format(start: ZonedMarker, end: ZonedMarker, env: DateEnv)
-}
-
-
-// TODO: optimize by only using DateTimeFormat?
-
-
-
-
-
-const DEFAULT_SEPARATOR = ' - '
-
-const SEVERITIES_FOR_PARTS = {
-  year: 4,
-  month: 3,
-  day: 2,
-  weekday: 2,
-  hour: 1,
-  minute: 1,
-  second: 1
-}
-
-class NativeFormatter implements DateFormatter {
-
-  formatSettings: any
-  separator: string // for ranges
-
-  constructor(formatSettings) {
-    formatSettings = assignTo({}, formatSettings, { timeZone: 'UTC' })
-
-    if (formatSettings.separator) {
-      this.separator = formatSettings.separator
-      delete formatSettings.separator
-    }
-
-    this.formatSettings = formatSettings
-  }
-
-  format(start: ZonedMarker, end: ZonedMarker, env: DateEnv) {
-    let { formatSettings } = this
-
-    // need to format the timeZone name in a zone other than UTC?
-    if (formatSettings.timeZoneName && env.timeZone !== 'UTC') {
-      formatSettings = assignTo({}, formatSettings) // copy
-
-      if (start.timeZoneOffset == null || end && end.timeZoneOffset == null) {
-        delete formatSettings.timeZoneName // don't have necessary tzo into. don't even try
-      } else {
-        formatSettings.timeZoneName = 'short' // only know how to display offset info for short (+00:00)
-      }
-    }
-
-    if (end) {
-      return formatRange(start, end, env, formatSettings, this.separator || DEFAULT_SEPARATOR)
-    } else {
-      return formatZonedMarker(start, env.locale, env.timeZone, formatSettings)
-    }
-  }
-
-}
-
-
-
-
-function formatRange(start: ZonedMarker, end: ZonedMarker, env: DateEnv, formatSettings: any, separator: string) {
-
-  let diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, env.calendarSystem)
-  if (!diffSeverity) {
-    return formatZonedMarker(start, env.locale, env.timeZone, formatSettings)
-  }
-
-  let biggestUnitForPartial = diffSeverity
-  if (
-    biggestUnitForPartial > 1 && // hour/min/sec/ms
-    (formatSettings.year === 'numeric' || formatSettings.year === '2-digit') &&
-    (formatSettings.month === 'numeric' || formatSettings.month === '2-digit') &&
-    (formatSettings.day === 'numeric' || formatSettings.day === '2-digit')
-  ) {
-    biggestUnitForPartial = 1
-  }
-
-  let full0 = formatZonedMarker(start, env.locale, env.timeZone, formatSettings)
-  let full1 = formatZonedMarker(end, env.locale, env.timeZone, formatSettings)
-  let partialFormatSettings = computePartialFormattingOptions(formatSettings, biggestUnitForPartial)
-  let partial0 = formatZonedMarker(start, env.locale, env.timeZone, partialFormatSettings)
-  let partial1 = formatZonedMarker(end, env.locale, env.timeZone, partialFormatSettings)
-  let insertion = findCommonInsertion(full0, partial0, full1, partial1)
-
-  if (insertion) {
-    return insertion.before + partial0 + separator + partial1 + insertion.after;
-  }
-
-  return full0 + separator + full1;
-}
-
-// 0 = exactly the same
-// 1 = different by time
-// 2 = different by day
-// 3 = different by month
-// 4 = different by year
-function computeMarkerDiffSeverity(d0: DateMarker, d1: DateMarker, ca: CalendarSystem) {
-  if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) {
-    return 4;
-  }
-  if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) {
-    return 3;
-  }
-  if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) {
-    return 2;
-  }
-  if (
-    d0.getUTCHours() !== d1.getUTCHours() ||
-    d0.getUTCMinutes() !== d1.getUTCMinutes() ||
-    d0.getUTCSeconds() !== d1.getUTCSeconds() ||
-    d0.getUTCMilliseconds() !== d1.getUTCMilliseconds()
-  ) {
-    return 1;
-  }
-  return 0;
-}
-
-function computePartialFormattingOptions(options, biggestUnit) {
-  var partialOptions = {};
-  var name;
-
-  for (name in options) {
-    if (
-      name === 'timeZone' ||
-      SEVERITIES_FOR_PARTS[name] <= biggestUnit // if undefined, will always be false
-    ) {
-      partialOptions[name] = options[name];
-    }
-  }
-
-  return partialOptions;
-}
-
-function findCommonInsertion(full0, partial0, full1, partial1) {
-  var i0, i1;
-  var found0, found1;
-  var before0, after0;
-  var before1, after1;
-
-  i0 = 0;
-  while (i0 < full0.length) {
-    found0 = full0.indexOf(partial0, i0);
-    if (found0 === -1) {
-      break;
-    }
-
-    before0 = full0.substr(0, found0);
-    i0 = found0 + partial0.length;
-    after0 = full0.substr(i0);
-
-    i1 = 0;
-    while (i1 < full1.length) {
-      found1 = full1.indexOf(partial1, i1);
-      if (found1 === -1) {
-        break;
-      }
-
-      before1 = full1.substr(0, found1);
-      i1 = found1 + partial1.length;
-      after1 = full1.substr(i1);
-
-      if (before0 === before1 && after0 === after1) {
-        return {
-          before: before0,
-          after: after0
-        }
-      }
-    }
-  }
-
-  return null;
-}
-
-
-
-
-
-
-
-
-
-export interface FuncFormatterDate extends ExpandedZonedMarker {
+export interface ExpandedZoneMarker extends ZonedMarker {
+  array: number[],
   year: number,
   month: number,
   day: number,
@@ -209,180 +21,104 @@ export interface FuncFormatterDate extends ExpandedZonedMarker {
   millisecond: number
 }
 
-export interface FuncFormatterArg {
-  date: FuncFormatterDate
-  start: FuncFormatterDate
-  end?: FuncFormatterDate
-  timeZone: string
-  locale: string
-}
-
-export type funcFormatterFunc = (arg: FuncFormatterArg) => string
-
-class FuncFormatter implements DateFormatter {
-
-  func: funcFormatterFunc
-
-  constructor(func: funcFormatterFunc) {
-    this.func = func
-  }
-
-  format(start: ZonedMarker, end: ZonedMarker, env: DateEnv) {
-    let startInfo = dateForFuncFormatter(start, env)
-    let endInfo = end ? dateForFuncFormatter(end, env) : null
-
-    return this.func({
-      date: startInfo,
-      start: startInfo,
-      end: endInfo,
-      timeZone: env.timeZone,
-      locale: env.locale
-    })
-  }
-
-}
-
-function dateForFuncFormatter(dateInfo: ZonedMarker, env: DateEnv): FuncFormatterDate {
-  let arr = env.calendarSystem.markerToArray(dateInfo.marker)
-  return {
-    marker: dateInfo.marker,
-    timeZoneOffset: dateInfo.timeZoneOffset,
-    arr: arr,
-    year: arr[0],
-    month: arr[1],
-    day: arr[2],
-    hour: arr[3],
-    minute: arr[4],
-    second: arr[5],
-    millisecond: arr[6],
-  }
-}
-
-
-
-
-
-
-
-export interface CmdStrFormatterArg {
-  date: ExpandedZonedMarker
-  start: ExpandedZonedMarker
-  end?: ExpandedZonedMarker
+export interface VerboseFormattingArg {
+  date: ExpandedZoneMarker
+  start: ExpandedZoneMarker
+  end?: ExpandedZoneMarker
   timeZone: string
-  locale: string
-}
-
-export type cmdStrFormatterFunc = (cmd: string, arg: CmdStrFormatterArg) => string
-
-let soleCmdStrProcessor: cmdStrFormatterFunc = null
-
-export function registerCmdStrProcessor(name, input: cmdStrFormatterFunc) {
-  if (!soleCmdStrProcessor) {
-    soleCmdStrProcessor = input
-  }
+  localeIds: string[]
 }
 
-class CmdStrFormatter implements DateFormatter {
-
-  cmdStr: string
-
-  constructor(cmdStr: string) {
-    this.cmdStr = cmdStr
-  }
-
-  format(start: ZonedMarker, end: ZonedMarker, env: DateEnv) {
-    let startInfo = dateInfoForCmdStrFormatter(start, env)
-    let endInfo = end ? dateInfoForCmdStrFormatter(end, env) : null
-
-    return soleCmdStrProcessor(this.cmdStr, {
-      date: startInfo,
-      start: startInfo,
-      end: endInfo,
-      timeZone: env.timeZone,
-      locale: env.locale
-    })
-  }
-
+export interface DateFormattingContext {
+  timeZone: string,
+  locale: Locale,
+  calendarSystem: CalendarSystem
 }
 
-function dateInfoForCmdStrFormatter(dateInfo: ZonedMarker, env: DateEnv): ExpandedZonedMarker {
-  return {
-    marker: dateInfo.marker,
-    timeZoneOffset: dateInfo.timeZoneOffset,
-    arr: env.calendarSystem.markerToArray(dateInfo.marker)
-  }
+export interface DateFormatter {
+  format(date: ZonedMarker, context: DateFormattingContext)
+  formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext)
 }
 
 
-
-
-
-
-
-
+// Formatter Object Creation
 
 export function createFormatter(input): DateFormatter {
-  if (typeof input === 'string') {
-    return new CmdStrFormatter(input)
+  if (typeof input === 'object') {
+    return new NativeFormatter(input)
+  }
+  else if (typeof input === 'string') {
+    return new CmdFormatter(input)
   }
   else if (typeof input === 'function') {
     return new FuncFormatter(input)
   }
-  else if (typeof input === 'object') {
-    return new NativeFormatter(input)
-  }
 }
 
 
+// String Utils
 
+export function buildIsoString(marker: DateMarker, timeZoneOffset?: number, stripZeroTime: boolean = false) {
+  let s = marker.toISOString()
 
+  s = s.replace('.000', '')
+  s = s.replace('Z', '')
 
-function formatPrettyTimeZoneOffset(minutes: number) { // combine these two???
-  let sign = minutes < 0 ? '+' : '-' // whaaa
-  let abs = Math.abs(minutes)
-  let hours = Math.floor(abs / 60)
-  let mins = Math.round(abs % 60)
+  if (timeZoneOffset != null) { // provided?
+    s += formatTimeZoneOffset(timeZoneOffset, true)
+  } else if (stripZeroTime) {
+    s = s.replace('T00:00:00', '')
+  }
 
-  return 'GMT' + sign + hours + (mins ? ':' + pad(mins) : '')
+  return s
 }
 
-function formatIsoTimeZoneOffset(minutes: number) {
+export function formatTimeZoneOffset(minutes: number, doIso = false) {
   let sign = minutes < 0 ? '+' : '-' // whaaa
   let abs = Math.abs(minutes)
   let hours = Math.floor(abs / 60)
   let mins = Math.round(abs % 60)
 
-  return sign + pad(hours) + ':' + pad(mins)
+  if (doIso) {
+    return sign + pad(hours) + ':' + pad(mins)
+  } else {
+    return 'GMT' + sign + hours + (mins ? ':' + pad(mins) : '')
+  }
 }
 
-
 function pad(n) {
   return n < 10 ? '0' + n : '' + n
 }
 
 
-export function buildIsoString(marker: DateMarker, timeZoneOffset?: number, stripZeroTime: boolean = false) {
-  let s = marker.toISOString()
+// Arg Utils
 
-  s = s.replace('.000', '')
-  s = s.replace('Z', '')
+export function createVerboseFormattingArg(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext) {
+  let startInfo = expandZonedMarker(start, context.calendarSystem)
+  let endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null
 
-  if (timeZoneOffset != null) { // provided?
-    s += formatIsoTimeZoneOffset(timeZoneOffset)
-  } else if (stripZeroTime) {
-    s = s.replace('T00:00:00', '')
+  return {
+    date: startInfo,
+    start: startInfo,
+    end: endInfo,
+    timeZone: context.timeZone,
+    localeIds: context.locale.ids
   }
-
-  return s
 }
 
+function expandZonedMarker(dateInfo: ZonedMarker, calendarSystem: CalendarSystem): ExpandedZoneMarker {
+  let a = calendarSystem.markerToArray(dateInfo.marker)
 
-function formatZonedMarker(d: ZonedMarker, locale: string, desiredTimeZone: string, formatSettings: any) {
-  let s = d.marker.toLocaleString(locale, formatSettings)
-
-  if (formatSettings.timeZoneName && d.timeZoneOffset != null && desiredTimeZone !== 'UTC') {
-    s = s.replace(/UTC|GMT/, formatPrettyTimeZoneOffset(d.timeZoneOffset))
+  return {
+    marker: dateInfo.marker,
+    timeZoneOffset: dateInfo.timeZoneOffset,
+    array: a,
+    year: a[0],
+    month: a[1],
+    day: a[2],
+    hour: a[3],
+    minute: a[4],
+    second: a[5],
+    millisecond: a[6]
   }
-
-  return s
 }

+ 68 - 10
src/datelib/locale.ts

@@ -1,25 +1,83 @@
+import { mergeProps } from '../util/object'
 
-export interface LocaleData {
+export type LocaleQuery = string | string[]
+
+export interface Locale {
+  ids: string[]
   week: { dow: number, doy: number }
+  simpleNumberFormat: Intl.NumberFormat
+  options: any
 }
 
-let localeMap: { [name: string]: LocaleData } = {}
-
-const EN_LOCALE = {
+const RAW_EN_LOCALE = {
   week: {
     dow: 0, // Sunday is the first day of the week
     doy: 4 // 4 days need to be within the year to be considered the first week
-  }
+  },
+  isRTL: false,
+  buttonText: {
+    prev: 'prev',
+    next: 'next',
+    prevYear: 'prev year',
+    nextYear: 'next year',
+    year: 'year',
+    today: 'today',
+    month: 'month',
+    week: 'week',
+    day: 'day',
+    list: 'list',
+  },
+  weekHeader: 'Wk',
+  allDayText: 'all-day',
+  eventLimitText: 'more',
+  noEventsMessage: 'No events to display'
 }
 
+let rawMap = {}
+
+export function getLocale(query: LocaleQuery): Locale {
+  let nativeQuery = query === 'auto' ? null : query // for instantiating Intl objects
+
+  let ids
+  if (Array.isArray(nativeQuery)) {
+    ids = nativeQuery
+  } else if (typeof nativeQuery === 'string') {
+    ids = [ nativeQuery ]
+  } else {
+    ids = []
+  }
+
+  let raw = getRawLocale(ids) || {}
+  let merged = mergeProps([ RAW_EN_LOCALE, raw ], [ 'buttonText' ])
 
-export function registerLocale(name: string, data: LocaleData) {
-  localeMap[name] = data
+  let week = merged.week
+  delete merged.week
+
+  return {
+    ids,
+    week,
+    simpleNumberFormat: new Intl.NumberFormat(nativeQuery),
+    options: merged
+  }
 }
 
-export function getLocale(name: string) {
-  return localeMap[name] || EN_LOCALE
+function getRawLocale(ids: string[]) {
+  for (let i = 0; i < ids.length; i++) {
+    let parts = ids[i].toLocaleLowerCase().split('-')
+
+    for (let j = parts.length; j > 0; j--) {
+      let simpleId = parts.slice(0, j).join('-')
+
+      if (rawMap[simpleId]) {
+        return rawMap[simpleId]
+      }
+    }
+  }
+  return null
 }
 
+export function defineLocale(simpleId: string, rawData) {
+  rawMap[simpleId] = rawData
+}
 
-registerLocale('en', EN_LOCALE)
+defineLocale('en', RAW_EN_LOCALE)

+ 0 - 40
src/datelib/main.ts

@@ -1,40 +0,0 @@
-import { DateEnv } from './env'
-import { createFormatter } from './formatting'
-import { DateMarker, nowMarker } from './util'
-//import './moment'
-//import './moment-timezone'
-
-let env = new DateEnv({
-  timeZone: 'Asia/Hong_Kong',
-  timeZoneImpl: 'UTC-coercion', //'moment-timezone',
-  calendarSystem: 'gregorian',
-  locale: 'es' // TODO: what about 'auto'?
-})
-
-let start: DateMarker = nowMarker()
-let end: DateMarker = env.startOfMonth(start)
-
-let formatter = createFormatter({
-  year: 'numeric',
-  month: 'long',
-  //weekday: 'long',
-  day: 'numeric',
-  //hour: '2-digit',
-  //minute: '2-digit',
-  //hour12: true,
-  //timeZoneName: 'long'
-})
-
-// let formatter = createFormatter(function() {
-//   debugger
-//   return '!!!'
-// })
-
-// let formatter = createFormatter('dddd, MMMM Do YYYY, h:mm:ss a Z')
-
-console.log(
-  env.toRangeFormat(end, start, formatter, {
-    forcedStartTimeZoneOffset: 60,
-    forcedEndTimeZoneOffset: 60
-  })
-)

+ 208 - 0
src/datelib/marker.ts

@@ -0,0 +1,208 @@
+import { Duration } from './duration'
+
+export type DateMarker = Date
+
+
+// Adding
+
+export function addWeeks(m: DateMarker, n: number) {
+  let a = dateToUtcArray(m)
+  a[2] += n * 7
+  return arrayToUtcDate(a)
+}
+
+export function addDays(m: DateMarker, n: number) {
+  let a = dateToUtcArray(m)
+  a[2] += n
+  return arrayToUtcDate(a)
+}
+
+export function addMs(m: DateMarker, n: number) {
+  let a = dateToUtcArray(m)
+  a[6] += n
+  return arrayToUtcDate(a)
+}
+
+
+// Diffing (all return floats)
+
+export function diffWeeks(m0, m1) {
+  return diffDays(m0, m1) / 7
+}
+
+export function diffDays(m0, m1) {
+  return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24)
+}
+
+export function diffHours(m0, m1) {
+  return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60)
+}
+
+export function diffMinutes(m0, m1) {
+  return (m1.valueOf() - m0.valueOf()) / (1000 * 60)
+}
+
+export function diffSeconds(m0, m1) {
+  return (m1.valueOf() - m0.valueOf()) / 1000
+}
+
+export function diffDayAndTime(m0: DateMarker, m1: DateMarker): Duration {
+  let m0day = this.startOfDay(m0)
+  let m1day = this.startOfDay(m1)
+
+  return {
+    year: 0,
+    month: 0,
+    day: Math.round(diffDays(m1day, m0day)),
+    time: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf())
+  }
+}
+
+
+// Diffing Whole Units
+
+export function diffWholeWeeks(m0: DateMarker, m1: DateMarker): number {
+  let d = diffWholeDays(m0, m1)
+
+  if (d !== null && d % 7 === 0) {
+    return d / 7
+  }
+
+  return null
+}
+
+export function diffWholeDays(m0: DateMarker, m1: DateMarker): number {
+  if (timeAsMs(m0) === timeAsMs(m1)) {
+    return Math.round(diffDays(m0, m1))
+  }
+  return null
+}
+
+
+// Start-Of
+
+export function startOfDay(m: DateMarker): DateMarker {
+  return arrayToUtcDate([
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate()
+  ])
+}
+
+export function startOfHour(m: DateMarker) {
+  return arrayToUtcDate([
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours()
+  ])
+}
+
+export function startOfMinute(m: DateMarker) {
+  return arrayToUtcDate([
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours(),
+    m.getUTCMinutes()
+  ])
+}
+
+export function startOfSecond(m: DateMarker) {
+  return arrayToUtcDate([
+    m.getUTCFullYear(),
+    m.getUTCMonth(),
+    m.getUTCDate(),
+    m.getUTCHours(),
+    m.getUTCMinutes(),
+    m.getUTCSeconds()
+  ])
+}
+
+
+// Week Computation
+
+export function weekOfYear(marker, dow, doy) {
+  let y = marker.getUTCFullYear()
+  let w = weekOfGivenYear(marker, y, dow, doy)
+
+  if (w < 1) {
+    return weekOfGivenYear(marker, y - 1, dow, doy)
+  }
+
+  let nextW = weekOfGivenYear(marker, y + 1, dow, doy)
+  if (nextW >= 1) {
+    return Math.min(w, nextW)
+  }
+
+  return w
+}
+
+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(days / 7) + 1 // zero-indexed
+}
+
+// start-of-first-week - start-of-year
+function firstWeekOffset(year, dow, doy) {
+  var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
+      fwd = 7 + dow - doy,
+      // first-week day local weekday -- which local weekday is fwd
+      fwdlw = (7 + arrayToUtcDate([ year, 0, fwd ]).getUTCDay() - dow) % 7;
+  return -fwdlw + fwd - 1;
+}
+
+
+// Array Conversion
+
+export function dateToLocalArray(date) {
+  return [
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate(),
+    date.getHours(),
+    date.getMinutes(),
+    date.getSeconds(),
+    date.getMilliseconds()
+  ]
+}
+
+export function arrayToLocalDate(a) {
+  return new Date(
+    a[0],
+    a[1] || 0,
+    a[2] == null ? 1 : a[2], // day of month
+    a[3] || 0,
+    a[4] || 0,
+    a[5] || 0,
+  )
+}
+
+export function dateToUtcArray(date) {
+  return [
+    date.getUTCFullYear(),
+    date.getUTCMonth(),
+    date.getUTCDate(),
+    date.getUTCHours(),
+    date.getUTCMinutes(),
+    date.getUTCSeconds(),
+    date.getUTCMilliseconds()
+  ]
+}
+
+export function arrayToUtcDate(a) {
+  return new Date(Date.UTC.apply(Date, a))
+}
+
+
+// Other Utils
+
+export function timeAsMs(m: DateMarker) {
+  return m.getUTCHours() * 1000 * 60 * 60 +
+    m.getUTCMinutes() * 1000 * 60 +
+    m.getUTCSeconds() * 1000 +
+    m.getUTCMilliseconds()
+}

+ 16 - 5
src/datelib/moment-timezone.ts

@@ -1,8 +1,19 @@
 import * as moment from 'moment'
 import 'moment-timezone'
-import { registerNamedTimeZoneOffsetGenerator } from './timezone'
+import { NamedTimeZoneImpl, registerNamedTimeZoneImpl } from './timezone'
 
-registerNamedTimeZoneOffsetGenerator('moment-timezone', function(timeZoneName: string, array: number[]) {
-  // TODO: need to return ms!!!
-  return -(moment as any).tz(array, timeZoneName).utcOffset() // need negative!
-})
+
+class MomentNamedTimeZone extends NamedTimeZoneImpl {
+
+  offsetForArray(a: number[]): number {
+    return -(moment as any).tz(a, this.name).utcOffset()
+  }
+
+  timestampToArray(ms: number): number[] {
+    return (moment as any).tz(ms, this.name).toArray()
+  }
+
+}
+
+
+registerNamedTimeZoneImpl('moment-timezone', MomentNamedTimeZone)

+ 11 - 9
src/datelib/moment.ts

@@ -1,19 +1,21 @@
 import * as moment from 'moment'
-import { registerCmdStrProcessor } from './formatting'
+import { VerboseFormattingArg } from './formatting'
+import { registerCmdFormatter } from './formatting-cmd'
 
-registerCmdStrProcessor('moment', function(cmdStr: string, marker, params) {
+// TODO: what about range!!??
+
+registerCmdFormatter('moment', function(cmdStr: string, arg: VerboseFormattingArg) {
   let mom: moment.Moment
-  let arr = params.calendarSystem.markerToArray(marker)
 
-  if (params.timeZone === 'local') {
-    mom = moment(arr)
-  } else if (params.timeZone === 'UTC' || !(moment as any).tz) {
-    mom = moment.utc(arr)
+  if (arg.timeZone === 'local') {
+    mom = moment(arg.date.array)
+  } else if (arg.timeZone === 'UTC' || !(moment as any).tz) {
+    mom = moment.utc(arg.date.array)
   } else {
-    mom = (moment as any).tz(arr, params.timeZone)
+    mom = (moment as any).tz(arg.date.array, arg.timeZone)
   }
 
-  mom.locale(params.locale)
+  mom.locale(arg.localeIds[0])
 
   return mom.format(cmdStr)
 })

+ 7 - 1
src/datelib/parsing.ts

@@ -27,8 +27,14 @@ export function parse(str) {
     }
   }
 
+  let marker = new Date(str)
+
+  if (isNaN(marker.valueOf())) {
+    return null
+  }
+
   return {
-    marker: new Date(str),
+    marker,
     isTimeUnspecified,
     timeZoneOffset
   }

+ 19 - 6
src/datelib/timezone.ts

@@ -1,16 +1,29 @@
 
+export abstract class NamedTimeZoneImpl {
 
+  name: string
 
-export type namedTimeZoneOffsetGenerator = (timeZoneName: string, array: number[]) => number
+  constructor(name: string) {
+    this.name = name
+  }
+
+  abstract offsetForArray(a: number[]): number
+  abstract timestampToArray(ms: number): number[]
+}
 
-let namedTimeZoneOffsetGeneratorMap = {}
 
+let namedTimeZonedImpls = {}
 
-export function registerNamedTimeZoneOffsetGenerator(name, timeZoneOffsetGenerator: namedTimeZoneOffsetGenerator) {
-  namedTimeZoneOffsetGeneratorMap[name] = timeZoneOffsetGenerator
+export function registerNamedTimeZoneImpl(implName, theClass) {
+  namedTimeZonedImpls[implName] = theClass
 }
 
+export function createNamedTimeZoneImpl(implName, tzName) {
+  let theClass = namedTimeZonedImpls[implName]
+
+  if (theClass) {
+    return theClass(tzName)
+  }
 
-export function getNamedTimeZoneOffsetGenerator(name) {
-  return namedTimeZoneOffsetGeneratorMap[name]
+  return null
 }

+ 0 - 156
src/datelib/util.ts

@@ -1,159 +1,3 @@
-import { Duration } from './duration'
-
-export type DateMarker = Date
-
-export function nowMarker(): DateMarker {
-  return arrayToUtcDate(dateToLocalArray(new Date()))
-}
-
 
 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)
-
-
-export function dateToLocalArray(date) {
-  return [
-    date.getFullYear(),
-    date.getMonth(),
-    date.getDate(),
-    date.getHours(),
-    date.getMinutes(),
-    date.getSeconds(),
-    date.getMilliseconds()
-  ]
-}
-
-export function arrayToLocalDate(arr) {
-  if (!arr.length) {
-    return new Date()
-  }
-  return new Date(
-    arr[0],
-    arr[1] || 0,
-    arr[2] || 1,
-    arr[3] || 0,
-    arr[4] || 0,
-    arr[5] || 0,
-  )
-}
-
-export function dateToUtcArray(date) {
-  return [
-    date.getUTCFullYear(),
-    date.getUTCMonth(),
-    date.getUTCDate(),
-    date.getUTCHours(),
-    date.getUTCMinutes(),
-    date.getUTCSeconds(),
-    date.getUTCMilliseconds()
-  ]
-}
-
-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 }
-    }
-  }
-}