From f5b40d2293d304da894a89e6e5ad1a716a686021 Mon Sep 17 00:00:00 2001 From: littletower Date: Sat, 18 Nov 2017 17:28:16 +0000 Subject: [PATCH] feat: added timezones support and migrated from date-fns to moment.js, hours configuration is now in BREAKING CHANGE: hours configuration must be updated to new format --- README.md | 175 +++++++++++++++++------------- dist/index.js | 155 +++++++++++++++++---------- dist/utils/index.js | 33 ++++++ example/package.json | 5 +- example/src/App.css | 17 ++- example/src/App.js | 37 +++++-- example/src/hours.json | 34 +++--- example/yarn.lock | 12 ++- package.json | 18 ++-- src/hours.json | 34 +++--- src/hoursMissingHolidays.json | 67 ++++++++++++ src/index.js | 134 ++++++++++++++++------- src/index.test.js | 195 ++++++++++++++++++++++++++-------- src/utils/index.js | 24 +++++ 14 files changed, 666 insertions(+), 274 deletions(-) create mode 100644 dist/utils/index.js create mode 100644 src/hoursMissingHolidays.json create mode 100644 src/utils/index.js diff --git a/README.md b/README.md index d5a5517..08542e9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # business-hours.js -Handle business hours of a restaurant, office or any other business. Highly customizable with lots of features. +Handle business hours of a restaurant, office or any other business. Highly customizable with lots of features, based on moment.js. + +[Demo](https://codesandbox.io/s/github/littletower/business-hours.js/tree/master/example) -[Demo](https://business-hours-example.herokuapp.com/) [![Travis](https://img.shields.io/travis/littletower/business-hours.js.svg?style=flat-square)]() [![Codecov](https://img.shields.io/codecov/c/github/littletower/business-hours.js.svg?style=flat-square)]() @@ -20,80 +21,82 @@ yarn add business-hours.js # Configuration -To get started, setup a JSON file where you define the business hours. - -`0` stands for `Sunday`
-`1` stands for `Monday` and so on. +To get started, you'll need to define your business hours in JSON format for every weekday. You can have 1 to N `from/to` pairs per weekday. If on a given day you are closed, instead of a `from/to` pair, just put `closed`. +In `holidays` you can define on which day the business is closed for holidays. It can be one day (YYYY/MM/DD) or a range (YYYY/MM/DD-YYYY/MM/DD). + +Set the desired timezone in `timeZone`, use any `tz` timezone. + ```json -{ - "0": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "17:00", - "to": "20:00" - }, - { - "from": "21:00", - "to": "24:00" - } - ], - "1": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "18:00", - "to": "22:00" - } - ], - "2": "closed", - "3": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "18:00", - "to": "22:00" - } - ], - "4": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "18:00", - "to": "22:00" - } - ], - "5": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "18:00", - "to": "22:00" - } - ], - "6": [ - { - "from": "10:00", - "to": "13:30" - }, - { - "from": "18:00", - "to": "22:00" - } - ] +"Monday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } +], +"Tuesday": "closed", +"Wednesday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } +], +"Thursday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } +], +"Friday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } +], +"Saturday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } +], +"Sunday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "17:00", + "to": "20:00" + }, + { + "from": "21:00", + "to": "24:00" + } +], +"holidays": ["2017/12/11", "2017/12/23-2018/01/02"], +"timeZone":"Europe/Amsterdam" } ``` @@ -114,9 +117,33 @@ import hoursJson from "./hours.json"; or it could come from any other endpoint (DB, GraphQL, Firebase...), as long as it's in JSON. # Example +To check if your business is currently open: +``` +let isBusinessOpenNow = businessHours.isOpenNow(); //returns boolean value +``` + Find a whole example in React here [here](example/) +# Doc + +Method | Argument | Description +------ | -------- | ----------- +`isOpenNow` | optional : date | Returns if your business is open or not. If an argument is provided, the method will be executed for the given date. +`isClosedNow` | optional : date | Returns if your business is closed or not. If an argument is provided, the method will be executed for the given date. +`willBeOpenOn` | date | Returns if your business will be open on given date. +`isOpenTomorrow` | | Returns if your business is open tomorrow. +`isOpenAfterTomorrow` | | Returns if your business is open after tomorrow. +`nextOpeningDate` | optional : boolean | Returns the next opening date. If argument is set to `true`, the next opening date could be today. +`nextOpeningHour` | | Returns the next opening hour. +`isOnHoliday` | optional : date | Returns if your business is closed for holidays. +`isOnHolidayInDays` | integer | Returns if your business will be closed for holidays in `x` days. + + # TODOs -- [ ] use ISO formate for weekday, meaning, starting the week on Monday instead of Sunday -- [ ] add holidays (single day or range) in ISO (ISO 8601) format YYYY-MM-DD +- [ ] support after midnight hours, (eg. open from 18:00 to 03:00) +- [x] Time zones support +- [x] use ISO format for weekdays, meaning, starting the week on Monday instead of Sunday +- [x] add holidays (single day or range) in ISO (ISO 8601) format YYYY-MM-DD +- [ ] support hourly holidays, like business opens from 20:00 instead of 18:00 on a given date. - [ ] add always closed on public holidays (country specific) +- [ ] add localized formatter to display all the business hours diff --git a/dist/index.js b/dist/index.js index 3753878..b3165a6 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2,41 +2,13 @@ var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); -var _set_minutes = require("date-fns/set_minutes"); +var _momentTimezone = require("moment-timezone"); -var _set_minutes2 = _interopRequireDefault(_set_minutes); +var _momentTimezone2 = _interopRequireDefault(_momentTimezone); -var _set_hours = require("date-fns/set_hours"); +var _utils = require("./utils"); -var _set_hours2 = _interopRequireDefault(_set_hours); - -var _get_day = require("date-fns/get_day"); - -var _get_day2 = _interopRequireDefault(_get_day); - -var _format = require("date-fns/format"); - -var _format2 = _interopRequireDefault(_format); - -var _is_within_range = require("date-fns/is_within_range"); - -var _is_within_range2 = _interopRequireDefault(_is_within_range); - -var _is_future = require("date-fns/is_future"); - -var _is_future2 = _interopRequireDefault(_is_future); - -var _add_days = require("date-fns/add_days"); - -var _add_days2 = _interopRequireDefault(_add_days); - -var _is_equal = require("date-fns/is_equal"); - -var _is_equal2 = _interopRequireDefault(_is_equal); - -var _is_before = require("date-fns/is_before"); - -var _is_before2 = _interopRequireDefault(_is_before); +var _utils2 = _interopRequireDefault(_utils); var _lodash = require("lodash"); @@ -46,7 +18,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -var weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +var weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; var BusinessHours = function () { function BusinessHours() { @@ -66,6 +38,11 @@ var BusinessHours = function () { if (isNaN(hour)) return false; return true; } + }, { + key: "_getISOWeekDayName", + value: function _getISOWeekDayName(isoDay) { + return weekdays[isoDay - 1]; + } }, { key: "init", value: function init(hours) { @@ -76,17 +53,17 @@ var BusinessHours = function () { } weekdays.forEach(function (day, index) { - if (!hours.hasOwnProperty(index.toString())) { + if (!hours.hasOwnProperty(day)) { throw new Error(day + " is missing from config"); } else { - if (hours[index.toString()] !== "closed") { - if (!hours[index.toString()][0].hasOwnProperty("from")) { + if (hours[day] !== "closed") { + if (!hours[day][0].hasOwnProperty("from")) { console.error(day + " is missing 'from' in config"); - } else if (!hours[index.toString()][0].hasOwnProperty("to")) { + } else if (!hours[day][0].hasOwnProperty("to")) { console.error(day + " is missing 'to' in config"); - } else if (!_this._isHourValid(hours[index.toString()][0].from)) { + } else if (!_this._isHourValid(hours[day][0].from)) { console.error(day + "'s 'from' has not the right format. Should be ##:##"); - } else if (!_this._isHourValid(hours[index.toString()][0].to)) { + } else if (!_this._isHourValid(hours[day][0].to)) { console.error(day + "'s 'to' has not the right format. Should be ##:##"); } } @@ -98,16 +75,19 @@ var BusinessHours = function () { }, { key: "isClosedNow", value: function isClosedNow() { - var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date(); + var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _utils2.default.now(this.hours.timeZone); return !this.isOpenNow(now); } }, { key: "isOpenNow", value: function isOpenNow() { - var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date(); + var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _utils2.default.now(this.hours.timeZone); - var day = (0, _get_day2.default)(now); + var day = this._getISOWeekDayName(now.isoWeekday()); + if (this.isOnHoliday(now)) { + return false; + } // console.log("now: ", format(now, "DD/MM/YYYY HH:mm")); var isOpenNow = false; if (this.hours[day.toString()] === "closed") return isOpenNow; @@ -118,9 +98,17 @@ var BusinessHours = function () { var fromMinutes = from.substr(3, 2); var toHours = to.substr(0, 2); var toMinutes = to.substr(3, 2); - var fromDate = (0, _set_hours2.default)((0, _set_minutes2.default)(now, fromMinutes), fromHours); - var toDate = (0, _set_hours2.default)((0, _set_minutes2.default)(now, toMinutes), toHours); - isOpenNow = (0, _is_within_range2.default)(now, fromDate, toDate); + var fromDate = now.clone(); + fromDate.set({ + hour: fromHours, + minute: fromMinutes + }); + var toDate = now.clone(); + toDate.set({ + hour: toHours, + minute: toMinutes + }); + isOpenNow = now.isBetween(fromDate, toDate); return isOpenNow; }); @@ -129,26 +117,28 @@ var BusinessHours = function () { }, { key: "willBeOpenOn", value: function willBeOpenOn(date) { - var day = (0, _get_day2.default)(date); - if ((0, _is_future2.default)(date) || (0, _is_equal2.default)(new Date(), date)) { - if (this.hours[day.toString()] !== "closed") { + var day = this._getISOWeekDayName(date.isoWeekday()); + var now = _utils2.default.now(this.hours.timeZone); + if (now.isBefore(date) || now.isSame(date)) { + if (this.hours[day] !== "closed" && !this.isOnHoliday(date)) { return true; } else { return false; } } + return false; } }, { key: "isOpenTomorrow", value: function isOpenTomorrow() { - var tomorrow = (0, _add_days2.default)(new Date(), 1); + var tomorrow = _utils2.default.now(this.hours.timeZone).add(1, "days"); return this.willBeOpenOn(tomorrow); } }, { key: "isOpenAfterTomorrow", value: function isOpenAfterTomorrow() { - var afterTomorrow = (0, _add_days2.default)(new Date(), 2); + var afterTomorrow = _utils2.default.now(this.hours.timeZone).add(2, "days"); return this.willBeOpenOn(afterTomorrow); } }, { @@ -156,9 +146,10 @@ var BusinessHours = function () { value: function nextOpeningDate() { var includeToday = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - var date = (0, _add_days2.default)(new Date(), 1); - if (includeToday) { - date = new Date(); + var date = _utils2.default.now(this.hours.timeZone); + + if (!includeToday) { + date.add(1, "days"); } var nextOpeningDate = null; @@ -166,10 +157,10 @@ var BusinessHours = function () { if (this.willBeOpenOn(date)) { nextOpeningDate = date; } else { - date = (0, _add_days2.default)(date, 1); + date.add(1, "days"); } } - return nextOpeningDate; + return nextOpeningDate.hours(0).minutes(0).seconds(0); } }, { key: "nextOpeningHour", @@ -183,18 +174,21 @@ var BusinessHours = function () { }, { key: "_nextOpeningHour", value: function _nextOpeningHour(nextOpeningDate) { - var day = (0, _get_day2.default)(nextOpeningDate); + var _this2 = this; + + var day = this._getISOWeekDayName(nextOpeningDate.isoWeekday()); var firstDate = null; - this.hours[day.toString()].some(function (fromTo, index) { + this.hours[day].some(function (fromTo, index) { var from = fromTo.from; var to = fromTo.to; var fromHours = from.substr(0, 2); var fromMinutes = from.substr(3, 2); var toHours = to.substr(0, 2); var toMinutes = to.substr(3, 2); - var fromDate = (0, _set_hours2.default)((0, _set_minutes2.default)(nextOpeningDate, fromMinutes), fromHours); - if ((0, _is_before2.default)(new Date(), fromDate)) { + var fromDate = nextOpeningDate.hours(fromHours).minutes(fromMinutes).seconds(0); + + if (_utils2.default.now(_this2.hours.timeZone).isBefore(fromDate)) { firstDate = fromDate; return true; } @@ -205,6 +199,49 @@ var BusinessHours = function () { //nextOpeningDateText //nextOpeningHourText + }, { + key: "isOnHoliday", + value: function isOnHoliday() { + var now = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _utils2.default.now(this.hours.timeZone); + var callback = arguments[1]; + + if (_lodash2.default.isEmpty(this.hours)) { + throw new Error("Hours are not set. Check your init() function or configuration."); + } + if (_lodash2.default.isEmpty(this.hours.holidays)) { + this.hours.holidays = []; + } + for (var i = 0; i < this.hours.holidays.length; i++) { + if (this.hours.holidays[i].indexOf("-") > -1) { + var dates = this.hours.holidays[i].split("-"); + var beginDate = (0, _momentTimezone2.default)(dates[0]); + var endDate = (0, _momentTimezone2.default)(dates[1]); + if (now.isBetween(beginDate, endDate)) { + typeof callback === "function" && callback(); + return true; + } + } else { + var holidayDate = (0, _momentTimezone2.default)(this.hours.holidays[i]); + if (now.isSame(holidayDate, "day")) { + typeof callback === "function" && callback(); + return true; + } + } + } + return false; + } + }, { + key: "isOnHolidayInDays", + value: function isOnHolidayInDays() { + var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; + + if (!_lodash2.default.isInteger(x)) { + throw new Error("isOnHolidayInDays(:int) only accepts integers."); + } + var futureDate = _utils2.default.now(this.hours.timeZone).add(x, "days"); + + return this.isOnHoliday(futureDate); + } }]); return BusinessHours; diff --git a/dist/utils/index.js b/dist/utils/index.js new file mode 100644 index 0000000..63e808f --- /dev/null +++ b/dist/utils/index.js @@ -0,0 +1,33 @@ +"use strict"; + +var _momentTimezone = require("moment-timezone"); + +var _momentTimezone2 = _interopRequireDefault(_momentTimezone); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// hours and month start from 0 +// date starts at 1 +var utils = { + createDate: function createDate(year, month, date, hour, minute) { + var timeZone = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : "Europe/Amsterdam"; + + var newDate = _momentTimezone2.default.tz(timeZone); + newDate.set({ + year: year, + month: month, + date: date, + hour: hour, + minute: minute + }); + + return newDate; + }, + now: function now() { + var timeZone = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Europe/Amsterdam"; + + return _momentTimezone2.default.tz(timeZone); + } +}; + +module.exports = utils; \ No newline at end of file diff --git a/example/package.json b/example/package.json index 4b68b5a..4f89d2c 100644 --- a/example/package.json +++ b/example/package.json @@ -6,10 +6,11 @@ "react-scripts": "1.0.17" }, "dependencies": { - "business-hours.js": "1.0.3", + "business-hours.js": "latest", + "moment-timezone": "0.5.14", "react": "16.1.0", "react-dom": "16.1.0", - "react-live-clock": "2.0.1" + "react-live-clock": "2.0.2" }, "scripts": { "start": "react-scripts start", diff --git a/example/src/App.css b/example/src/App.css index c5c6e8a..b0514fa 100644 --- a/example/src/App.css +++ b/example/src/App.css @@ -9,7 +9,7 @@ .App-header { background-color: #222; - height: 150px; + height: 200px; padding: 20px; color: white; } @@ -23,6 +23,17 @@ } @keyframes App-logo-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.closed { + color: red; +} +.open { + color: green; } diff --git a/example/src/App.js b/example/src/App.js index 541ad96..9342822 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -13,20 +13,36 @@ class App extends Component { return (
- Now it is:{" "} - +

