# moment-range [![CircleCI](https://circleci.com/gh/rotaready/moment-range.svg?style=shield)](https://circleci.com/gh/rotaready/moment-range) Fancy date ranges for [Moment.js][moment]. - [Installation](#installation) - [Node / NPM](#node--npm) - [Browser](#browser) - [Older browsers and IE11](#older-browsers-and-ie11) - [Examples](#examples) - [Create](#create) - [rangeFromInterval](#rangefrominterval) - [parseZoneRange](#parsezonerange) - [rangeFromISOString](#rangefromisostring) - [Attributes](#attributes) - [Querying](#querying) - [Adjacent](#adjacent) - [Center](#center) - [Contains](#contains) - [Within](#within) - [Overlaps](#overlaps) - [Intersect](#intersect) - [IsRange](#isrange) - [Manipulation](#manipulation) - [Add](#add) - [Clone](#clone) - [SnapTo](#snapto) - [Subtract](#subtract) - [Iteration](#iteration) - [by](#by) - [byRange](#byrange) - [reverseBy](#reverseby) - [reverseByRange](#reversebyrange) - [Compare](#compare) - [Equality](#equality) - [Difference](#difference) - [Conversion](#conversion) - [`toDate`](#todate) - [`toString`](#tostring) - [`valueOf`](#valueof) - [Running Tests](#running-tests) - [Contributors](#contributors) - [License](#license) ## Installation moment-range works in both the browser and [node.js][node]. ### Node / NPM Install via npm: ``` sh npm install --save moment-range ``` **ES6:** ``` js import Moment from 'moment'; import { extendMoment } from 'moment-range'; const moment = extendMoment(Moment); ``` **CommonJS:** ``` js const Moment = require('moment'); const MomentRange = require('moment-range'); const moment = MomentRange.extendMoment(Moment); ``` ### Browser ``` html ``` ``` js window['moment-range'].extendMoment(moment); ``` Thanks to the fine people at [cdnjs][cdnjs], you can link to moment-range from the [cdnjs servers][cdnjs-moment-range]. ### Older browsers and IE11 This library makes use of `Symbol.iterator` to provide the [iteration protocols] now that there is [broad support] for them, if you need to support older browsers (specifically IE11) you will need to include a polyfill. Any of the following should work, depending on your project configuration: * [babel runtime transform plugin] * [babel polyfill] * https://github.com/medikoo/es6-iterator * https://github.com/zloirock/core-js ## Examples ### Create Create a date range: ``` js const start = new Date(2012, 0, 15); const end = new Date(2012, 4, 23); const range = moment.range(start, end); ``` You can also create a date range with moment objects: ``` js const start = moment('2011-04-15', 'YYYY-MM-DD'); const end = moment('2011-11-27', 'YYYY-MM-DD'); const range = moment.range(start, end); ``` Arrays work too: ``` js const dates = [moment('2011-04-15', 'YYYY-MM-DD'), moment('2011-11-27', 'YYYY-MM-DD')]; const range = moment.range(dates); ``` You can also create a range from an [ISO 8601 time interval][interval] string: ``` js const timeInterval = '2015-01-17T09:50:04+00:00/2015-04-17T08:29:55+00:00'; const range = moment.range(timeInterval); ``` You can also create a range from the start until the end of a named interval: ``` js const date = moment('2011-04-15', 'YYYY-MM-DD'); const range = date.range('month'); ``` You can also create open-ended ranges which go to the earliest or latest possible date: ``` js const rangeUntil = moment.range(null, '2011-05-05'); const rangeFrom = moment.range('2011-03-05'); const rangeAllTime = moment.range(); ``` Note that any falsy value except 0 is treated as a missing date, resulting in an open-ended range. *Note:* Dates and moment objects both use a timestamp of 00:00:000 if none is provided. To ensure your range includes any timestamp for the given end date, use `.setHours(23,59,59,999)` when constructing a Date object, or `.endOf('day')` when constructing a moment object. #### rangeFromInterval You can also create a range between an interval and a specified date. This accepts positive or negative values for `count` and the date will default to _now_ if not provided. ``` js const interval = 'month'; const count = 4; const date = moment('2017-07-20'); const range1 = moment.rangeFromInterval(interval, count, date); // moment.range('2017-07-20', '2017-11-20') const range2 = moment.rangeFromInterval('month', -2, date); // moment.range('2017-05-20', '2017-07-20') ``` Note: The date can be provided as a Date, String, or Moment. When using a negative interval, the date provided will be set as the end of the range. #### parseZoneRange **DEPRECATED** in `4.0.0`: Replaced by `rangeFromISOString` to follow naming conventions. #### rangeFromISOString Converts an [ISO 8601 time interval string][interval] into a date range while preserving the time zones using [moment.parseZone][parseZone]. ``` js const interval = '2015-01-17T09:50:00+03:00/2015-04-17T08:29:55-04:00'; const range = moment.rangeFromISOString(interval); range.toString(); // '2015-01-17T09:50:00+03:00/2015-04-17T08:29:55-04:00' ``` ### Attributes You can access the start and end moments of the range easily enough: ``` js const start = new Date(2012, 0, 15); const end = new Date(2012, 4, 23); const range = moment.range(start, end); range.start // moment range.end // moment ``` ### Querying Many of the following examples make use of these moments: ``` js const a = moment('2016-03-10'); const b = moment('2016-03-15'); const c = moment('2016-03-29'); const d = moment('2016-04-01'); ``` #### Adjacent Check if two ranges are touching but not overlapping: ``` js const range1 = moment.range(a, b); const range2 = moment.range(b, c); const range3 = moment.range(c, d); range1.adjacent(range2) // true range1.adjacent(range3) // false ``` #### Center Calculate the center of a range: ``` js const start = new Date(2011, 2, 5); const end = new Date(2011, 3, 5); const range = moment.range(start, end); range.center(); // 1300622400000 ``` #### Contains Check to see if your range contains a date/moment. By default the start and end dates are included in the search. E.g.: ``` js const range = moment.range(a, c); range.contains(a); // true range.contains(b); // true range.contains(c); // true range.contains(d); // false ``` You can also control whether the start or end dates should be excluded from the search with the `excludeStart` and `excludeEnd` options: ``` js const range = moment.range(a, c); range.contains(a); // true range.contains(a, { excludeStart: true }); // false range.contains(c); // true range.contains(c, { excludeEnd: true; }); // false ``` **DEPRECATED** in `4.0.0`: The `exclusive` options is used to indicate if the start/end of the range should be excluded when testing for inclusion: **Note**: You can obtain the same functionality by setting `{ excludeStart: true, excludeEnd: true }` ``` js range.contains(c); // true range.contains(c, { exclusive: false }); // true range.contains(c, { exclusive: true }); // false ``` #### Within Find out if your moment falls within a date range: ``` js const range = moment.range(a, c); b.within(range); // true ``` #### Overlaps Does it overlap another range? ``` js const range1 = moment.range(a, c); const range2 = moment.range(b, d); range1.overlaps(range2); // true ``` Include adjacent ranges: ``` js const range1 = moment.range(a, b); const range2 = moment.range(b, c); range1.overlaps(range2) // false range1.overlaps(range2, { adjacent: false }) // false range1.overlaps(range2, { adjacent: true }) // true ``` #### Intersect What is the intersecting range? ``` js const range1 = moment.range(a, c); const range2 = moment.range(b, d); range1.intersect(range2); // moment.range(b, c) ``` #### IsRange Is it a Range? ``` js moment.isRange(range); // true moment.isRange(IamNotRange); // false ``` ### Manipulation #### Add Add/combine/merge overlapping or adjacent ranges. ``` js const range1 = moment.range(a, c); const range2 = moment.range(b, d); range1.add(range2); // moment.range(a, d) const range3 = moment.range(a, b); const range4 = moment.range(c, d); range3.add(range4); // null ``` Include adjacent ranges: ``` js const range1 = moment.range(a, b); const range2 = moment.range(b, c); range1.add(range2); // null range1.add(range2, { adjacent: false }); // null range1.add(range2, { adjacent: true }); // moment.range(a, c) ``` #### Clone Deep clone a range ``` js const range1 = moment.range(a, d); const range2 = range1.clone(); range2.start.add(2, 'days'); range1.start.toDate().getTime() === range2.start.toDate().getTime() // false ``` #### SnapTo Snap the start and end of a range to a given interval. ``` js const start = moment('2018-01-25 17:05:33'); const end = moment('2018-01-28 06:10:00'); const range1 = moment.range(start, end); const range2 = range1.snapTo('day'); // 2018-01-25T00:00:00 -> 2018-01-28T23:59:59 range1.diff('days'); // 2 range2.diff('days'); // 3 ``` #### Subtract Subtracting one range from another. ``` js const range_ab = moment.range(a, b); const range_bc = moment.range(b, c); const range_cd = moment.range(c, d); const range_ad = moment.range(a, d); range_ad.subtract(range_bc); // [moment.range(a, b) moment.range(c, d)] range_ac.subtract(range_bc); // [moment.range(a, b)] range_ab.subtract(range_cd); // [moment.range(a, b)] range_bc.subtract(range_bd); // [null] ``` ### Iteration Each of the iteration methods returns an [Iterable][iterable], providing a convenient and performant interface to iterating over your ranges by a given period. #### by Iterate over your range by a given period. Any of the units accepted by [moment.js' `add` method][add] may be used. E.g.: `'years' | 'quarters' | 'months' | 'weeks' | 'days' | 'hours' | 'minutes' | 'seconds' | 'milliseconds'` ``` js const range = moment.range('2010-01-01', '2015-01-01'); for (let month of range.by('month')) { month.format('YYYY-MM-DD'); } const years = Array.from(range.by('year')); years.length == 6 // true years.map(m => m.format('YYYY')) // ['2010', '2011', '2012', '2013', '2014', '2015'] ``` Iteration also supports excluding the final time slice of the range by setting the `excludeEnd` option to `true`. In the example below, the 5:00 -> 6:00 time slice is omitted. ``` js const range = moment.range('2018-01-01 00:00', '2018-01-01 05:30'); const hours = Array.from(range.by('hour', { excludeEnd: true })); hours.length == 5 // true hours.map(m => m.format('HH:mm')) // ['00:00', '01:00', '02:00', '03:00', '04:00'] ``` Additionally it's possible to iterate by a given step that defaults to `1`: ``` js const start = new Date(2012, 2, 2); const end = new Date(2012, 2, 6); const range1 = moment.range(start, end); let acc = Array.from(range1.by('day', { step: 2 })); acc.map(m => m.format('DD')) // ['02', '04', '06'] acc = Array.from(range1.by('day', { excludeEnd: true, step: 2 })); acc.map(m => m.format('DD')) // ['02', '04'] ``` You can iterate over the span of a range for a period that is entered but not complete by using the [snapTo()](#snapto) method: ``` js const start = moment("2017-01-01T13:30:00"); const end = moment("2017-01-05T01:45:12"); const r1 = moment.range(start, end); const r2 = r1.snapTo('day'); Array.from(r1.by('days')).map(m => m.format('DD')); // ['01', '02', '03', '04'] Array.from(r2.by('days')).map(m => m.format('DD')); // ['01', '02', '03', '04', '05'] ``` **DEPRECATED** in `4.0.0`: The `exclusive` options is used to indicate if the end of the range should be excluded when testing for inclusion: **Note**: You can obtain the same functionality by setting `{ excludeEnd: true }` #### byRange ``` js const start = new Date(2012, 2, 1); // 1st const two = new Date(2012, 2, 2); // 2nd const end = new Date(2012, 2, 5); // 5th const range1 = moment.range(start, end); const range2 = moment.range(start, two); // One day ``` Iterate by another range: ``` js const acc = Array.from(range1.byRange(range2)); acc.length == 5 // true acc.map(m => m.format('DD')) // ['01','02','03','04','05'] ``` Exclude the end time slice: ``` js const acc = Array.from(range1.byRange(range2, { excludeEnd: true })); acc.length == 4 // true acc.map(m => m.format('DD')) // ['01','02','03','04'] ``` By step: ``` js let acc = Array.from(range1.byRange(range2, { step: 2 })); acc.map(m => m.format('DD')) // ['01', '03', '05'] acc = Array.from(range1.byRange(range2, { excludeEnd, true, step: 2 })); acc.map(m => m.format('DD')) // ['01', '03'] ``` **DEPRECATED** in `4.0.0`: The `exclusive` options is used to indicate if the end of the range should be excluded when testing for inclusion: **Note**: You can obtain the same functionality by setting `{ excludeEnd: true }` #### reverseBy Iterate over a range in reverse: ``` js const range = moment.range('2012-01-01', '2015-01-01'); const acc = Array.from(range.reverseBy('years')); acc.map(m => m.format('YYYY')) // ['2015', '2014', '2013', '2012'] ``` Exclude the start time slice: ``` js const range = moment.range('2012-01-01', '2015-01-01'); const acc = Array.from(range.reverseBy('years', { excludeStart: true })); acc.map(m => m.format('YYYY')) // ['2015', '2014', '2013'] ``` By step: ``` js const start = new Date(2012, 2, 2); const end = new Date(2012, 2, 6); const range1 = moment.range(start, end); let acc = Array.from(range1.reverseBy('day', { step: 2 })); acc.map(m => m.format('DD')) // ['06', '04', '02'] acc = Array.from(range1.reverseBy('day', { excludeStart: true, step: 2 })); acc.map(m => m.format('DD')) // ['06', '04'] ``` **DEPRECATED** in `4.0.0`: The `exclusive` options is used to indicate if the start of the range should be excluded when testing for inclusion: **Note**: You can obtain the same functionality by setting `{ excludeStart: true }` #### reverseByRange ``` js const start = new Date(2012, 2, 1); const two = new Date(2012, 2, 2); const end = new Date(2012, 2, 5); const range1 = moment.range(start, end); const range2 = moment.range(start, two); // One day ``` Iterate by another range in reverse: ``` js const acc = Array.from(range1.reverseByRange(range2)); acc.length == 5 // true acc.map(m => m.format('DD')) // ['05', '04', '03', '02', '01'] ``` Exclude the start value: ``` js const acc = Array.from(range1.reverseByRange(range2, { excludeStart: true })); acc.length == 4 // true acc.map(m => m.format('DD')) // ['05', '04', '03', '02'] ``` By step: ``` js let acc = Array.from(range1.reverseByRange(range2, { step: 2 })); acc.map(m => m.format('DD')) // ['05', '03', '01'] acc = Array.from(range1.reverseByRange(range2, { excludeStart: true, step: 2 })); acc.map(m => m.format('DD')) // ['05', '03'] ``` **DEPRECATED** in `4.0.0`: The `exclusive` options is used to indicate if the start of the range should be excluded when testing for inclusion: **Note**: You can obtain the same functionality by setting `{ excludeStart: true }` ### Compare Compare range lengths or add them together with simple math: ``` js const range1 = moment.range(new Date(2011, 2, 5), new Date(2011, 3, 15)); const range2 = moment.range(new Date(1995, 0, 1), new Date(1995, 12, 25)); range2 > range1 // true range1 + range2 // duration of both ranges in milliseconds Math.abs(range1 - range2); // difference of ranges in milliseconds ``` #### Equality Check if two ranges are the same, i.e. their starts and ends are the same: ``` js const range1 = moment.range(new Date(2011, 2, 5), new Date(2011, 3, 15)); const range2 = moment.range(new Date(2011, 2, 5), new Date(2011, 3, 15)); const range3 = moment.range(new Date(2011, 3, 5), new Date(2011, 6, 15)); range1.isSame(range2); // true range2.isSame(range3); // false range1.isEqual(range2); // true range2.isEqual(range3); // false ``` #### Difference The difference of the entire range given various units. Any of the units accepted by [moment.js' `add` method][add] may be used. ``` js const start = new Date(2011, 2, 5); const end = new Date(2011, 5, 5); const range = moment.range(start, end); range.diff('months'); // 3 range.diff('days'); // 92 range.diff(); // 7945200000 ``` Optionally you may specify if the difference should not be truncated. By default it mimics moment-js' behaviour and truncates the values: ``` js const d1 = new Date(Date.UTC(2011, 4, 1)); const d2 = new Date(Date.UTC(2011, 4, 5, 12)); const range = moment.range(d1, d2); range.diff('days') // 4 range.diff('days', false) // 4 range.diff('days', true) // 4.75 ``` `#duration` is an alias for `#diff` and they may be used interchangeably. ### Conversion #### `toDate` Converts the `DateRange` to an `Array` of the start and end `Date` objects. ``` js const start = new Date(2011, 2, 5); const end = new Date(2011, 5, 5); const range = moment.range(start, end); range.toDate(); // [new Date(2011, 2, 5), new Date(2011, 5, 5)] ``` #### `toString` Converting a `DateRange` to a `String` will format it as an [ISO 8601 time interval][interval]: ``` js const start = '2015-01-17T09:50:04+00:00'; const end = '2015-04-17T08:29:55+00:00'; const range = moment.range(moment.utc(start), moment.utc(end)); range.toString() // '2015-01-17T09:50:04+00:00/2015-04-17T08:29:55+00:00' ``` #### `valueOf` The difference between the end date and start date in milliseconds. ``` js const start = new Date(2011, 2, 5); const end = new Date(2011, 5, 5); const range = moment.range(start, end); range.valueOf(); // 7945200000 ``` ## Running Tests Clone this bad boy: ``` sh git clone https://git@github.com/rotaready/moment-range.git ``` Install the dependencies: ``` sh yarn install ``` Do all the things! ``` sh yarn run check yarn run test yarn run lint ``` ## Contributors - [**Adam Biggs**](https://github.com/adambiggs) (http://lightmaker.com) - [**Mats Julian Olsen**](https://github.com/mewwts) - [**Matt Patterson**](https://github.com/fidothe) (http://reprocessed.org/) - [**Wilgert Velinga**](https://github.com/wilgert) (http://neocles.io) - [**Tomasz Bak**](https://github.com/tb) (http://twitter.com/tomaszbak) - [**Stuart Kelly**](https://github.com/stuartleigh) - [**Jeremy Forsythe**](https://github.com/jdforsythe) - [**Александр Гренишин**](https://github.com/nd0ut) - [**@scotthovestadt**](https://github.com/scotthovestadt) - [**Thomas van Lankveld**](https://github.com/thomasvanlankveld) - [**nebel**](https://github.com/pronebel) - [**Kevin Ross**](https://github.com/rosskevin) (http://www.alienfast.com) - [**Thomas Walpole**](https://github.com/twalpole) - [**Jonathan Kim**](https://github.com/jkimbo) (http://jkimbo.co.uk) - [**Tymon Tobolski**](https://github.com/teamon) (http://teamon.eu) - [**Aristide Niyungeko**](https://github.com/aristiden7o) - [**Bradley Ayers**](https://github.com/bradleyayers) - [**Ross Hadden**](https://github.com/rosshadden) (http://rosshadden.github.com/resume) - [**Victoria French**](https://github.com/victoriafrench) - [**Jochen Diekenbrock**](https://github.com/JochenDiekenbrock) ## License moment-range is [UNLICENSED][unlicense]. [add]: http://momentjs.com/docs/#/manipulating/add/ [babel runtime transform plugin]: https://babeljs.io/docs/plugins/transform-runtime [babel polyfill]: https://babeljs.io/docs/usage/polyfill [broad support]: http://kangax.github.io/compat-table/es6/#test-well-known_symbols_Symbol.iterator,_existence_a_href=_https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator_title=_MDN_documentation_img_src=_../mdn.png_alt=_MDN_(Mozilla_Development_Network)_logo_width=_15_height=_13_/_/a_nbsp; [cdnjs]: https://github.com/cdnjs/cdnjs [cdnjs-moment-range]: https://cdnjs.com/libraries/moment-range [interval]: http://en.wikipedia.org/wiki/ISO_8601#Time_intervals [iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#Syntaxes_expecting_iterables [iteration protocols]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols [moment]: http://momentjs.com/ [node]: http://nodejs.org/ [unlicense]: http://unlicense.org/ [parseZone]: https://momentjs.com/docs/#/parsing/parse-zone/