diff --git a/README.md b/README.md index 965772c8..1304ccae 100644 --- a/README.md +++ b/README.md @@ -781,6 +781,7 @@ $ yarn build * [Jakub Roztocil](http://roztocil.co/) ([@jakubroztocil](http://twitter.com/jakubroztocil)) * Lars Schöning ([@lyschoening](http://twitter.com/lyschoening)) +* David Golightly ([@davigoli](http://twitter.com/davigoli)) Python `dateutil` is written by [Gustavo Niemeyer](http://niemeyer.net/). diff --git a/demo/demo.coffee b/demo/demo.coffee deleted file mode 100644 index d4870c45..00000000 --- a/demo/demo.coffee +++ /dev/null @@ -1,218 +0,0 @@ -RRule = rrule.RRule - -getFormValues = ($form) -> - paramObj = {} - $.each $form.serializeArray(), (_, kv) -> - if paramObj.hasOwnProperty(kv.name) - paramObj[kv.name] = $.makeArray(paramObj[kv.name]) - paramObj[kv.name].push kv.value - else - paramObj[kv.name] = kv.value - - paramObj - - -getOptionsCode = (options) -> - days = [ - "RRule.MO" - "RRule.TU" - "RRule.WE" - "RRule.TH" - "RRule.FR" - "RRule.SA" - "RRule.SU" - ] - - items = for k, v of options - if v == null - v = 'null' - else if k is 'freq' - v = 'RRule.' + RRule.FREQUENCIES[v] - else if k in ["dtstart", "until"] - v = "new Date(Date.UTC(" + [ - v.getUTCFullYear() - v.getUTCMonth() - v.getUTCDate() - v.getUTCHours() - v.getUTCMinutes() - v.getUTCSeconds() - ].join(', ') + "))" - else if k is "byweekday" - if v instanceof Array - v = v.map (wday)-> - console.log 'wday', wday - s = days[wday.weekday] - if wday.n - s+= '.nth(' + wday.n + ')' - s - else - v = days[v.weekday] - else if k is "wkst" - if v is RRule.MO - continue - v = days[v.weekday] - - if v instanceof Array - v = '[' + v.join(', ') + ']' - - console.log k, ' =', v - "#{k}: #{v}" - - "{\n #{items.join(',\n ')}\n}" - - -makeRows = (dates)-> - prevParts = [] - prevStates = [] - index = 1 - rows = for date in dates - - states = [] - parts = date.toUTCString().split(' ') - - cells = for part, i in parts - if part != prevParts[i] - states[i] = not prevStates[i] - else - states[i] = prevStates[i] - cls = if states[i] then 'a' else 'b' - "#{ part }" - - prevParts = parts - prevStates = states - - "#{ index++ }#{ cells.join('\n') }" - - rows.join('\n\n') - - -$ -> - $tabs = $("#tabs") - - activateTab = ($a) -> - id = $a.attr("href").split("#")[1] - $tabs.find("a").removeClass "active" - $a.addClass "active" - $("#input-types section").hide() - $("#input-types #" + id).show().find("input:first").focus().change() - - - $("#input-types section").hide().each -> - $("", - href: "#" + $(this).attr("id") - ).text($(this).find("h3").hide().text()).appendTo($tabs).on "click", -> - activateTab $(this) - false - - $(".examples code").on "click", -> - $code = $(this) - $code.parents("section:first").find("input").val($code.text()).change() - - $("input, select").on 'keyup change', -> - $in = $(this) - $section = $in.parents("section:first") - inputMethod = $section.attr("id").split("-")[0] - - switch inputMethod - when "text" - makeRule = -> RRule.fromText($in.val()) - init = "RRule.fromText(\"" + @value + "\")" - when "rfc" - makeRule = => RRule.fromString(@value) - init = "RRule.fromString(\"" + @value + "\")" - when 'options' - values = getFormValues($in.parents("form")) - options = {} - days = [ - RRule.MO - RRule.TU - RRule.WE - RRule.TH - RRule.FR - RRule.SA - RRule.SU - ] - getDay = (i)-> days[i] - - for key, value of values - - if not value - continue - else if key in ['dtstart', 'until'] - date = new Date(Date.parse(value + 'Z')) - value = date - else if key is 'byweekday' - if value instanceof Array - value = value.map(getDay) - else - value = getDay(value) - else if key is 'tzid' - value = value - else if /^by/.test(key) - if not (value instanceof Array) - value = value.split(/[,\s]+/) - value = (v for v in value when v) - value = value.map (n) -> parseInt(n, 10) - else - value = parseInt(value, 10) - - if key is 'wkst' - value = getDay(value) - - if key is 'interval' and (value is 1 or not value) - continue - - options[key] = value - - makeRule = -> new RRule(options) - init = "new RRule(" + getOptionsCode(options) + ")" - console.log options - - $("#init").html init - $("#rfc-output a").html "" - $("#text-output a").html "" - $("#options-output").html "" - $("#dates").html "" - - try - rule = makeRule() - catch e - $("#init").append($('
').text('=> ' + String(e||null)))
-            return
-
-        rfc = rule.toString()
-        text = rule.toText()
-        $("#rfc-output a").text(rfc).attr('href', "#/rfc/#{rfc}")
-        $("#text-output a").text(text).attr('href', "#/text/#{text}")
-        $("#options-output").text(getOptionsCode(rule.origOptions))
-        if inputMethod is 'options'
-            $("#options-output").parents('tr').hide()
-        else
-            $("#options-output").parents('tr').show()
-        max = 500
-        dates = rule.all (date, i)->
-            if not rule.options.count and i == max
-                return false  # That's enough
-            return true
-
-        html = makeRows dates
-        if not rule.options.count
-            html += """
-                Showing first #{max} dates, set
-                count to see more.
-            """
-        $("#dates").html html
-
-    activateTab $tabs.find("a:first")
-
-    processHash = ->
-        hash = location.hash.substring(1)
-        if hash
-            match = /^\/(rfc|text)\/(.+)$/.exec(hash)
-            if match
-                method = match[1]  # rfc | text
-                arg = match[2]
-                activateTab($("a[href='##{method}-input']"))
-                $("##{method}-input input:first").val(arg).change()
-    processHash()
-    $(window).on('hashchange', processHash)
diff --git a/demo/demo.ts b/demo/demo.ts
new file mode 100644
index 00000000..009381e4
--- /dev/null
+++ b/demo/demo.ts
@@ -0,0 +1,275 @@
+import * as $ from 'jquery'
+import {
+  RRule,
+  Options,
+  Weekday
+} from '../src/index'
+
+const getDay = (i: number) => [
+  RRule.MO,
+  RRule.TU,
+  RRule.WE,
+  RRule.TH,
+  RRule.FR,
+  RRule.SA,
+  RRule.SU
+][i]
+
+const makeArray = (s: string | string[]) =>
+  Array.isArray(s) ? s : [s]
+
+const getFormValues = function ($form: JQuery) {
+  const paramObj: { [K in keyof Partial]: string | string[] } = {}
+  $form.serializeArray().forEach(kv => {
+    const k = kv.name as keyof Options
+    if (paramObj.hasOwnProperty(k)) {
+      const v = makeArray(paramObj[k]!)
+      v.push(kv.value)
+      paramObj[k] = v
+    } else {
+      paramObj[k] = kv.value
+    }
+  })
+
+  return paramObj
+}
+
+const getOptionsCode = function (options: Partial) {
+  const days = [
+    'RRule.MO',
+    'RRule.TU',
+    'RRule.WE',
+    'RRule.TH',
+    'RRule.FR',
+    'RRule.SA',
+    'RRule.SU'
+  ]
+
+  const items = Object.keys(options).map((k: keyof Options) => {
+    let v: unknown = options[k]
+    if (v === null) {
+      v = 'null'
+    } else if (k === 'freq') {
+      v = `RRule.${RRule.FREQUENCIES[v as number]}`
+    } else if (k === 'dtstart' || k === 'until') {
+      const d = v as Date
+      v = 'new Date(Date.UTC(' + [
+        d.getUTCFullYear(),
+        d.getUTCMonth(),
+        d.getUTCDate(),
+        d.getUTCHours(),
+        d.getUTCMinutes(),
+        d.getUTCSeconds()
+      ].join(', ') + '))'
+    } else if (k === 'byweekday') {
+      if (Array.isArray(v)) {
+        v = (v as Weekday[]).map(function (wday) {
+          console.log('wday', wday)
+          let s = days[wday.weekday]
+          if (wday.n) {
+            return s + `.nth(${wday.n})`
+          }
+          return s
+        })
+      } else {
+        const w = v as Weekday
+        v = days[w.weekday]
+      }
+    } else if (k === 'wkst') {
+      if (v === RRule.MO) {
+        return ''
+      }
+      const w = v as Weekday
+      v = days[w.weekday]
+    }
+
+    if (Array.isArray(v)) {
+      v = `[${v.join(', ')}]`
+    }
+
+    console.log(k, ' =', v)
+    return `${k}: ${v}`
+  })
+
+  return `{\n  ${items.filter(v => !!v).join(',\n  ')}\n}`
+}
+
+const makeRows = function (dates: Date[]) {
+  let prevParts: string[] = []
+  let prevStates: boolean[] = []
+
+  const rows = dates.map((date, index) => {
+    let states: boolean[] = []
+    let parts = date.toUTCString().split(' ')
+
+    const cells = parts.map((part, i) => {
+      if (part !== prevParts[i]) {
+        states[i] = !prevStates[i]
+      } else {
+        states[i] = prevStates[i]
+      }
+      const cls = states[i] ? 'a' : 'b'
+      return `${ part }`
+    })
+
+    prevParts = parts
+    prevStates = states
+
+    return `${ index + 1 }${ cells.join('\n') }`
+  })
+
+  return rows.join('\n\n')
+}
+
+$(function () {
+  const $tabs = $('#tabs')
+
+  const activateTab = function ($a: JQuery) {
+    const id = $a.attr('href')!.split('#')[1]
+    $tabs.find('a').removeClass('active')
+    $a.addClass('active')
+    $('#input-types section').hide()
+    return $(`#input-types #${id}`).show().find('input:first').trigger('focus').trigger('change')
+  }
+
+  $('#input-types section').hide().each(function () {
+    $('', {
+      href: `#${$(this).attr('id')}`
+    }).text($(this).find('h3').hide().text()).appendTo($tabs).on('click', function () {
+      activateTab($(this))
+      return false
+    })
+  })
+
+  $('.examples code').on('click', function () {
+    const $code = $(this)
+    return $code.parents('section:first').find('input').val($code.text()).trigger('change')
+  })
+
+  let init: string
+  let makeRule: () => RRule
+
+  $('input, select').on('keyup change', function () {
+    const $in = $(this)
+    const $section = $in.parents('section:first')
+    const inputMethod = $section.attr('id')!.split('-')[0]
+
+    switch (inputMethod) {
+      case 'text':
+        makeRule = () => RRule.fromText($in.val()!.toString())
+        init = `RRule.fromText("${(this as HTMLFormElement).value}")`
+        break
+      case 'rfc':
+        makeRule = () => RRule.fromString((this as HTMLFormElement).value)
+        init = `RRule.fromString("${(this as HTMLFormElement).value}")`
+        break
+      case 'options':
+        let values = getFormValues($in.parents('form'))
+        let options: Partial = {}
+
+        for (let k in values) {
+          const key = k as keyof Options
+
+          let value: string | string[] | Date | Weekday | Weekday[] | number | number[] = values[key]!
+          if (!value) {
+            continue
+          } else if (key === 'dtstart' || key === 'until') {
+            const date = new Date(Date.parse(value + 'Z'))
+            options[key] = date
+          } else if (key === 'byweekday') {
+            if (Array.isArray(value)) {
+              options[key] = value.map(i => getDay(parseInt(i, 10)))
+            } else {
+              options[key] = getDay(parseInt(value, 10))
+            }
+          } else if (/^by/.test(key)) {
+            if (!Array.isArray(value)) {
+              value = value.split(/[,\s]+/)
+            }
+            value = value.filter(v => v)
+            options[key] = value.map(n => parseInt(n, 10))
+          } else if (key === 'tzid') {
+            options[key] = value as string
+          } else {
+            options[key] = parseInt(value as string, 10)
+          }
+
+          if (key === 'wkst') {
+            options[key] = getDay(parseInt(value as string, 10))
+          }
+
+          if (key === 'interval') {
+            const i = parseInt(value as string, 10)
+            if (i === 1 || !value) {
+              continue
+            }
+
+            options[key] = i
+          }
+        }
+
+        makeRule = () => new RRule(options)
+        init = `new RRule(${getOptionsCode(options)})`
+        console.log(options)
+        break
+    }
+
+    $('#init').html(init)
+    $('#rfc-output a').html('')
+    $('#text-output a').html('')
+    $('#options-output').html('')
+    $('#dates').html('')
+
+    let rule: RRule
+    try {
+      rule = makeRule()
+    } catch (e) {
+      $('#init').append($('
').text(`=> ${String(e || null)}`))
+      return
+    }
+
+    const rfc = rule.toString()
+    const text = rule.toText()
+    $('#rfc-output a').text(rfc).attr('href', `#/rfc/${rfc}`)
+    $('#text-output a').text(text).attr('href', `#/text/${text}`)
+    $('#options-output').text(getOptionsCode(rule.origOptions))
+    if (inputMethod === 'options') {
+      $('#options-output').parents('tr').hide()
+    } else {
+      $('#options-output').parents('tr').show()
+    }
+    const max = 500
+    const dates = rule.all(function (date, i) {
+      if (!rule.options.count && (i === max)) {
+        return false // That's enough
+      }
+      return true
+    })
+
+    let html = makeRows(dates)
+    if (!rule.options.count) {
+      html += `\
+Showing first ${max} dates, set
+count to see more.\
+`
+    }
+    return $('#dates').html(html)
+  })
+
+  activateTab($tabs.find('a:first'))
+
+  const processHash = function () {
+    const hash = location.hash.substring(1)
+    if (hash) {
+      const match = /^\/(rfc|text)\/(.+)$/.exec(hash)
+      if (match) {
+        const method = match[1] // rfc | text
+        const arg = match[2]
+        activateTab($(`a[href='#${method}-input']`))
+        return $(`#${method}-input input:first`).val(arg).trigger('change')
+      }
+    }
+  }
+  processHash()
+  return $(window).on('hashchange', processHash)
+})
diff --git a/demo/index.html b/demo/index.html
index e3f51fec..7a83376c 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -4,9 +4,7 @@
 
 
   rrule.js demo
-  
-  
-  
+  
 
 
 
diff --git a/package.json b/package.json
index f54cefd3..2c6008ff 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,10 @@
     "icalendar",
     "rfc"
   ],
-  "author": "Jakub Roztocil and Lars Schöning",
+  "author": "Jakub Roztocil, Lars Schöning, and David Golightly",
   "main": "dist/es5/rrule.js",
-  "module": "dist/esm/index.js",
-  "typings": "dist/esm/index.d.ts",
+  "module": "dist/esm/src/index.js",
+  "typings": "dist/esm/src/index.d.ts",
   "repository": {
     "type": "git",
     "url": "git://github.com/jakubroztocil/rrule.git"
@@ -25,7 +25,7 @@
     }
   },
   "scripts": {
-    "build": "yarn lint && tsc && webpack && tsc dist/esm/*.d.ts",
+    "build": "yarn lint && tsc && webpack && tsc dist/esm/**/*.d.ts",
     "lint": "yarn tslint --project . --fix --config tslint.json",
     "test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts",
     "test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts"
@@ -46,13 +46,12 @@
   "devDependencies": {
     "@types/assert": "^0.0.31",
     "@types/chai": "^4.1.4",
+    "@types/jquery": "^3.3.29",
     "@types/luxon": "^1.2.2",
     "@types/mocha": "^5.2.5",
     "@types/mockdate": "^2.0.0",
     "@types/node": "^10.5.4",
     "chai": "^4.1.2",
-    "coffee-loader": "^0.9.0",
-    "coffeescript": "^2.3.1",
     "copy-webpack-plugin": "^4.5.2",
     "coverage": "^0.0.0",
     "html-webpack-plugin": "^3.2.0",
@@ -61,7 +60,7 @@
     "mocha": "^5.2.0",
     "mockdate": "^2.0.2",
     "nyc": "^12.0.2",
-    "source-map-loader": "^0.2.3",
+    "source-map-loader": "^0.2.4",
     "source-map-support": "^0.5.8",
     "ts-loader": "^4.4.2",
     "ts-node": "^7.0.0",
diff --git a/src/iter.ts b/src/iter.ts
deleted file mode 100644
index 0bfeddf6..00000000
--- a/src/iter.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-import IterResult from './iterresult'
-import { ParsedOptions, freqIsDailyOrGreater, QueryMethodTypes } from './types'
-import dateutil from './dateutil'
-import Iterinfo from './iterinfo/index'
-import RRule from './rrule'
-import { buildTimeset } from './parseoptions'
-import { notEmpty, includes, pymod, isPresent } from './helpers'
-import { DateWithZone } from './datewithzone'
-import { Time, DateTime as DTime } from './datetime'
-
-export function iter  (iterResult: IterResult, options: ParsedOptions) {
-  const {
-    dtstart,
-    freq,
-    until,
-    bysetpos
-  } = options
-
-  let counterDate = DTime.fromDate(dtstart)
-
-  const ii = new Iterinfo(options)
-  ii.rebuild(counterDate.year, counterDate.month)
-
-  let timeset = makeTimeset(ii, counterDate, options)
-
-  let count = options.count
-
-  while (true) {
-    let [dayset, start, end] = ii.getdayset(freq)(
-      counterDate.year,
-      counterDate.month,
-      counterDate.day
-    )
-
-    let filtered = removeFilteredDays(dayset, start, end, ii, options)
-
-    if (notEmpty(bysetpos)) {
-      const poslist = buildPoslist(bysetpos, timeset!, start, end, ii, dayset)
-
-      for (let j = 0; j < poslist.length; j++) {
-        const res = poslist[j]
-        if (until && res > until) {
-          return emitResult(iterResult)
-        }
-
-        if (res >= dtstart) {
-          const rezonedDate = rezoneIfNeeded(res, options)
-          if (!iterResult.accept(rezonedDate)) {
-            return emitResult(iterResult)
-          }
-
-          if (count) {
-            --count
-            if (!count) {
-              return emitResult(iterResult)
-            }
-          }
-        }
-      }
-    } else {
-      for (let j = start; j < end; j++) {
-        const currentDay = dayset[j]
-        if (!isPresent(currentDay)) {
-          continue
-        }
-
-        const date = dateutil.fromOrdinal(ii.yearordinal + currentDay)
-        for (let k = 0; k < timeset!.length; k++) {
-          const time = timeset![k]
-          const res = dateutil.combine(date, time)
-          if (until && res > until) {
-            return emitResult(iterResult)
-          }
-
-          if (res >= dtstart) {
-            const rezonedDate = rezoneIfNeeded(res, options)
-            if (!iterResult.accept(rezonedDate)) {
-              return emitResult(iterResult)
-            }
-
-            if (count) {
-              --count
-              if (!count) {
-                return emitResult(iterResult)
-              }
-            }
-          }
-        }
-      }
-    }
-    if (options.interval === 0) {
-      return emitResult(iterResult)
-    }
-
-    // Handle frequency and interval
-    counterDate.add(options, filtered)
-
-    if (counterDate.year > dateutil.MAXYEAR) {
-      return emitResult(iterResult)
-    }
-
-    if (!freqIsDailyOrGreater(freq)) {
-      timeset = ii.gettimeset(freq)(counterDate.hour, counterDate.minute, counterDate.second, 0)
-    }
-
-    ii.rebuild(counterDate.year, counterDate.month)
-  }
-}
-
-function isFiltered (
-  ii: Iterinfo,
-  currentDay: number,
-  options: ParsedOptions
-): boolean {
-  const {
-    bymonth,
-    byweekno,
-    byweekday,
-    byeaster,
-    bymonthday,
-    bynmonthday,
-    byyearday
-  } = options
-
-  return (
-    (notEmpty(bymonth) && !includes(bymonth, ii.mmask[currentDay])) ||
-    (notEmpty(byweekno) && !ii.wnomask![currentDay]) ||
-    (notEmpty(byweekday) && !includes(byweekday, ii.wdaymask[currentDay])) ||
-    (notEmpty(ii.nwdaymask) && !ii.nwdaymask[currentDay]) ||
-    (byeaster !== null && !includes(ii.eastermask!, currentDay)) ||
-    ((notEmpty(bymonthday) || notEmpty(bynmonthday)) &&
-      !includes(bymonthday, ii.mdaymask[currentDay]) &&
-      !includes(bynmonthday, ii.nmdaymask[currentDay])) ||
-    (notEmpty(byyearday) &&
-      ((currentDay < ii.yearlen &&
-        !includes(byyearday, currentDay + 1) &&
-        !includes(byyearday, -ii.yearlen + currentDay)) ||
-        (currentDay >= ii.yearlen &&
-          !includes(byyearday, currentDay + 1 - ii.yearlen) &&
-          !includes(byyearday, -ii.nextyearlen + currentDay - ii.yearlen))))
-  )
-}
-
-function rezoneIfNeeded (date: Date, options: ParsedOptions) {
-  return new DateWithZone(date, options.tzid).rezonedDate()
-}
-
-function emitResult  (iterResult: IterResult) {
-  return iterResult.getValue()
-}
-
-function removeFilteredDays (dayset: (number | null)[], start: number, end: number, ii: Iterinfo, options: ParsedOptions) {
-  let filtered = false
-  for (let dayCounter = start; dayCounter < end; dayCounter++) {
-    let currentDay = dayset[dayCounter] as number
-
-    filtered = isFiltered(
-      ii,
-      currentDay,
-      options
-    )
-
-    if (filtered) dayset[currentDay] = null
-  }
-
-  return filtered
-}
-
-function makeTimeset (ii: Iterinfo, counterDate: DTime, options: ParsedOptions): Time[] | null {
-  const {
-    freq,
-    byhour,
-    byminute,
-    bysecond
-  } = options
-
-  if (freqIsDailyOrGreater(freq)) {
-    return buildTimeset(options)
-  }
-
-  if (
-    (freq >= RRule.HOURLY &&
-      notEmpty(byhour) &&
-      !includes(byhour, counterDate.hour)) ||
-    (freq >= RRule.MINUTELY &&
-      notEmpty(byminute) &&
-      !includes(byminute, counterDate.minute)) ||
-    (freq >= RRule.SECONDLY &&
-      notEmpty(bysecond) &&
-      !includes(bysecond, counterDate.second))
-  ) {
-    return []
-  }
-
-  return ii.gettimeset(freq)(
-    counterDate.hour,
-    counterDate.minute,
-    counterDate.second,
-    counterDate.millisecond
-  )
-}
-
-function buildPoslist (bysetpos: number[], timeset: Time[], start: number, end: number, ii: Iterinfo, dayset: (number | null)[]) {
-  const poslist: Date[] = []
-
-  for (let j = 0; j < bysetpos.length; j++) {
-    let daypos: number
-    let timepos: number
-    const pos = bysetpos[j]
-
-    if (pos < 0) {
-      daypos = Math.floor(pos / timeset.length)
-      timepos = pymod(pos, timeset.length)
-    } else {
-      daypos = Math.floor((pos - 1) / timeset.length)
-      timepos = pymod(pos - 1, timeset.length)
-    }
-
-    const tmp = []
-    for (let k = start; k < end; k++) {
-      const val = dayset[k]
-      if (!isPresent(val)) continue
-      tmp.push(val)
-    }
-    let i: number
-    if (daypos < 0) {
-      i = tmp.slice(daypos)[0]
-    } else {
-      i = tmp[daypos]
-    }
-
-    const time = timeset[timepos]
-    const date = dateutil.fromOrdinal(ii.yearordinal + i)
-    const res = dateutil.combine(date, time)
-    // XXX: can this ever be in the array?
-    // - compare the actual date instead?
-    if (!includes(poslist, res)) poslist.push(res)
-  }
-
-  dateutil.sort(poslist)
-
-  return poslist
-}
diff --git a/src/iter/index.ts b/src/iter/index.ts
index 506b026b..fef6562b 100644
--- a/src/iter/index.ts
+++ b/src/iter/index.ts
@@ -13,10 +13,16 @@ export function iter  (iterResult: IterResult, op
   const {
     dtstart,
     freq,
+    interval,
     until,
     bysetpos
   } = options
 
+  let count = options.count
+  if (count === 0 || interval === 0) {
+    return emitResult(iterResult)
+  }
+
   let counterDate = DateTime.fromDate(dtstart)
 
   const ii = new Iterinfo(options)
@@ -24,8 +30,6 @@ export function iter  (iterResult: IterResult, op
 
   let timeset = makeTimeset(ii, counterDate, options)
 
-  let count = options.count
-
   while (true) {
     let [dayset, start, end] = ii.getdayset(freq)(
       counterDate.year,
diff --git a/tsconfig.json b/tsconfig.json
index 836a7127..d0d3cb0c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
   "compilerOptions": {
     "outDir": "./dist/esm",
     "declaration": true,
+    "declarationMap": true,
     "noImplicitAny": true,
     "sourceMap": true,
     "module": "es2015",
@@ -9,8 +10,8 @@
     "target": "es5",
     "jsx": "react",
     "strictNullChecks": true,
-    "rootDirs": ["./src/", "./test/"]
+    "rootDirs": ["./src/", "./test/", "./demo/"]
   },
-  "include": ["./src/**/*"],
+  "include": ["./src/**/*", "./demo/**/*"],
   "exclude": ["node_modules", "./test/**/*"]
 }
diff --git a/webpack.config.js b/webpack.config.js
index c59a46d7..42e04419 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,31 +6,43 @@ const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 
 const paths = {
   demo: {
-    styles: path.resolve(__dirname, "demo/demo.css"),
-    template: path.resolve(__dirname, "demo/index.html")
+    source: path.resolve(__dirname, 'demo'),
+    styles: path.resolve(__dirname, "demo", "demo.css"),
+    template: path.resolve(__dirname, "demo", "index.html")
   },
-  demoDist: path.resolve(__dirname, "dist"),
+  source: path.resolve(__dirname, 'src'),
+  demoDist: path.resolve(__dirname, "dist", "esm", "demo"),
   es5: path.resolve(__dirname, "dist", "es5"),
   esm: path.resolve(__dirname, "dist", "esm")
 };
 
 const commonConfig = {
   output: {
-    filename: "[name].js",
+    filename: '[name].js',
     path: paths.es5,
-    library: "rrule",
-    libraryTarget: "umd",
+    library: 'rrule',
+    libraryTarget: 'umd',
     globalObject: "typeof self !== 'undefined' ? self : this"
   },
-  devtool: "source-map",
-  mode: "production",
+  devtool: 'source-map',
+  mode: 'production',
   resolve: {
-    extensions: [".js"]
+    extensions: ['.js', '.ts']
+  },
+  module: {
+    rules: [
+      {
+        exclude: /node_modules/,
+        loader: "ts-loader",
+        test: /\.ts$/
+      }
+    ]
   },
   optimization: {
     minimize: true,
     minimizer: [
       new UglifyJsPlugin({
+        exclude: /\.ts$/,
         include: /\.min\.js$/
       })
     ]
@@ -39,8 +51,8 @@ const commonConfig = {
 
 const rruleConfig = Object.assign({
   entry: {
-    rrule: path.join(paths.esm, "index.js"),
-    'rrule.min': path.join(paths.esm, "index.js")
+    rrule: path.join(paths.source, "index.ts"),
+    'rrule.min': path.join(paths.source, "index.ts")
   },
   externals: {
     luxon: 'luxon'
@@ -49,21 +61,26 @@ const rruleConfig = Object.assign({
 
 const rruleWithLuxonConfig = Object.assign({
   entry: {
-    'rrule-tz': path.join(paths.esm, "index.js"),
-    'rrule-tz.min': path.join(paths.esm, "index.js")
+    'rrule-tz': path.join(paths.source, "index.ts"),
+    'rrule-tz.min': path.join(paths.source, "index.ts")
   },
 }, commonConfig);
 
 const demoConfig = {
   entry: {
-    demo: "./demo/demo.coffee"
+    demo: path.join(paths.demo.source, "demo.ts"),
   },
   module: {
     rules: [
+      {
+        test: /\.js$/,
+        use: ["source-map-loader"],
+        enforce: "pre"
+      },
       {
         exclude: /node_modules/,
-        loader: "coffee-loader",
-        test: /\.coffee$/
+        loader: "ts-loader",
+        test: /\.ts$/
       }
     ]
   },
@@ -71,6 +88,9 @@ const demoConfig = {
     filename: "demo.js",
     path: paths.demoDist
   },
+  resolve: {
+    extensions: [".js", ".ts"]
+  },
   plugins: [
     new webpack.ProvidePlugin({
       $: "jquery",
diff --git a/yarn.lock b/yarn.lock
index e3e58ded..1ab66d90 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -90,6 +90,12 @@
   version "4.1.4"
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.4.tgz#5ca073b330d90b4066d6ce18f60d57f2084ce8ca"
 
+"@types/jquery@^3.3.29":
+  version "3.3.29"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
+  dependencies:
+    "@types/sizzle" "*"
+
 "@types/luxon@^1.2.2":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.2.2.tgz#3b402da20bd8ca357123851e062d2142cdbdd9bc"
@@ -106,6 +112,10 @@
   version "10.5.4"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.4.tgz#6eccc158504357d1da91434d75e86acde94bb10b"
 
+"@types/sizzle@*":
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
+
 "@webassemblyjs/ast@1.5.13":
   version "1.5.13"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25"
@@ -750,16 +760,6 @@ code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
-coffee-loader@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/coffee-loader/-/coffee-loader-0.9.0.tgz#6deabd336062ddc6d773da4dfd16367fc7107bd6"
-  dependencies:
-    loader-utils "^1.0.2"
-
-coffeescript@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.3.1.tgz#a25f69c251d25805c9842e57fc94bfc453ef6aed"
-
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -2044,7 +2044,7 @@ loader-runner@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
 
-loader-utils@^0.2.16, loader-utils@~0.2.2:
+loader-utils@^0.2.16:
   version "0.2.17"
   resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
   dependencies:
@@ -3147,13 +3147,12 @@ source-list-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
 
-source-map-loader@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.3.tgz#d4b0c8cd47d54edce3e6bfa0f523f452b5b0e521"
+source-map-loader@^0.2.4:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271"
   dependencies:
     async "^2.5.0"
-    loader-utils "~0.2.2"
-    source-map "~0.6.1"
+    loader-utils "^1.1.0"
 
 source-map-resolve@^0.5.0:
   version "0.5.2"