business-hours.js Example

+

+ {" "} + Browser timezone:{" "} + +

+

+ {" "} + Business timezone:{" "} + +

+

- Is open now? {businessHours.isOpenNow().toString()} + The business is currently:{" "} + {businessHours.isOpenNow() ? ( + OPEN + ) : ( + CLOSED + )}

-

Next opening hour: {businessHours.nextOpeningHour().toString()}

- Is it open tomorrow? {businessHours.isOpenTomorrow().toString()} + Next opening hour:{" "} + {businessHours.nextOpeningHour().format("YYYY-MM-DD HH:mm z")}

-

business-hours.js Example

-

- Sunday:
10:00-13:30 and 17:00-20:00 and 21:00-24:00 -

+

Defined business hours:

Monday:
10:00-13:30 and 18:00-22:00

@@ -45,6 +61,9 @@ class App extends Component {

Satuday:
10:00-13:30 and 18:00-22:00

+

+ Sunday:
10:00-13:30 and 17:00-20:00 and 21:00-24:00 +

); } diff --git a/example/src/hours.json b/example/src/hours.json index c0b4aa3..52cd942 100644 --- a/example/src/hours.json +++ b/example/src/hours.json @@ -1,19 +1,16 @@ { - "0": [ + "Monday": [ { "from": "10:00", "to": "13:30" }, { - "from": "17:00", - "to": "20:00" - }, - { - "from": "21:00", - "to": "24:00" + "from": "18:00", + "to": "22:00" } ], - "1": [ + "Tuesday": "closed", + "Wednesday": [ { "from": "10:00", "to": "13:30" @@ -23,8 +20,7 @@ "to": "22:00" } ], - "2": "closed", - "3": [ + "Thursday": [ { "from": "10:00", "to": "13:30" @@ -34,7 +30,7 @@ "to": "22:00" } ], - "4": [ + "Friday": [ { "from": "10:00", "to": "13:30" @@ -44,7 +40,7 @@ "to": "22:00" } ], - "5": [ + "Saturday": [ { "from": "10:00", "to": "13:30" @@ -54,14 +50,20 @@ "to": "22:00" } ], - "6": [ + "Sunday": [ { "from": "10:00", "to": "13:30" }, { - "from": "18:00", - "to": "22:00" + "from": "17:00", + "to": "20:00" + }, + { + "from": "21:00", + "to": "24:00" } - ] + ], + "holidays": ["2017/12/11", "2017/12/23-2018/01/02"], + "timeZone": "Europe/Amsterdam" } diff --git a/example/yarn.lock b/example/yarn.lock index a8481ea..e61712d 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -4163,6 +4163,12 @@ moment-timezone@0.5.13: dependencies: moment ">= 2.9.0" +moment-timezone@0.5.14: + version "0.5.14" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" + dependencies: + moment ">= 2.9.0" + moment@2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -5155,9 +5161,9 @@ react-error-overlay@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" -react-live-clock@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/react-live-clock/-/react-live-clock-2.0.1.tgz#f50c47059b6ab7f66979430803695a3bf2946174" +react-live-clock@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/react-live-clock/-/react-live-clock-2.0.2.tgz#c9e7a75b525db9c897f9a7b7f266cd3d671acc5d" dependencies: moment "2.18.1" moment-timezone "0.5.13" diff --git a/package.json b/package.json index 48a717c..184c8fd 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,19 @@ "scripts": { "commit": "git-cz", "precommit": "npm run build && npm run test:single", - "report-coverage": - "nyc report --reporter=lcov | codecov -t c1cea4d6-9aec-445c-8179-bc5543358876", + "report-coverage": "nyc report --reporter=lcov | codecov -t c1cea4d6-9aec-445c-8179-bc5543358876", "prebuild": "rimraf dist", - "build-with-files": - "npm run prebuild && babel --copy-files --out-dir dist --ignore *.test.js src", + "build-with-files": "npm run prebuild && babel --copy-files --out-dir dist --ignore *.test.js src", "build": "npm run prebuild && babel --out-dir dist --ignore *.test.js src", "test": "mocha --compilers js:babel-register src/index.test.js -w", "test:single": "nyc mocha --compilers js:babel-register src/index.test.js", - "semantic-release": - "semantic-release pre && npm publish && semantic-release post" + "semantic-release": "semantic-release pre && npm publish && semantic-release post" }, "babel": { - "presets": ["es2015", "stage-2"] + "presets": [ + "es2015", + "stage-2" + ] }, "repository": { "type": "git", @@ -55,8 +55,8 @@ } }, "dependencies": { - "date-fns": "1.28.5", "lodash": "4.17.4", - "mockdate": "2.0.2" + "mockdate": "2.0.2", + "moment-timezone": "0.5.14" } } diff --git a/src/hours.json b/src/hours.json index c0b4aa3..52cd942 100644 --- a/src/hours.json +++ b/src/hours.json @@ -1,19 +1,16 @@ { - "0": [ + "Monday": [ { "from": "10:00", "to": "13:30" }, { - "from": "17:00", - "to": "20:00" - }, - { - "from": "21:00", - "to": "24:00" + "from": "18:00", + "to": "22:00" } ], - "1": [ + "Tuesday": "closed", + "Wednesday": [ { "from": "10:00", "to": "13:30" @@ -23,8 +20,7 @@ "to": "22:00" } ], - "2": "closed", - "3": [ + "Thursday": [ { "from": "10:00", "to": "13:30" @@ -34,7 +30,7 @@ "to": "22:00" } ], - "4": [ + "Friday": [ { "from": "10:00", "to": "13:30" @@ -44,7 +40,7 @@ "to": "22:00" } ], - "5": [ + "Saturday": [ { "from": "10:00", "to": "13:30" @@ -54,14 +50,20 @@ "to": "22:00" } ], - "6": [ + "Sunday": [ { "from": "10:00", "to": "13:30" }, { - "from": "18:00", - "to": "22:00" + "from": "17:00", + "to": "20:00" + }, + { + "from": "21:00", + "to": "24:00" } - ] + ], + "holidays": ["2017/12/11", "2017/12/23-2018/01/02"], + "timeZone": "Europe/Amsterdam" } diff --git a/src/hoursMissingHolidays.json b/src/hoursMissingHolidays.json new file mode 100644 index 0000000..9668327 --- /dev/null +++ b/src/hoursMissingHolidays.json @@ -0,0 +1,67 @@ +{ + "Monday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } + ], + "Tuesday": "closed", + "Wednesday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } + ], + "Thursday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } + ], + "Friday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } + ], + "Saturday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "18:00", + "to": "22:00" + } + ], + "Sunday": [ + { + "from": "10:00", + "to": "13:30" + }, + { + "from": "17:00", + "to": "20:00" + }, + { + "from": "21:00", + "to": "24:00" + } + ] +} diff --git a/src/index.js b/src/index.js index a029c73..3abb217 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,14 @@ -import setMinutes from "date-fns/set_minutes"; -import setHours from "date-fns/set_hours"; -import getDay from "date-fns/get_day"; -import format from "date-fns/format"; -import isWithinRange from "date-fns/is_within_range"; -import isFuture from "date-fns/is_future"; -import addDays from "date-fns/add_days"; -import isEqual from "date-fns/is_equal"; -import isBefore from "date-fns/is_before"; +import moment from "moment-timezone"; +import utils from "./utils"; import _ from "lodash"; const weekdays = [ - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", - "Saturday" + "Saturday", + "Sunday" ]; class BusinessHours { @@ -32,6 +25,9 @@ class BusinessHours { if (isNaN(hour)) return false; return true; } + _getISOWeekDayName(isoDay) { + return weekdays[isoDay - 1]; + } init(hours) { if (_.isEmpty(hours)) { @@ -39,19 +35,19 @@ class BusinessHours { } weekdays.forEach((day, index) => { - if (!hours.hasOwnProperty(index.toString())) { + if (!hours.hasOwnProperty(day)) { throw new Error(day + " is missing from config"); } else { - if (hours[index.toString()] !== "closed") { - if (!hours[index.toString()][0].hasOwnProperty("from")) { + if (hours[day] !== "closed") { + if (!hours[day][0].hasOwnProperty("from")) { console.error(day + " is missing 'from' in config"); - } else if (!hours[index.toString()][0].hasOwnProperty("to")) { + } else if (!hours[day][0].hasOwnProperty("to")) { console.error(day + " is missing 'to' in config"); - } else if (!this._isHourValid(hours[index.toString()][0].from)) { + } else if (!this._isHourValid(hours[day][0].from)) { console.error( day + "'s 'from' has not the right format. Should be ##:##" ); - } else if (!this._isHourValid(hours[index.toString()][0].to)) { + } else if (!this._isHourValid(hours[day][0].to)) { console.error( day + "'s 'to' has not the right format. Should be ##:##" ); @@ -63,12 +59,15 @@ class BusinessHours { this.hours = hours; } - isClosedNow(now = new Date()) { + isClosedNow(now = utils.now(this.hours.timeZone)) { return !this.isOpenNow(now); } - isOpenNow(now = new Date()) { - const day = getDay(now); + isOpenNow(now = utils.now(this.hours.timeZone)) { + const day = this._getISOWeekDayName(now.isoWeekday()); + if (this.isOnHoliday(now)) { + return false; + } // console.log("now: ", format(now, "DD/MM/YYYY HH:mm")); let isOpenNow = false; if (this.hours[day.toString()] === "closed") return isOpenNow; @@ -79,9 +78,17 @@ class BusinessHours { const fromMinutes = from.substr(3, 2); const toHours = to.substr(0, 2); const toMinutes = to.substr(3, 2); - const fromDate = setHours(setMinutes(now, fromMinutes), fromHours); - const toDate = setHours(setMinutes(now, toMinutes), toHours); - isOpenNow = isWithinRange(now, fromDate, toDate); + let fromDate = now.clone(); + fromDate.set({ + hour: fromHours, + minute: fromMinutes + }); + let toDate = now.clone(); + toDate.set({ + hour: toHours, + minute: toMinutes + }); + isOpenNow = now.isBetween(fromDate, toDate); return isOpenNow; }); @@ -89,29 +96,34 @@ class BusinessHours { } willBeOpenOn(date) { - const day = getDay(date); - if (isFuture(date) || isEqual(new Date(), date)) { - if (this.hours[day.toString()] !== "closed") { + const day = this._getISOWeekDayName(date.isoWeekday()); + const now = utils.now(this.hours.timeZone); + if (now.isBefore(date) || now.isSame(date)) { + if (this.hours[day] !== "closed" && !this.isOnHoliday(date)) { return true; } else { return false; } } + return false; } + isOpenTomorrow() { - const tomorrow = addDays(new Date(), 1); + const tomorrow = utils.now(this.hours.timeZone).add(1, "days"); return this.willBeOpenOn(tomorrow); } + isOpenAfterTomorrow() { - const afterTomorrow = addDays(new Date(), 2); + const afterTomorrow = utils.now(this.hours.timeZone).add(2, "days"); return this.willBeOpenOn(afterTomorrow); } nextOpeningDate(includeToday = false) { - let date = addDays(new Date(), 1); - if (includeToday) { - date = new Date(); + let date = utils.now(this.hours.timeZone); + + if (!includeToday) { + date.add(1, "days"); } let nextOpeningDate = null; @@ -119,10 +131,13 @@ class BusinessHours { if (this.willBeOpenOn(date)) { nextOpeningDate = date; } else { - date = addDays(date, 1); + date.add(1, "days"); } } - return nextOpeningDate; + return nextOpeningDate + .hours(0) + .minutes(0) + .seconds(0); } nextOpeningHour() { @@ -133,21 +148,22 @@ class BusinessHours { return nextOpeningHour; } _nextOpeningHour(nextOpeningDate) { - const day = getDay(nextOpeningDate); + const day = this._getISOWeekDayName(nextOpeningDate.isoWeekday()); let firstDate = null; - this.hours[day.toString()].some((fromTo, index) => { + this.hours[day].some((fromTo, index) => { const from = fromTo.from; const to = fromTo.to; const fromHours = from.substr(0, 2); const fromMinutes = from.substr(3, 2); const toHours = to.substr(0, 2); const toMinutes = to.substr(3, 2); - const fromDate = setHours( - setMinutes(nextOpeningDate, fromMinutes), - fromHours - ); - if (isBefore(new Date(), fromDate)) { + let fromDate = nextOpeningDate + .hours(fromHours) + .minutes(fromMinutes) + .seconds(0); + + if (utils.now(this.hours.timeZone).isBefore(fromDate)) { firstDate = fromDate; return true; } @@ -157,6 +173,44 @@ class BusinessHours { } //nextOpeningDateText //nextOpeningHourText + + isOnHoliday(now = utils.now(this.hours.timeZone), callback) { + if (_.isEmpty(this.hours)) { + throw new Error( + "Hours are not set. Check your init() function or configuration." + ); + } + if (_.isEmpty(this.hours.holidays)) { + this.hours.holidays = []; + } + for (let i = 0; i < this.hours.holidays.length; i++) { + if (this.hours.holidays[i].indexOf("-") > -1) { + let dates = this.hours.holidays[i].split("-"); + let beginDate = moment(dates[0]); + let endDate = moment(dates[1]); + if (now.isBetween(beginDate, endDate)) { + typeof callback === "function" && callback(); + return true; + } + } else { + let holidayDate = moment(this.hours.holidays[i]); + if (now.isSame(holidayDate, "day")) { + typeof callback === "function" && callback(); + return true; + } + } + } + return false; + } + + isOnHolidayInDays(x = 1) { + if (!_.isInteger(x)) { + throw new Error("isOnHolidayInDays(:int) only accepts integers."); + } + let futureDate = utils.now(this.hours.timeZone).add(x, "days"); + + return this.isOnHoliday(futureDate); + } } module.exports = new BusinessHours(); diff --git a/src/index.test.js b/src/index.test.js index 85ded12..4ecc3ad 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -2,36 +2,73 @@ var expect = require("chai").expect; var bh = require("./index.js"); var hoursJson = require("./hours.json"); var hoursJson2 = require("./hours2.json"); +var hoursJsonMissingHolidays = require("./hoursMissingHolidays.json"); +var utils = require("./utils/index.js"); var MockDate = require("mockdate"); -import format from "date-fns/format"; -bh.init(hoursJson); +const TIMEZONE = "Europe/Luxembourg"; describe("business-hours-js", function() { - after(function() { + beforeEach(function() { + bh.init(hoursJson); + }); + afterEach(function() { MockDate.reset(); }); + describe("isOpenNow", function() { it("outside business hours, should equal false", function() { - expect(bh.isOpenNow(new Date(2017, 9, 1, 16, 0))).to.equal(false); + expect( + bh.isOpenNow(utils.createDate(2017, 9, 1, 14, 0, TIMEZONE)) + ).to.equal(false); }); it("inside business hours, should equal true", function() { - expect(bh.isOpenNow(new Date(2017, 9, 1, 18, 0))).to.equal(true); + expect( + bh.isOpenNow(utils.createDate(2017, 9, 1, 18, 0, TIMEZONE)) + ).to.equal(true); + }); + it("closed day, should equal false", function() { + expect( + bh.isOpenNow(utils.createDate(2017, 9, 3, 18, 0, TIMEZONE)) + ).to.equal(false); + }); + it("on holiday day, should equal false", function() { + expect( + bh.isOpenNow(utils.createDate(2017, 11, 9, 0, 0, TIMEZONE)) + ).to.equal(false); }); - it("cloded day, should equal false", function() { - expect(bh.isOpenNow(new Date(2017, 9, 3, 18, 0))).to.equal(false); + it("on holiday day (range), should equal false", function() { + expect( + bh.isOpenNow(utils.createDate(2017, 11, 24, 0, 0, TIMEZONE)) + ).to.equal(false); }); }); describe("isClosedNow", function() { it("outside business hours, should equal true", function() { - expect(bh.isClosedNow(new Date(2017, 9, 1, 16, 0))).to.equal(true); + expect( + bh.isClosedNow(utils.createDate(2017, 9, 1, 14, 0, TIMEZONE)) + ).to.equal(true); }); it("inside business hours, should equal false", function() { - expect(bh.isClosedNow(new Date(2017, 9, 1, 18, 0))).to.equal(false); + expect( + bh.isClosedNow(utils.createDate(2017, 9, 1, 18, 0, TIMEZONE)) + ).to.equal(false); }); - it("cloded day, should equal true", function() { - expect(bh.isClosedNow(new Date(2017, 9, 3, 18, 0))).to.equal(true); + it("closed day, should equal true", function() { + expect( + bh.isClosedNow(utils.createDate(2017, 9, 3, 18, 0, TIMEZONE)) + ).to.equal(true); + }); + it("on holiday day, should equal true", function() { + expect( + bh.isClosedNow(utils.createDate(2017, 11, 9, 0, 0, TIMEZONE)) + ).to.equal(true); + }); + it("on holiday day (range), should equal true", function() { + expect( + bh.isClosedNow(utils.createDate(2017, 11, 24, 0, 0, TIMEZONE)) + ).to.equal(true); }); }); @@ -58,13 +95,19 @@ describe("business-hours-js", function() { describe("willBeOpenOn", function() { it("with future date, should be true", function() { - expect(bh.willBeOpenOn(new Date(2019, 9, 3))).to.equal(true); + expect( + bh.willBeOpenOn(utils.createDate(2018, 0, 3, 0, 0, TIMEZONE)) + ).to.equal(true); }); it("with future date, on closed day, should be false", function() { - expect(bh.willBeOpenOn(new Date(2019, 9, 1))).to.equal(false); + expect( + bh.willBeOpenOn(utils.createDate(2019, 0, 1, 0, 0, TIMEZONE)) + ).to.equal(false); }); it("with past date, should be false", function() { - expect(bh.willBeOpenOn(new Date(2015, 9, 1))).to.equal(false); + expect( + bh.willBeOpenOn(utils.createDate(2015, 0, 1, 0, 0, TIMEZONE)) + ).to.equal(false); }); }); describe("isOpenTomorrow", function() { @@ -96,74 +139,140 @@ describe("business-hours-js", function() { describe("nextOpeningDate", function() { it("monday 2/10/2017, should return 4/10/2017", function() { MockDate.set("10/2/2017"); - var expectedDate = new Date(2017, 9, 4, 0, 0); - expect(bh.nextOpeningDate().getTime()).to.equal(expectedDate.getTime()); + var expectedDate = utils.createDate(2017, 9, 4, 0, 0, TIMEZONE); + expect(bh.nextOpeningDate().isSame(expectedDate, "day")).to.equal(true); MockDate.reset(); }); it("tuesday 3/10/2017, should return 4/10/2017", function() { MockDate.set("10/3/2017"); - var expectedDate = new Date(2017, 9, 4, 0, 0); - expect(bh.nextOpeningDate().getTime()).to.equal(expectedDate.getTime()); + var expectedDate = utils.createDate(2017, 9, 4, 0, 0, TIMEZONE); + expect(bh.nextOpeningDate().isSame(expectedDate, "day")).to.equal(true); MockDate.reset(); }); it("wednesday 4/10/2017, should return 5/10/2017", function() { MockDate.set("10/4/2017"); - var expectedDate = new Date(2017, 9, 5, 0, 0); - expect(bh.nextOpeningDate().getTime()).to.equal(expectedDate.getTime()); + var expectedDate = utils.createDate(2017, 9, 5, 0, 0, TIMEZONE); + expect(bh.nextOpeningDate().isSame(expectedDate, "day")).to.equal(true); MockDate.reset(); }); it("sunday 8/10/2017, should return monday 9/10/2017", function() { MockDate.set("10/8/2017"); - var expectedDate = new Date(2017, 9, 9, 0, 0); - expect(bh.nextOpeningDate().getTime()).to.equal(expectedDate.getTime()); + var expectedDate = utils.createDate(2017, 9, 9, 0, 0, TIMEZONE); + expect(bh.nextOpeningDate().isSame(expectedDate, "day")).to.equal(true); MockDate.reset(); }); it("monday 2/10/2017, should return monday 2/10/2017", function() { MockDate.set("10/2/2017"); - var expectedDate = new Date(2017, 9, 2, 0, 0); - expect(bh.nextOpeningDate(true).getTime()).to.equal( - expectedDate.getTime() + var expectedDate = utils.createDate(2017, 9, 2, 0, 0, TIMEZONE); + expect(bh.nextOpeningDate(true).isSame(expectedDate, "day")).to.equal( + true ); MockDate.reset(); }); }); describe("nextOpeningHour", function() { it("monday 2/10/2017 6:00, should return 2/10/2017 10:00", function() { - MockDate.set(new Date(2017, 9, 2, 6, 0)); - var expectedDate = new Date(2017, 9, 2, 10, 0); - expect(bh.nextOpeningHour().getTime()).to.equal(expectedDate.getTime()); + MockDate.set(utils.createDate(2017, 9, 2, 6, 0, TIMEZONE)); + var expectedDate = utils.createDate(2017, 9, 2, 10, 0); + + expect(bh.nextOpeningHour().isSame(expectedDate, "minute")).to.equal( + true + ); MockDate.reset(); }); it("monday 2/10/2017 16:00, should return 2/10/2017 18:00", function() { - MockDate.set(new Date(2017, 9, 2, 16, 0)); - var expectedDate = new Date(2017, 9, 2, 18, 0); - expect(bh.nextOpeningHour().getTime()).to.equal(expectedDate.getTime()); + MockDate.set(utils.createDate(2017, 9, 2, 16, 0, TIMEZONE)); + var expectedDate = utils.createDate(2017, 9, 2, 18, 0, TIMEZONE); + expect(bh.nextOpeningHour().isSame(expectedDate, "minute")).to.equal( + true + ); MockDate.reset(); }); it("sunday 1/10/2017 20:30, should return 1/10/2017 21:00", function() { - MockDate.set(new Date(2017, 9, 1, 20, 30)); - var expectedDate = new Date(2017, 9, 1, 21, 0); - expect(bh.nextOpeningHour().getTime()).to.equal(expectedDate.getTime()); + MockDate.set(utils.createDate(2017, 9, 1, 20, 30, TIMEZONE)); + var expectedDate = utils.createDate(2017, 9, 1, 21, 0, TIMEZONE); + expect(bh.nextOpeningHour().isSame(expectedDate, "minute")).to.equal( + true + ); MockDate.reset(); }); it("sunday 1/10/2017 21:30, should return 2/10/2017 10:00", function() { - MockDate.set(new Date(2017, 9, 1, 21, 30)); - var expectedDate = new Date(2017, 9, 2, 10, 0); - expect(bh.nextOpeningHour().getTime()).to.equal(expectedDate.getTime()); + MockDate.set(utils.createDate(2017, 9, 1, 21, 30, TIMEZONE)); + var expectedDate = utils.createDate(2017, 9, 2, 10, 0, TIMEZONE); + expect(bh.nextOpeningHour().isSame(expectedDate, "minute")).to.equal( + true + ); MockDate.reset(); }); it("tuesday(closed) 3/10/2017 18:30, should return 4/10/2017 10:00", function() { - MockDate.set(new Date(2017, 9, 3, 18, 30)); - var expectedDate = new Date(2017, 9, 4, 10, 0); - expect(bh.nextOpeningHour().getTime()).to.equal(expectedDate.getTime()); + MockDate.set(utils.createDate(2017, 9, 3, 18, 30, TIMEZONE)); + var expectedDate = utils.createDate(2017, 9, 4, 10, 0, TIMEZONE); + expect(bh.nextOpeningHour().isSame(expectedDate, "minute")).to.equal( + true + ); MockDate.reset(); }); }); + describe("isOnHoliday", function() { + it("on holiday 2017/12/11", function() { + MockDate.set(utils.createDate(2017, 11, 11, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(true); + }); + it("after a holiday 2017/12/12", function() { + MockDate.set(utils.createDate(2017, 11, 12, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(false); + }); + it("before a holiday 2017/12/9", function() { + MockDate.set(utils.createDate(2017, 11, 9, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(false); + }); + it("in range 2017/12/24", function() { + MockDate.set(utils.createDate(2017, 11, 24, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(true); + }); + it("within range, with year change 2018/01/01", function() { + MockDate.set(utils.createDate(2018, 0, 1, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(true); + }); + it("outside range, with year change 2018/01/04", function() { + MockDate.set(utils.createDate(2018, 0, 4, 0, 0, TIMEZONE)); + expect(bh.isOnHoliday()).to.equal(false); + }); + it("missing holidays in config", function() { + bh.init(hoursJsonMissingHolidays); + expect(bh.isOnHoliday()).to.equal(false); + }); + }); + describe("isOnHolidayInDays", function() { + it("on holiday 3 days from 2017/12/08", function() { + MockDate.set(utils.createDate(2017, 11, 8, 0, 0, TIMEZONE)); + expect(bh.isOnHolidayInDays(3)).to.equal(true); + }); + it('Nan param "3 days"', function() { + expect(bh.isOnHolidayInDays.bind(bh, "3 days")).to.throw( + "isOnHolidayInDays(:int) only accepts integers." + ); + }); + it('NaN param "3"', function() { + expect(bh.isOnHolidayInDays.bind(bh, "3")).to.throw( + "isOnHolidayInDays(:int) only accepts integers." + ); + }); + it('NaN param "undefined", x defaults to 1', function() { + MockDate.set(utils.createDate(2017, 11, 8, 0, 0, TIMEZONE)); + expect(bh.isOnHolidayInDays(undefined)).to.equal(false); + }); + it('NaN param ""', function() { + expect(bh.isOnHolidayInDays.bind(bh, "")).to.throw( + "isOnHolidayInDays(:int) only accepts integers." + ); + }); + }); //MockDate.set('1/1/2000'); describe("init", function() { - it("missing sunday", function() { + it("missing monday", function() { expect(bh.init.bind(bh, hoursJson2)).to.throw( - "Sunday is missing from config" + "Monday is missing from config" ); }); it("missing config, empty object", function() { @@ -183,7 +292,7 @@ describe("business-hours-js", function() { }); it("missing config, {'a':'test'} ", function() { expect(bh.init.bind(bh, { a: "test" })).to.throw( - "Sunday is missing from config" + "Monday is missing from config" ); }); }); diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..8e3abee --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,24 @@ +import moment from "moment-timezone"; + +// hours and month start from 0 +// date starts at 1 +var utils = { + createDate(year, month, date, hour, minute, timeZone = "Europe/Amsterdam") { + let newDate = moment.tz(timeZone); + newDate.set({ + year: year, + month: month, + date: date, + hour: hour, + minute: minute + }); + + return newDate; + }, + + now(timeZone = "Europe/Amsterdam") { + return moment.tz(timeZone); + } +}; + +module.exports = utils;