├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── OpeningHours.js ├── OpeningHoursForDay.js ├── Time.js ├── TimeRange.js ├── index.js └── lib │ ├── Arr.js │ ├── Day.js │ └── Validation.js └── test ├── .eslintrc ├── OpeningHoursFillTest.js ├── OpeningHoursForDayTest.js ├── OpeningHoursTest.js ├── TimeRangeTest.js └── TimeTest.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [{package.json,*.scss,*.css}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "experimentalObjectRestSpread": true 7 | } 8 | }, 9 | "env": { 10 | "es6": true, 11 | "browser": true, 12 | "node": true 13 | }, 14 | "extends": "eslint:recommended", 15 | "rules": { 16 | "comma-dangle": [2, "always-multiline"], 17 | "indent": [2, 4], 18 | "linebreak-style": [2, "unix"], 19 | "quotes": [2, "single"], 20 | "semi": [2, "always"], 21 | "space-before-function-paren": ["error", { "anonymous": "always", "named": "never" }], 22 | "strict": [2, "never"] 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `opening-hours` will be documented in this file 4 | 5 | ## 1.0.0 6 | - Initial release 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Since this is an internal project, we don't accept pull requests at this time. 4 | 5 | **OR** 6 | 7 | Contributions are **welcome** and will be fully **credited**. 8 | 9 | We accept contributions via Pull Requests on [Github](https://github.com/spatie/opening-hours). 10 | 11 | ## Pull Requests 12 | 13 | - Use the ES2015 syntax. 14 | - Your patch won't be accepted if it doesn't pass the tests and lints (`npm run test`). 15 | - If there's a `/demo` section, try to add an example. 16 | - **Document any change in behaviour:** Make sure the `README.md`, `CHANGELOG.md` and any other relevant documentation are kept up-to-date. 17 | - **Consider our release cycle:** We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 18 | - **Create feature branches:** Don't ask us to pull from your master branch. 19 | - **One pull request per feature:** If you want to do more than one thing, send multiple pull requests. 20 | - **Send coherent history:** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 21 | 22 | ## Running Tests 23 | 24 | ``` bash 25 | $ npm test 26 | ``` 27 | 28 | **Happy coding**! 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Spatie bvba 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS PACKAGE IS NOT MAINTAINED ANYMORE** 2 | 3 | # opening-hours 4 | 5 | [![Latest Version on NPM](https://img.shields.io/npm/v/opening-hours.svg?style=flat-square)](https://npmjs.com/package/opening-hours) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 7 | [![Build Status](https://img.shields.io/travis/spatie/opening-hours/master.svg?style=flat-square)](https://travis-ci.org/spatie/opening-hours) 8 | [![Code Climate](https://img.shields.io/codeclimate/github/spatie/opening-hours.svg?style=flat-square)](https://img.shields.io/codeclimate/github/spatie/opening-hours.svg) 9 | 10 | This is where your description should go. Try and limit it to a paragraph or two, and maybe throw in a mention of what 11 | PSRs you support to avoid any confusion with users and contributors. 12 | 13 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 14 | 15 | ## Install 16 | 17 | You can install the package via npm: 18 | 19 | ```bash 20 | npm install opening-hours 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | const myPackage = require('my-package'); 27 | 28 | myPackage.doStuff(); 29 | ``` 30 | 31 | ## Change log 32 | 33 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 34 | 35 | ## Testing 36 | 37 | ``` bash 38 | npm run test 39 | ``` 40 | 41 | ## Contributing 42 | 43 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 44 | 45 | ## Security 46 | 47 | If you discover any security related issues, please contact [Sebastian De Deyne](https://github.com/sebastiandedeyne) instead of using the issue tracker. 48 | 49 | ## Credits 50 | 51 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 52 | - [All Contributors](../../contributors) 53 | 54 | ## About Spatie 55 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 56 | 57 | ## License 58 | 59 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opening-hours", 3 | "version": "0.3.3", 4 | "description": ":package_description", 5 | "main": "dist/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "lint": "eslint src test", 9 | "prepublish": "npm test && npm run build", 10 | "build": "babel src -d dist", 11 | "test": "mocha --compilers js:babel-register" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/spatie/opening-hours.git" 16 | }, 17 | "keywords": [ 18 | "spatie" 19 | ], 20 | "author": "Sebastian De Deyne", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/spatie/opening-hours/issues" 24 | }, 25 | "homepage": "https://github.com/spatie/opening-hours", 26 | "devDependencies": { 27 | "babel": "^6.5.2", 28 | "babel-cli": "^6.16.0", 29 | "babel-eslint": "^7.0.0", 30 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 31 | "babel-preset-es2015": "^6.16.0", 32 | "babel-register": "^6.16.3", 33 | "chai": "^3.5.0", 34 | "eslint": "^3.7.0", 35 | "mocha": "^3.1.0" 36 | }, 37 | "dependencies": { 38 | "lodash": "^4.16.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/OpeningHours.js: -------------------------------------------------------------------------------- 1 | import { forEach, forIn, mapValues } from 'lodash'; 2 | import { isValidDateString, isValidDayName } from './lib/Validation'; 3 | import Day from './lib/Day'; 4 | import OpeningHoursForDay from './OpeningHoursForDay'; 5 | 6 | class OpeningHours { 7 | constructor() { 8 | this._openingHours = {}; 9 | Day.days().forEach(day => this._openingHours[day] = new OpeningHoursForDay()); 10 | } 11 | 12 | static create(data) { 13 | return (new OpeningHours()).fill(data); 14 | } 15 | 16 | fill(data) { 17 | const { openingHours, exceptions } = this._parseOpeningHoursAndExceptions(data); 18 | 19 | forEach(openingHours, (openingHours, day) => { 20 | this._setOpeningHoursFromStrings(day, openingHours); 21 | }); 22 | 23 | this._setExceptionsFromStrings(exceptions); 24 | 25 | return this; 26 | } 27 | 28 | forWeek() { 29 | return this._openingHours; 30 | } 31 | 32 | forDay(day) { 33 | day = this._normalizeDayName(day); 34 | 35 | return this._openingHours[day]; 36 | } 37 | 38 | exceptions() { 39 | return this._exceptions; 40 | } 41 | 42 | isOpenOn(day) { 43 | return this.forDay(day).openingHours.length > 0; 44 | } 45 | 46 | isClosedOn(day) { 47 | return this.isOpenOn(day); 48 | } 49 | 50 | _parseOpeningHoursAndExceptions(data) { 51 | const { exceptions = [], ...openingHours } = data; 52 | 53 | forEach(openingHours, (openingHoursData, day) => { 54 | openingHours[this._normalizeDayName(day)] = openingHoursData; 55 | }); 56 | 57 | return { openingHours, exceptions }; 58 | } 59 | 60 | _setOpeningHoursFromStrings(day, openingHours) { 61 | day = this._normalizeDayName(day); 62 | 63 | this._openingHours[day] = OpeningHoursForDay.fromStrings(openingHours); 64 | } 65 | 66 | _setExceptionsFromStrings(exceptions) { 67 | forIn(exceptions, (_, date) => { 68 | if (! isValidDateString(date)) { 69 | throw new Error(`Exception \`${date}\` isn't a valid date string. ` + 70 | 'Date strings must be formatted as `YYYY-MM-DD`, e.g. `2016-10-06`.'); 71 | } 72 | }); 73 | 74 | this._exceptions = mapValues(exceptions, (openingHours) => { 75 | return OpeningHoursForDay.fromStrings(openingHours); 76 | }); 77 | } 78 | 79 | _normalizeDayName(day) { 80 | day = day.toLowerCase(); 81 | 82 | if (! isValidDayName(day)) { 83 | throw new Error(`Day \`${day}\` isn't a valid day name. ` + 84 | 'Valid day names are lowercase english words, e.g. `monday`, `thursday`.'); 85 | } 86 | 87 | return day; 88 | } 89 | } 90 | 91 | export default OpeningHours; 92 | -------------------------------------------------------------------------------- /src/OpeningHoursForDay.js: -------------------------------------------------------------------------------- 1 | import { hasTimeRangeOverlap } from './lib/Validation'; 2 | import TimeRange from './TimeRange'; 3 | 4 | class OpeningHoursForDay { 5 | constructor() { 6 | this._openingHours = []; 7 | } 8 | 9 | static fromStrings(strings) { 10 | const openingHoursForDay = new OpeningHoursForDay(); 11 | 12 | openingHoursForDay._guardAgainstTimeRangeOverlaps(strings); 13 | 14 | openingHoursForDay._openingHours = strings.map(string => TimeRange.fromString(string)); 15 | 16 | return openingHoursForDay; 17 | } 18 | 19 | get openingHours() { 20 | return this._openingHours; 21 | } 22 | 23 | isOpenAt(time) { 24 | for (let timeRange of this.openingHours) { 25 | if (timeRange.containsTime(time)) { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | 33 | _guardAgainstTimeRangeOverlaps(openingHours) { 34 | if (hasTimeRangeOverlap(openingHours)) { 35 | throw new Error('Time ranges aren\'t allowed to overlap.'); 36 | } 37 | } 38 | } 39 | 40 | export default OpeningHoursForDay; 41 | -------------------------------------------------------------------------------- /src/Time.js: -------------------------------------------------------------------------------- 1 | import { isValidTimeString } from './lib/Validation'; 2 | import { padStart } from 'lodash'; 3 | 4 | class Time { 5 | constructor(hours, minutes) { 6 | this._hours = hours; 7 | this._minutes = minutes; 8 | } 9 | 10 | static fromString(string) { 11 | if (! isValidTimeString(string)) { 12 | throw new Error(`The string \`${string}\` isn't a valid time string. ` 13 | + 'A time string must be a formatted as `H:i`, e.g. `06:00`, `18:00`.'); 14 | } 15 | 16 | const [ hours, minutes ] = string.split(':'); 17 | 18 | return new Time(parseInt(hours), parseInt(minutes)); 19 | } 20 | 21 | get hours() { 22 | return this._hours; 23 | } 24 | 25 | get minutes() { 26 | return this._minutes; 27 | } 28 | 29 | isSame(time) { 30 | return this._hours === time.hours && this._minutes === time.minutes; 31 | } 32 | 33 | isAfter(time) { 34 | if (this.isSame(time)) { 35 | return false; 36 | } 37 | 38 | if (this._hours > time.hours) { 39 | return true; 40 | } 41 | 42 | return this._hours === time.hours && this._minutes >= time.minutes; 43 | } 44 | 45 | isBefore(time) { 46 | if (this.isSame(time)) { 47 | return false; 48 | } 49 | 50 | return ! this.isAfter(time); 51 | } 52 | 53 | isSameOrAfter(time) { 54 | return this.isSame(time) || this.isAfter(time); 55 | } 56 | 57 | toString() { 58 | return `${padStart(this._hours, 2, '0')}:${padStart(this._minutes, 2, '0')}`; 59 | } 60 | } 61 | 62 | export default Time; 63 | -------------------------------------------------------------------------------- /src/TimeRange.js: -------------------------------------------------------------------------------- 1 | import { isValidTimeRangeString } from './lib/Validation'; 2 | import Time from './Time'; 3 | 4 | class TimeRange { 5 | constructor(start, end) { 6 | this._start = start; 7 | this._end = end; 8 | } 9 | 10 | static fromString(string) { 11 | if (! isValidTimeRangeString(string)) { 12 | throw new Error(`The string \`${string}\` isn't a valid time range string. ` 13 | + 'A time string must be a formatted as `H:i-H:i`, e.g. `09:00-18:00`.'); 14 | } 15 | 16 | const times = string.split('-'); 17 | 18 | return new TimeRange(Time.fromString(times[0]), Time.fromString(times[1])); 19 | } 20 | 21 | get start() { 22 | return this._start; 23 | } 24 | 25 | get end() { 26 | return this._end; 27 | } 28 | 29 | spillsOverToNextDay() { 30 | return this._end.isBefore(this._start); 31 | } 32 | 33 | containsTime(time) { 34 | if (this.spillsOverToNextDay()) { 35 | if (time.isAfter(this._start)) { 36 | return time.isAfter(this._end); 37 | } 38 | 39 | return time.isBefore(this._end); 40 | } 41 | 42 | return time.isSameOrAfter(this._start) && time.isBefore(this._end); 43 | } 44 | 45 | overlaps(timeRange) { 46 | return this.containsTime(timeRange.start) || this.containsTime(timeRange.end); 47 | } 48 | 49 | toString() { 50 | return `${this._start.toString()}-${this._end.toString()}`; 51 | } 52 | } 53 | 54 | export default TimeRange; 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Day } from './lib/Day'; 2 | export { default as OpeningHours } from './OpeningHours'; 3 | export { default as OpeningHoursForDay } from './OpeningHoursForDay'; 4 | export { default as Time } from './Time'; 5 | export { default as TimeRange } from './TimeRange'; 6 | export { default as Validation } from './lib/Validation'; 7 | 8 | export { default } from './OpeningHours'; 9 | -------------------------------------------------------------------------------- /src/lib/Arr.js: -------------------------------------------------------------------------------- 1 | export const createUniquePairs = (array) => { 2 | const pairs = []; 3 | 4 | // Create a clone of the array first so we don't modify the original. 5 | array = array.slice(); 6 | 7 | while (array.length > 1) { 8 | const a = array.shift(); 9 | array.forEach(b => pairs.push([a, b])); 10 | } 11 | 12 | return pairs; 13 | }; 14 | 15 | export default { 16 | createUniquePairs, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/Day.js: -------------------------------------------------------------------------------- 1 | export const days = () => { 2 | return ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; 3 | }; 4 | 5 | export default { 6 | days, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/Validation.js: -------------------------------------------------------------------------------- 1 | import { createUniquePairs } from './Arr'; 2 | import { days } from './Day'; 3 | import TimeRange from '../TimeRange'; 4 | 5 | export const hasTimeRangeOverlap = (timeRanges) => { 6 | for (const [rangeA, rangeB] of createUniquePairs(timeRanges)) { 7 | if (TimeRange.fromString(rangeA).overlaps(TimeRange.fromString(rangeB))) { 8 | return true; 9 | } 10 | } 11 | return false; 12 | }; 13 | 14 | export const isValidDateString = (string) => { 15 | const parts = string.split('-'); 16 | 17 | if (parts.length !== 3) { 18 | return false; 19 | } 20 | 21 | const year = parseInt(parts[0]); 22 | const month = parseInt(parts[1]); 23 | const day = parseInt(parts[2]); 24 | 25 | // http://stackoverflow.com/a/8390325/999733 26 | 27 | if((month < 1) || (month > 12)) { 28 | return false; 29 | } 30 | 31 | if((day < 1) || (day > 31)) { 32 | return false; 33 | } 34 | 35 | if(((month == 4) || (month == 6) || (month == 9) || (month == 11)) && (day > 30)) { 36 | return false; 37 | } 38 | 39 | if((month == 2) && (((year % 400) == 0) || ((year % 4) == 0)) && ((year % 100) != 0) && (day > 29)) { 40 | return false; 41 | } 42 | 43 | if((month == 2) && ((year % 100) == 0) && (day > 29)) { 44 | return false; 45 | } 46 | 47 | if((month == 2) && (day > 28)) { 48 | return false; 49 | } 50 | 51 | return true; 52 | }; 53 | 54 | export const isValidDayName = (name) => { 55 | return days().filter(day => day === name).length > 0; 56 | }; 57 | 58 | export const isValidTimeString = (string) => { 59 | return string.match('^(([0-1][0-9])|(2[0-4])):[0-5][0-9]$'); 60 | }; 61 | 62 | export const isValidTimeRangeString = (string) => { 63 | const times = string.split('-'); 64 | 65 | if (times.length !== 2) { 66 | return false; 67 | } 68 | 69 | return isValidTimeString(times[0]) && isValidTimeString(times[1]); 70 | }; 71 | 72 | export default { 73 | hasTimeRangeOverlap, 74 | isValidDateString, 75 | isValidDayName, 76 | isValidTimeString, 77 | isValidTimeRangeString, 78 | }; 79 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | "env": { 2 | "mocha": true 3 | } 4 | -------------------------------------------------------------------------------- /test/OpeningHoursFillTest.js: -------------------------------------------------------------------------------- 1 | import OpeningHours, { Day } from '../src'; 2 | import { assert } from 'chai'; 3 | 4 | describe('OpeningHours::fill', () => { 5 | 6 | it('fills_opening_hours', () => { 7 | const openingHours = OpeningHours.create({ 8 | monday: ['09:00-18:00'], 9 | tuesday: ['09:00-18:00'], 10 | wednesday: ['09:00-12:00', '14:00-18:00'], 11 | thursday: [], 12 | friday: ['09:00-20:00'], 13 | exceptions: { 14 | '2016-09-26': [], 15 | }, 16 | }); 17 | 18 | assert.strictEqual(openingHours.forDay('monday').openingHours[0].toString(), '09:00-18:00'); 19 | assert.strictEqual(openingHours.forDay('tuesday').openingHours[0].toString(), '09:00-18:00'); 20 | assert.strictEqual(openingHours.forDay('wednesday').openingHours[0].toString(), '09:00-12:00'); 21 | assert.strictEqual(openingHours.forDay('wednesday').openingHours[1].toString(), '14:00-18:00'); 22 | assert.lengthOf(openingHours.forDay('thursday').openingHours, 0); 23 | assert.strictEqual(openingHours.forDay('friday').openingHours[0].toString(), '09:00-20:00'); 24 | }); 25 | 26 | it('can_handle_empty_input', () => { 27 | const openingHours = OpeningHours.create({}); 28 | 29 | Day.days().forEach(day => { 30 | assert.lengthOf(openingHours.forDay(day).openingHours, 0); 31 | }); 32 | }); 33 | 34 | it('handles_day_names_in_a_case_insensitive_manner', () => { 35 | const openingHours = OpeningHours.create({ 36 | Monday: ['09:00-18:00'], 37 | }); 38 | 39 | assert.strictEqual(openingHours.forDay('monday').openingHours[0].toString(), '09:00-18:00'); 40 | assert.strictEqual(openingHours.forDay('Monday').openingHours[0].toString(), '09:00-18:00'); 41 | }); 42 | 43 | it('will_throw_an_exception_when_using_an_invalid_day_name', () => { 44 | assert.throws(() => { 45 | OpeningHours.create({ 46 | mmmmonday: ['09:00-18:00'], 47 | }); 48 | }); 49 | }); 50 | 51 | it('will_throw_an_exception_when_using_an_invalid_exception_date', () => { 52 | assert.throws(() => { 53 | OpeningHours.create({ 54 | exceptions: { 55 | '2016-01-32': [], 56 | }, 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/OpeningHoursForDayTest.js: -------------------------------------------------------------------------------- 1 | import { OpeningHoursForDay, Time } from '../src'; 2 | import { assert } from 'chai'; 3 | 4 | describe('OpeningHoursForDay', () => { 5 | 6 | it('can be created from an array of time range strings', () => { 7 | const openingHoursForDay = OpeningHoursForDay.fromStrings(['09:00-12:00', '13:00-18:00']); 8 | assert.lengthOf(openingHoursForDay.openingHours, 2); 9 | assert.strictEqual(openingHoursForDay.openingHours[0].toString(), '09:00-12:00'); 10 | assert.strictEqual(openingHoursForDay.openingHours[1].toString(), '13:00-18:00'); 11 | }); 12 | 13 | it('cant be created when time ranges overlap', () => { 14 | assert.throws(() => { 15 | OpeningHoursForDay.fromStrings(['09:00-18:00', '14:00-20:00']); 16 | }); 17 | }); 18 | 19 | it('can determine whether its open at a time', () => { 20 | const openingHoursForDay = OpeningHoursForDay.fromStrings(['09:00-18:00']); 21 | assert.isTrue(openingHoursForDay.isOpenAt(Time.fromString('09:00'))); 22 | assert.isFalse(openingHoursForDay.isOpenAt(Time.fromString('08:00'))); 23 | assert.isFalse(openingHoursForDay.isOpenAt(Time.fromString('18:00'))); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/OpeningHoursTest.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import OpeningHours from '../src/OpeningHours'; 3 | 4 | describe('OpeningHours', () => { 5 | 6 | it('can return the opening hours for a regular week', () => { 7 | const openingHours = OpeningHours.create({ 8 | monday: ['09:00-18:00'], 9 | }); 10 | 11 | const openingHoursForWeek = openingHours.forWeek(); 12 | 13 | assert.strictEqual('09:00-18:00', openingHoursForWeek['monday'].openingHours[0].toString()); 14 | assert.lengthOf(openingHoursForWeek['tuesday'].openingHours, 0); 15 | assert.lengthOf(openingHoursForWeek['wednesday'].openingHours, 0); 16 | assert.lengthOf(openingHoursForWeek['thursday'].openingHours, 0); 17 | assert.lengthOf(openingHoursForWeek['friday'].openingHours, 0); 18 | assert.lengthOf(openingHoursForWeek['saturday'].openingHours, 0); 19 | assert.lengthOf(openingHoursForWeek['sunday'].openingHours, 0); 20 | }); 21 | 22 | it('can return the exceptions', () => { 23 | const openingHours = OpeningHours.create({ 24 | monday: ['09:00-18:00'], 25 | exceptions: { 26 | '2016-09-26': [], 27 | }, 28 | }); 29 | 30 | const exceptions = openingHours.exceptions(); 31 | 32 | assert.property(exceptions, '2016-09-26'); 33 | assert.lengthOf(exceptions['2016-09-26'].openingHours, 0); 34 | }); 35 | 36 | it('can return the opening hours for a regular week day', () => { 37 | const openingHours = OpeningHours.create({ 38 | monday: ['09:00-18:00'], 39 | }); 40 | 41 | const openingHoursForMonday = openingHours.forDay('monday'); 42 | assert.lengthOf(openingHoursForMonday.openingHours, 1); 43 | assert.strictEqual('09:00-18:00', openingHoursForMonday.openingHours[0].toString()); 44 | 45 | const openingHoursForTuesday = openingHours.forDay('tuesday'); 46 | assert.lengthOf(openingHoursForTuesday.openingHours, 0); 47 | }); 48 | 49 | it('can determine that its regularly open on a week day', () => { 50 | const openingHours = OpeningHours.create({ 51 | monday: ['09:00-18:00'], 52 | }); 53 | 54 | assert.isTrue(openingHours.isOpenOn('monday')); 55 | assert.isFalse(openingHours.isOpenOn('tuesday')); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/TimeRangeTest.js: -------------------------------------------------------------------------------- 1 | import { Time, TimeRange } from '../src'; 2 | import { assert } from 'chai'; 3 | 4 | describe('TimeRange', () => { 5 | 6 | it('can be created from a string', () => { 7 | const timeRange = TimeRange.fromString('16:00-18:00'); 8 | 9 | assert.strictEqual(timeRange.start.hours, 16); 10 | assert.strictEqual(timeRange.start.minutes, 0); 11 | assert.strictEqual(timeRange.end.hours, 18); 12 | assert.strictEqual(timeRange.end.minutes, 0); 13 | }); 14 | 15 | it('can\'t be created from an invalid range', () => { 16 | assert.throws(() => { 17 | TimeRange.fromString('16:00/18:00'); 18 | }); 19 | }); 20 | 21 | it('can determine that it spills over to the next day', () => { 22 | assert.isTrue(TimeRange.fromString('18:00-01:00').spillsOverToNextDay()); 23 | assert.isFalse(TimeRange.fromString('18:00-23:00').spillsOverToNextDay()); 24 | }); 25 | 26 | it('can determine that it contains a time', () => { 27 | assert.isTrue(TimeRange.fromString('16:00-18:00').containsTime(Time.fromString('16:00'))); 28 | assert.isTrue(TimeRange.fromString('16:00-18:00').containsTime(Time.fromString('17:00'))); 29 | assert.isFalse(TimeRange.fromString('16:00-18:00').containsTime(Time.fromString('18:00'))); 30 | 31 | assert.isTrue(TimeRange.fromString('18:00-01:00').containsTime(Time.fromString('00:30'))); 32 | assert.isTrue(TimeRange.fromString('18:00-01:00').containsTime(Time.fromString('22:00'))); 33 | assert.isFalse(TimeRange.fromString('18:00-01:00').containsTime(Time.fromString('17:00'))); 34 | assert.isFalse(TimeRange.fromString('18:00-01:00').containsTime(Time.fromString('02:00'))); 35 | }); 36 | 37 | it('can determine that it overlaps another time range', () => { 38 | assert.isTrue(TimeRange.fromString('16:00-18:00').overlaps(TimeRange.fromString('15:00-17:00'))); 39 | assert.isTrue(TimeRange.fromString('16:00-18:00').overlaps(TimeRange.fromString('17:00-19:00'))); 40 | assert.isTrue(TimeRange.fromString('16:00-18:00').overlaps(TimeRange.fromString('17:00-17:30'))); 41 | 42 | assert.isTrue(TimeRange.fromString('22:00-02:00').overlaps(TimeRange.fromString('21:00-23:00'))); 43 | assert.isTrue(TimeRange.fromString('22:00-02:00').overlaps(TimeRange.fromString('01:00-02:00'))); 44 | assert.isTrue(TimeRange.fromString('22:00-02:00').overlaps(TimeRange.fromString('23:00-23:30'))); 45 | 46 | assert.isFalse(TimeRange.fromString('16:00-18:00').overlaps(TimeRange.fromString('14:00-15:00'))); 47 | assert.isFalse(TimeRange.fromString('16:00-18:00').overlaps(TimeRange.fromString('19:00-20:00'))); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/TimeTest.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { Time } from '../src'; 3 | 4 | describe('Time', () => { 5 | 6 | it('can be created from a string', () => { 7 | const time = Time.fromString('16:00'); 8 | assert.strictEqual(time.hours, 16); 9 | assert.strictEqual(time.minutes, 0); 10 | }); 11 | 12 | it('can\'t be created from an invalid string', () => { 13 | assert.throws(() => { 14 | Time.fromString('aa:bb'); 15 | }); 16 | }); 17 | 18 | it('can determine that its the same as another time', () => { 19 | assert.isTrue(Time.fromString('09:00').isSame(Time.fromString('09:00'))); 20 | assert.isFalse(Time.fromString('09:00').isSame(Time.fromString('10:00'))); 21 | assert.isFalse(Time.fromString('09:00').isSame(Time.fromString('09:30'))); 22 | }); 23 | 24 | it('it can determine that its before another time', () => { 25 | assert.isTrue(Time.fromString('09:00').isBefore(Time.fromString('10:00'))); 26 | assert.isTrue(Time.fromString('09:00').isBefore(Time.fromString('09:30'))); 27 | assert.isFalse(Time.fromString('09:00').isBefore(Time.fromString('09:00'))); 28 | assert.isFalse(Time.fromString('09:00').isBefore(Time.fromString('08:00'))); 29 | assert.isFalse(Time.fromString('09:00').isBefore(Time.fromString('08:30'))); 30 | assert.isFalse(Time.fromString('08:30').isBefore(Time.fromString('08:00'))); 31 | }); 32 | 33 | it('it can determine that its after another time', () => { 34 | assert.isTrue(Time.fromString('09:00').isAfter(Time.fromString('08:00'))); 35 | assert.isTrue(Time.fromString('09:30').isAfter(Time.fromString('09:00'))); 36 | assert.isTrue(Time.fromString('09:00').isAfter(Time.fromString('08:30'))); 37 | assert.isFalse(Time.fromString('09:00').isAfter(Time.fromString('09:00'))); 38 | assert.isFalse(Time.fromString('09:00').isAfter(Time.fromString('09:30'))); 39 | assert.isFalse(Time.fromString('09:00').isAfter(Time.fromString('10:00'))); 40 | }); 41 | 42 | it('it can determine that its the same or after another time', () => { 43 | assert.isTrue(Time.fromString('09:00').isSameOrAfter(Time.fromString('08:00'))); 44 | assert.isTrue(Time.fromString('09:00').isSameOrAfter(Time.fromString('09:00'))); 45 | assert.isTrue(Time.fromString('09:30').isSameOrAfter(Time.fromString('09:30'))); 46 | assert.isTrue(Time.fromString('09:30').isSameOrAfter(Time.fromString('09:00'))); 47 | assert.isFalse(Time.fromString('09:00').isSameOrAfter(Time.fromString('10:00'))); 48 | }); 49 | }); 50 | --------------------------------------------------------------------------------