UNPKG

jquery-yohours

Version:

Jquery and bootstrap based input widget to create opening_hours data

1,581 lines (1,468 loc) 165 kB
/* globals jQuery,window,moment,jSmart:false */ var YoHours = YoHours || {}; (function (window, $, moment, YoHours, jSmart, Jed) { 'use strict'; let i18n; jSmart.prototype.registerPlugin('modifier', 't', function (s, ctx) { let t = s; if (i18n instanceof Jed) { if (ctx === undefined) { ctx = 'yohours'; } t = i18n.translate(s).withContext(ctx).fetch(); } return t; }); /** * Converts the day code from a sunday-starting week into the day code of a monday-starting week * @param {int|string} d The sunday-starting week day (as string) * @return {int} The monday-starting week day */ function swDayToMwDay(d) { let day = parseInt(d); return (day === 0) ? 6 : day - 1; } /** * Compares to array for equality * @param {Array} array * @return {boolean} */ window.Array.prototype.equals = function (array) { // if the other array is a falsy value, return if (!array) { return false; } // compare lengths - can save a lot of time if (this.length !== array.length) { return false; } let i = 0, l = this.length; for (; i < l; i++) { // Check if we have nested arrays if (this[i] instanceof Array && array[i] instanceof Array) { // recurse into the nested arrays if (!this[i].equals(array[i])) { return false; } } else if (this[i] !== array[i]) { // Warning - two different object instances will never be equal: {x:20} != {x:20} return false; } } return true; }; /** * Does the array contains the given object * @param {*} obj The object to look for * @return {boolean} True if obj in array */ window.Array.prototype.contains = function (obj) { let i = this.length; while (i--) { if (this[i] === obj) { return true; } } return false; }; YoHours = $.extend(YoHours, { counter: 0, DAYS: { MONDAY: 0, TUESDAY: 1, WEDNESDAY: 2, THURSDAY: 3, FRIDAY: 4, SATURDAY: 5, SUNDAY: 6 }, OSM_DAYS: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], IRL_DAYS: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], OSM_MONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], IRL_MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MONTH_END_DAY: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], MINUTES_MAX: 1440, DAYS_MAX: 6, PH_WEEKDAY: -2, RGX_RULE_MODIFIER: /^(open|closed|off)$/i, RGX_WEEK_KEY: /^week$/, RGX_WEEK_VAL: /^([01234]?[0-9]|5[0123])(-([01234]?[0-9]|5[0123]))?(,([01234]?[0-9]|5[0123])(-([01234]?[0-9]|5[0123]))?)*:?$/, RGX_MONTH: /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)(-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec))?:?$/, RGX_MONTHDAY: /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([012]?[0-9]|3[01])(-((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) )?([012]?[0-9]|3[01]))?:?$/, RGX_TIME: /^((([01]?[0-9]|2[01234]):[012345][0-9](-([01]?[0-9]|2[01234]):[012345][0-9])?(,([01]?[0-9]|2[01234]):[012345][0-9](-([01]?[0-9]|2[01234]):[012345][0-9])?)*)|(24\/7))$/, RGX_WEEKDAY: /^(((Mo|Tu|We|Th|Fr|Sa|Su)(-(Mo|Tu|We|Th|Fr|Sa|Su))?)|(PH|SH|easter))(,(((Mo|Tu|We|Th|Fr|Sa|Su)(-(Mo|Tu|We|Th|Fr|Sa|Su))?)|(PH|SH|easter)))*$/, RGX_HOLIDAY: /^(PH|SH|easter)$/, RGX_WD: /^(Mo|Tu|We|Th|Fr|Sa|Su)(-(Mo|Tu|We|Th|Fr|Sa|Su))?$/ }); /** * Class Interval, defines an interval in a week where the POI is open. * @param {int} dayStart The start week day (use DAYS constants) * @param {int|void} dayEnd The end week day (use DAYS constants) * @param {int|void} minStart The interval start (in minutes since midnight) * @param {int|void} minEnd The interval end (in minutes since midnight) * @constructor */ YoHours.Interval = function (dayStart, dayEnd, minStart, minEnd) { /** The start day in the week, see DAYS **/ this._dayStart = dayStart; /** The end day in the week, see DAYS **/ this._dayEnd = dayEnd; /** The interval start, in minutes since midnight (local hour) **/ this._start = minStart; /** The interval end, in minutes since midnight (local hour) **/ this._end = minEnd; if (!this._dayEnd && !this._end) { this._dayEnd = YoHours.DAYS_MAX; this._end = YoHours.MINUTES_MAX; } }; /** * @return {int} The start day in the week, see DAYS constants */ YoHours.Interval.prototype.getStartDay = function () { return parseInt(this._dayStart); }; /** * @return {int} The end day in the week, see DAYS constants */ YoHours.Interval.prototype.getEndDay = function () { return parseInt(this._dayEnd); }; /** * @return {int} The interval start, in minutes since midnight */ YoHours.Interval.prototype.getFrom = function () { return parseInt(this._start); }; /** * @return {int} The interval end, in minutes since midnight */ YoHours.Interval.prototype.getTo = function () { return parseInt(this._end); }; /** * A wide interval is an interval of one or more days, weeks, months, holidays. * Use WideInterval.days/weeks/months/holidays methods to construct one object. * @constructor */ YoHours.WideInterval = function () { //ATTRIBUTES /** The start of the interval **/ this._start = null; /** The end of the interval **/ this._end = null; /** The kind of interval **/ this._type = null; }; /** * @param {int} startDay * @param {int} startMonth, * @param {int|void} endDay * @param {int|void} endMonth * @return {YoHours.WideInterval} a day-based interval */ YoHours.WideInterval.prototype.day = function (startDay, startMonth, endDay, endMonth) { if (startDay === undefined || startMonth === undefined) { throw Error(i18n.translate('Start day and month can\'t be null').withContext('yohours/notifications').fetch()); } this._start = {day: startDay, month: startMonth}; this._end = (endDay && endMonth && (endDay !== startDay || endMonth !== startMonth)) ? { day: endDay, month: endMonth } : null; this._type = 'day'; return this; }; /** * @param {int} startWeek * @param {int|void} endWeek * @return {YoHours.WideInterval} a week-based interval */ YoHours.WideInterval.prototype.week = function (startWeek, endWeek) { if (startWeek === undefined) { throw Error(i18n.translate('Start week can\'t be null').withContext('yohours/notifications').fetch()); } this._start = {week: startWeek}; this._end = (endWeek && endWeek !== startWeek) ? {week: endWeek} : null; this._type = 'week'; return this; }; /** * @param {int} startMonth * @param {int|void} endMonth * @return {YoHours.WideInterval} a month-based interval */ YoHours.WideInterval.prototype.month = function (startMonth, endMonth) { if (startMonth === undefined) { throw Error(i18n.translate('Start month can\'t be null').withContext('yohours/notifications').fetch()); } this._start = {month: startMonth}; this._end = (endMonth && endMonth !== startMonth) ? {month: endMonth} : null; this._type = 'month'; return this; }; /** * @param {string} holiday * @return {YoHours.WideInterval} a holiday-based interval */ YoHours.WideInterval.prototype.holiday = function (holiday) { if (holiday === undefined || (holiday !== 'PH' && holiday !== 'SH' && holiday !== 'easter')) { throw Error(i18n.translate('Invalid holiday, must be PH, SH or easter').withContext('yohours/notifications').fetch()); } this._start = {holiday: holiday}; this._end = null; this._type = 'holiday'; return this; }; /** * @return {YoHours.WideInterval} a holiday-based interval */ YoHours.WideInterval.prototype.always = function () { this._start = null; this._end = null; this._type = 'always'; return this; }; /** * @return {string|null} The kind of wide interval (always, day, month, week, holiday) */ YoHours.WideInterval.prototype.getType = function () { return this._type; }; /** * @return {Object} The start moment */ YoHours.WideInterval.prototype.getStart = function () { return this._start; }; /** * @return {Object} end moment */ YoHours.WideInterval.prototype.getEnd = function () { return this._end; }; /** * @return {boolean} if the given object concerns the same interval as this one */ YoHours.WideInterval.prototype.equals = function (o) { if (!(o instanceof YoHours.WideInterval)) { return false; } if (this === o) { return true; } if (o._type === 'always') { return this._type === 'always'; } let result = false; switch (this._type) { case 'always': result = o._start === null; break; case 'day': result = (o._type === 'day' && o._start.month === this._start.month && o._start.day === this._start.day && ((o._end === null && this._end === null) || (o._end !== null && this._end !== null && this._end.month === o._end.month && this._end.day === o._end.day))) || (o._type === 'month' && o._start.month === this._start.month && (this.isFullMonth() && o.isFullMonth()) || (o._end !== null && this._end !== null && this._end.month === o._end.month && this.endsMonth() && o.endsMonth())); break; case 'week': result = o._start.week === this._start.week && (o._end === this._end || (this._end !== null && o._end !== null && o._end.week === this._end.week)); break; case 'month': result = (o._type === 'day' && this._start.month === o._start.month && o.startsMonth() && ((this._end === null && o._end !== null && this._start.month === o._end.month && o.endsMonth()) || (this._end !== null && o._end !== null && this._end.month === o._end.month && o.endsMonth()))) || (o._type === 'month' && o._start.month === this._start.month && ((this._end === null && o._end === null) || (this._end !== null && o._end !== null && this._end.month === o._end.month))); break; case 'holiday': result = o._start.holiday === this._start.holiday; break; default: } return result; }; /** * @return {string} The human readable time */ YoHours.WideInterval.prototype.getTimeForHumans = function () { let result; switch (this._type) { case 'day': if (this._end != null) { result = i18n.translate('every week from %s %u to %s %u') .withContext('yohours/timespan') .fetch(i18n.translate(YoHours.IRL_MONTHS[this._start.month - 1]).withContext('yohours').fetch(), this._start.day, (this._start.month !== this._end.month) ? i18n.translate(YoHours.IRL_MONTHS[this._end.month - 1]).withContext('yohours').fetch() : '', this._end.day); } else { result = i18n.translate('day %s %u').withContext('yohours/timespan') .fetch(i18n.translate(YoHours.IRL_MONTHS[this._start.month - 1]).withContext('yohours').fetch(), this._start.day); } break; case 'week': if (this._end != null) { result = i18n.translate('every week from week %u to %u').withContext('yohours/timespan').fetch(this._start.week, this._end.week); } else { result = i18n.translate('week %u').withContext('yohours/timespan').fetch(this._start.week); } break; case 'month': if (this._end != null) { result = i18n.translate('every week from %s to %s').withContext('yohours/timespan') .fetch(i18n.translate(YoHours.IRL_MONTHS[this._start.month - 1]).withContext('yohours').fetch(), i18n.translate(YoHours.IRL_MONTHS[this._end.month - 1]).withContext('yohours').fetch()); } else { result = 'every week in ' + YoHours.IRL_MONTHS[this._start.month - 1]; result = i18n.translate('every week in %s').withContext('yohours/timespan').fetch(i18n.translate(YoHours.IRL_MONTHS[this._start.month - 1]).withContext('yohours').fetch()); } break; case 'holiday': if (this._start.holiday === 'SH') { result = i18n.translate('every week during school holidays').withContext('yohours/timespan').fetch(); } else if (this._start.holiday === 'PH') { result = i18n.translate('every public holidays').withContext('yohours/timespan').fetch(); } else if (this._start.holiday === 'easter') { result = i18n.translate('each easter day').withContext('yohours/timespan').fetch(); } else { throw new Error(i18n.translate('Invalid holiday type: %s').withContext('yohours/notifications').fetch(this._start.holiday)); } break; case 'always': result = i18n.translate('every week of year').withContext('yohours/timespan').fetch(); break; default: result = i18n.translate('invalid time').withContext('yohours/timespan').fetch(); } return result; }; /** * @return {string} The time selector for OSM opening_hours */ YoHours.WideInterval.prototype.getTimeSelector = function () { let result; switch (this._type) { case 'day': result = YoHours.OSM_MONTHS[this._start.month - 1] + ' ' + ((this._start.day < 10) ? '0' : '') + this._start.day; if (this._end != null) { //Same month as start ? if (this._start.month === this._end.month) { result += '-' + ((this._end.day < 10) ? '0' : '') + this._end.day; } else { result += '-' + YoHours.OSM_MONTHS[this._end.month - 1] + ' ' + ((this._end.day < 10) ? '0' : '') + this._end.day; } } break; case 'week': result = 'week ' + ((this._start.week < 10) ? '0' : '') + this._start.week; if (this._end != null) { result += '-' + ((this._end.week < 10) ? '0' : '') + this._end.week; } break; case 'month': result = YoHours.OSM_MONTHS[this._start.month - 1]; if (this._end != null) { result += '-' + YoHours.OSM_MONTHS[this._end.month - 1]; } break; case 'holiday': result = this._start.holiday; break; case 'always': default: result = ''; } return result; }; /** * Does this interval corresponds to a full month ? * @return {boolean} */ YoHours.WideInterval.prototype.isFullMonth = function () { if (this._type === 'month' && this._end === null) { return true; } else if (this._type === 'day') { return (this._start.day === 1 && this._end !== null && this._end.month === this._start.month && this._end.day !== undefined && this._end.day === YoHours.MONTH_END_DAY[this._end.month - 1]); } else { return false; } }; /** * Does this interval starts the first day of a month * @return {boolean} */ YoHours.WideInterval.prototype.startsMonth = function () { return this._type === 'month' || this._type === 'always' || (this._type === 'day' && this._start.day === 1); }; /** * Does this interval ends the last day of a month * @return {boolean} */ YoHours.WideInterval.prototype.endsMonth = function () { return this._type === 'month' || this._type === 'always' || (this._type === 'day' && this._end !== null && this._end.day === YoHours.MONTH_END_DAY[this._end.month - 1]); }; /** * Does this interval strictly contains the given one (ie the second is a refinement of the first, and not strictly equal) * @param {Object} o The other wide interval * @return {boolean} True if this date contains the given one (and is not strictly equal to) */ YoHours.WideInterval.prototype.contains = function (o) { let result = false; /* * Check if it is contained in this one */ if (this.equals(o)) { result = false; } else if (this._type === 'always') { result = true; } else if (this._type === 'day') { if (o._type === 'day') { //Starting after if (o._start.month > this._start.month || (o._start.month === this._start.month && o._start.day >= this._start.day)) { //Ending before if (o._end != null) { if (this._end != null && (o._end.month < this._end.month || (o._end.month === this._end.month && o._end.day <= this._end.day))) { result = true; } } else { if (this._end != null && (o._start.month < this._end.month || (o._start.month === this._end.month && o._start.day <= this._end.day))) { result = true; } } } } else if (o._type === 'month') { //Starting after if (o._start.month > this._start.month || (o._start.month === this._start.month && this._start.day === 1)) { //Ending before if (o._end != null && this._end != null && (o._end.month < this._end.month || (o._end.month === this._end.month && this._end.day === YoHours.MONTH_END_DAY[end.month - 1]))) { result = true; } else if (o._end === null && (this._end != null && o._start.month < this._end.month)) { result = true; } } } } else if (this._type === 'week') { if (o._type === 'week') { if (o._start.week >= this._start.week) { if (o._end != null && this._end != null && o._end.week <= this._end.week) { result = true; } else if (o._end == null && ((this._end != null && o._start.week <= this._end.week) || o._start.week === this._start.week)) { result = true; } } } } else if (this._type === 'month') { if (o._type === 'month') { if (o._start.month >= this._start.month) { if (o._end != null && this._end != null && o._end.month <= this._end.month) { result = true; } else if (o._end == null && ((this._end != null && o._start.month <= this._end.month) || o._start.month === this._start.month)) { result = true; } } } else if (o._type === 'day') { if (o._end != null) { if (this._end == null) { if (o._start.month === this._start.month && o._end.month === this._start.month && ((o._start.day >= 1 && o._end.day < YoHours.MONTH_END_DAY[o._start.month - 1]) || (o._start.day > 1 && o._end.day <= YoHours.MONTH_END_DAY[o._start.month - 1]))) { result = true; } } else { if (o._start.month >= this._start.month && o._end.month <= this._end.month) { if ((o._start.month > this._start.month && o._end.month < this._end.month) || (o._start.month === this._start.month && o._end.month < this._end.month && o._start.day > 1) || (o._start.month > this._start.month && o._end.month === this._end.month && o._end.day < YoHours.MONTH_END_DAY[o._end.month - 1]) || (o._start.day >= 1 && o._end.day < YoHours.MONTH_END_DAY[o._end.month - 1]) || (o._start.day > 1 && o._end.day <= YoHours.MONTH_END_DAY[o._end.month - 1])) { result = true; } } } } else { if (this._end == null) { if (this._start.month === o._start.month) { result = true; } } else { if (o._start.month >= this._start.month && o._start.month <= this._end.month) { result = true; } } } } } return result; }; /** * Class Day, represents a typical day * @constructor */ YoHours.Day = function () { /** The intervals defining this week **/ this._intervals = []; /** The next interval ID **/ this._nextInterval = 0; }; /** * @return {Array} This day, as a boolean array (minutes since midnight). True if open, false else. */ YoHours.Day.prototype.getAsMinutesArray = function () { //Create array with all values set to false let minute; //For each minute let minuteArray = []; for (minute = 0; minute <= YoHours.MINUTES_MAX; minute++) { minuteArray[minute] = false; } //Set to true values where an interval is defined let id = 0, l = this._intervals.length; for (; id < l; id++) { if (this._intervals[id] !== undefined) { let startMinute = null; let endMinute = null; if (this._intervals[id].getStartDay() === this._intervals[id].getEndDay() || (this._intervals[id].getEndDay() === YoHours.DAYS_MAX && this._intervals[id].getTo() === YoHours.MINUTES_MAX)) { //Define start and end minute regarding the current day startMinute = this._intervals[id].getFrom(); endMinute = this._intervals[id].getTo(); } //Set to true the minutes for this day if (startMinute !== null && endMinute !== null) { for (minute = startMinute; minute <= endMinute; minute++) { minuteArray[minute] = true; } } else { console.log(this._intervals[id].getFrom() + ' ' + this._intervals[id].getTo() + ' ' + this._intervals[id].getStartDay() + ' ' + this._intervals[id].getEndDay()); throw new Error(i18n.translate('Invalid interval').withContext('yohours/notifications').fetch()); } } } return minuteArray; }; /** * @param {boolean|void} clean Clean intervals ? (default: false) * @return {Array} The intervals in this week */ YoHours.Day.prototype.getIntervals = function (clean) { clean = clean || false; if (clean) { //Create continuous intervals over days let minuteArray = this.getAsMinutesArray(); let intervals = []; let minStart = -1; let min = 0, lm = minuteArray.length; for (; min < lm; min++) { //First minute if (min === 0) { if (minuteArray[min]) { minStart = min; } } //Last minute else if (min === lm - 1) { if (minuteArray[min]) { intervals.push(new YoHours.Interval(0, 0, minStart, min)); } } //Other minutes else { //New interval if (minuteArray[min] && minStart < 0) { minStart = min; } else if (!minuteArray[min] && minStart >= 0) { intervals.push(new YoHours.Interval(0, 0, minStart, min - 1)); minStart = -1; } } } return intervals; } else { return this._intervals; } }; /** * Add a new interval to this week * @param interval The new interval * @return {int} The ID of the added interval */ YoHours.Day.prototype.addInterval = function (interval) { this._intervals[this._nextInterval] = interval; this._nextInterval++; return this._nextInterval - 1; }; /** * Edits the given interval * @param {int} id The interval ID * @param interval The new interval */ YoHours.Day.prototype.editInterval = function (id, interval) { this._intervals[id] = interval; }; /** * Remove the given interval * @param {int} id the interval ID */ YoHours.Day.prototype.removeInterval = function (id) { this._intervals[id] = undefined; delete this._intervals[id]; }; /** * Redefines this date range intervals with a copy of the given ones * @param {YoHours.Interval[]} intervals */ YoHours.Day.prototype.copyIntervals = function (intervals) { this._intervals = []; for (let i = 0; i < intervals.length; i++) { if (intervals[i] !== undefined && intervals[i].getStartDay() === 0 && intervals[i].getEndDay() === 0) { this._intervals.push($.extend(true, {}, intervals[i])); } } this._intervals = this.getIntervals(true); }; /** * Removes all defined intervals */ YoHours.Day.prototype.clearIntervals = function () { this._intervals = []; }; /** * Is this day defining the same intervals as the given one ? * @param {YoHours.Day} d */ YoHours.Day.prototype.sameAs = function (d) { return d.getAsMinutesArray().equals(this.getAsMinutesArray()); }; /** * Class Week, represents a typical week of opening hours. * @constructor */ YoHours.Week = function () { /** The intervals defining this week **/ this._intervals = []; }; /** * @return {Array} This week, as a two-dimensional boolean array. First dimension is for days (see DAYS), second dimension for minutes since midnight. True if open, false else. */ YoHours.Week.prototype.getAsMinutesArray = function () { let minute; let day; let minuteArray = []; for (day = 0; day <= YoHours.DAYS_MAX; day++) { //For each minute minuteArray[day] = []; for (minute = 0; minute <= YoHours.MINUTES_MAX; minute++) { minuteArray[day][minute] = false; } } //Set to true values where an interval is defined let id = 0, l = this._intervals.length; for (; id < l; id++) { if (this._intervals[id] !== undefined) { for (day = this._intervals[id].getStartDay(); day <= this._intervals[id].getEndDay(); day++) { //Define start and end minute regarding the current day let startMinute = (day === this._intervals[id].getStartDay()) ? this._intervals[id].getFrom() : 0; let endMinute = (day === this._intervals[id].getEndDay()) ? this._intervals[id].getTo() : YoHours.MINUTES_MAX; //Set to true the minutes for this day if (startMinute != null && endMinute != null) { for (minute = startMinute; minute <= endMinute; minute++) { minuteArray[day][minute] = true; } } } } } return minuteArray; }; /** * @param {boolean|void} clean Clean intervals ? (default: false) * @return {Array} The intervals in this week */ YoHours.Week.prototype.getIntervals = function (clean) { clean = clean || false; if (clean) { //Create continuous intervals over days let minuteArray = this.getAsMinutesArray(); let intervals = []; let dayStart = -1, minStart = -1; let day = 0, l = minuteArray.length; for (; day < l; day++) { let min = 0, lm = minuteArray[day].length; for (; min < lm; min++) { //First minute of monday if (day === 0 && min === 0) { if (minuteArray[day][min]) { dayStart = day; minStart = min; } } else if (day === YoHours.DAYS_MAX && min === lm - 1) { if (dayStart >= 0 && minuteArray[day][min]) { intervals.push(new YoHours.Interval(dayStart, day, minStart, min)); } } else { //New interval if (minuteArray[day][min] && dayStart < 0) { dayStart = day; minStart = min; } else if (!minuteArray[day][min] && dayStart >= 0) { if (min === 0) { intervals.push(new YoHours.Interval(dayStart, day - 1, minStart, YoHours.MINUTES_MAX)); } else { intervals.push(new YoHours.Interval(dayStart, day, minStart, min - 1)); } dayStart = -1; minStart = -1; } } } } return intervals; } else { return this._intervals; } }; /** * Returns the intervals which are different from those defined in the given week * @param {YoHours.Week} w The general week * @return {Object} The intervals which are different, as object { open: [ Intervals ], closed: [ Intervals ] } */ YoHours.Week.prototype.getIntervalsDiff = function (w) { //Get minutes arrays let myMinArray = this.getAsMinutesArray(); let wMinArray = w.getAsMinutesArray(); //Create diff array let intervals = {open: [], closed: []}; let dayStart = -1, minStart = -1; let diffDay, m, intervalsLength; for (let d = 0; d <= YoHours.DAYS_MAX; d++) { diffDay = false; m = 0; intervalsLength = intervals.open.length; while (m <= YoHours.MINUTES_MAX) { //Copy entire day if (diffDay) { //First minute of monday if (d === 0 && m === 0) { if (myMinArray[d][m]) { dayStart = d; minStart = m; } } else if (d === YoHours.DAYS_MAX && m === YoHours.MINUTES_MAX) { if (dayStart >= 0 && myMinArray[d][m]) { intervals.open.push(new YoHours.Interval(dayStart, d, minStart, m)); } } else { //New interval if (myMinArray[d][m] && dayStart < 0) { dayStart = d; minStart = m; } else if (!myMinArray[d][m] && dayStart >= 0) { if (m === 0) { intervals.open.push(new YoHours.Interval(dayStart, d - 1, minStart, YoHours.MINUTES_MAX)); } else { intervals.open.push(new YoHours.Interval(dayStart, d, minStart, m - 1)); } dayStart = -1; minStart = -1; } } m++; } else { diffDay = myMinArray[d][m] ? !wMinArray[d][m] : wMinArray[d][m]; //If there is a difference, start to copy full day if (diffDay) { m = 0; } //Else, continue checking else { m++; } } } //Close intervals if day is identical if (!diffDay && dayStart > -1) { intervals.open.push(new YoHours.Interval(dayStart, d - 1, minStart, YoHours.MINUTES_MAX)); dayStart = -1; minStart = -1; } //Create closed intervals if closed all day if (diffDay && dayStart === -1 && intervalsLength === intervals.open.length) { //Merge with previous interval if possible if (intervals.closed.length > 0 && intervals.closed[intervals.closed.length - 1].getEndDay() === d - 1) { intervals.closed[intervals.closed.length - 1] = new YoHours.Interval(intervals.closed[intervals.closed.length - 1].getStartDay(), d, 0, YoHours.MINUTES_MAX); } else { intervals.closed.push(new YoHours.Interval(d, d, 0, YoHours.MINUTES_MAX)); } } } return intervals; }; /** * Add a new interval to this week * @param {YoHours.Interval} interval The new interval * @return {int} The ID of the added interval */ YoHours.Week.prototype.addInterval = function (interval) { this._intervals[this._intervals.length] = interval; return this._intervals.length - 1; }; /** * Edits the given interval * @param {string|int} id The interval ID * @param {YoHours.Interval} interval The new interval */ YoHours.Week.prototype.editInterval = function (id, interval) { this._intervals[id] = interval; }; /** * Remove the given interval * @param {string|int} id the interval ID */ YoHours.Week.prototype.removeInterval = function (id) { this._intervals[id] = undefined; delete this._intervals[id]; }; /** * Removes all intervals during a given day */ YoHours.Week.prototype.removeIntervalsDuringDay = function (day) { let interval, itLength = this._intervals.length, dayDiff; for (let i = 0; i < itLength; i++) { interval = this._intervals[i]; if (interval !== undefined) { //If interval over given day if (interval.getStartDay() <= day && interval.getEndDay() >= day) { dayDiff = interval.getEndDay() - interval.getStartDay(); //Avoid deletion if over night interval if (dayDiff > 1 || dayDiff === 0 || interval.getStartDay() === day || interval.getFrom() <= interval.getTo()) { //Create new interval if several day if (interval.getEndDay() - interval.getStartDay() >= 1 && interval.getFrom() <= interval.getTo()) { if (interval.getStartDay() < day) { this.addInterval(new YoHours.Interval(interval.getStartDay(), day - 1, interval.getFrom(), 24 * 60)); } if (interval.getEndDay() > day) { this.addInterval(new YoHours.Interval(day + 1, interval.getEndDay(), 0, interval.getTo())); } } //Delete this.removeInterval(i); } } } } }; /** * Redefines this date range intervals with a copy of the given ones * @param {YoHours.Interval[]} intervals */ YoHours.Week.prototype.copyIntervals = function (intervals) { this._intervals = []; for (let i = 0; i < intervals.length; i++) { if (intervals[i] !== undefined) { this._intervals.push($.extend(true, {}, intervals[i])); } } }; /** * Is this week defining the same intervals as the given one ? * @param {YoHours.Week} w * @return {boolean} */ YoHours.Week.prototype.sameAs = function (w) { return w.getAsMinutesArray().equals(this.getAsMinutesArray()); }; /** * Class DateRange, defines a range of months, weeks or days. * A typical week or day will be associated. * @param {YoHours.WideInterval} w * @constructor */ YoHours.DateRange = function (w) { /** The wide interval of this date range **/ this._wideInterval = null; /** The typical week or day associated **/ this._typical = undefined; this.updateRange(w); }; /** * Is this interval defining a typical day ? * @return {boolean} */ YoHours.DateRange.prototype.definesTypicalDay = function () { return this._typical instanceof YoHours.Day; }; /** * Is this interval defining a typical week ? * @return {boolean} */ YoHours.DateRange.prototype.definesTypicalWeek = function () { return this._typical instanceof YoHours.Week; }; /** * @return {YoHours.Day|YoHours.Week|null} The typical day or week */ YoHours.DateRange.prototype.getTypical = function () { return this._typical; }; /** * @return {YoHours.WideInterval} The wide interval this date range concerns */ YoHours.DateRange.prototype.getInterval = function () { return this._wideInterval; }; /** * Changes the date range */ YoHours.DateRange.prototype.updateRange = function (wide) { this._wideInterval = (wide != null) ? wide : new YoHours.WideInterval().always(); //Create typical week/day if (this._typical === undefined) { switch (this._wideInterval.getType()) { case 'day': if (this._wideInterval.getEnd() === null) { this._typical = new YoHours.Day(); } else { this._typical = new YoHours.Week(); } break; case 'week': this._typical = new YoHours.Week(); break; case 'month': this._typical = new YoHours.Week(); break; case 'holiday': if (this._wideInterval.getStart().holiday === 'SH') { this._typical = new YoHours.Week(); } else { this._typical = new YoHours.Day(); } break; case 'always': this._typical = new YoHours.Week(); break; default: throw Error('Invalid interval type: ' + this._wideInterval.getType()); } } }; /** * Check if the typical day/week of this date range is the same as in the given date range * @param {YoHours.DateRange} dr The other DateRange * @return {boolean} if same typical day/week */ YoHours.DateRange.prototype.hasSameTypical = function (dr) { return this.definesTypicalDay() === dr.definesTypicalDay() && this._typical.sameAs(dr.getTypical()); }; /** * Does this date range contains the given date range (ie the second is a refinement of the first) * @param {YoHours.DateRange} dr The other DateRange * @return {boolean} if this date contains the given one (and is not strictly equal to) */ YoHours.DateRange.prototype.isGeneralFor = function (dr) { return dr.definesTypicalDay() === this.definesTypicalDay() && this._wideInterval.contains(dr.getInterval()); }; /** * An opening_hours time, such as "08:00" or "08:00-10:00" or "off" (if no start and end) * @param {int|void|string} start The start minute (from midnight), can be null * @param {int|void|string} end The end minute (from midnight), can be null * @constructor */ YoHours.OhTime = function (start, end) { /** The start minute **/ this._start = (start >= 0) ? start : null; /** The end minute **/ this._end = (end >= 0 && end !== start) ? end : null; }; /** * @return {string} The time in opening_hours format */ YoHours.OhTime.prototype.get = function () { if (this._start === null && this._end === null) { return 'off'; } else { return this._timeString(this._start) + ((this._end == null) ? '' : '-' + this._timeString(this._end)); } }; /** * @return {string|null} The start minutes */ YoHours.OhTime.prototype.getStart = function () { return this._start === null ? null : this._start.toString(); }; /** * @return {string|null} The end minutes */ YoHours.OhTime.prototype.getEnd = function () { return this._end === null ? null : this._end.toString(); }; /** * @param {YoHours.OhTime} t * @return {boolean} True if same time */ YoHours.OhTime.prototype.equals = function (t) { return this.getStart() === t.getStart() && this.getEnd() === t.getEnd(); }; /** * @return {string} The hour in HH:MM format */ YoHours.OhTime.prototype._timeString = function (minutes) { let h = Math.floor(minutes / 60); let period = ''; let m = minutes % 60; return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + period; }; /** * An opening_hours date, such as "Apr 21", "week 1-15 Mo,Tu", "Apr-Dec Mo-Fr", "SH Su", ... * @param {string} w The wide selector, as string * @param {string} wt The wide selector type (month, week, day, holiday, always) * @param {int[]} wd The weekdays, as integer array (0 to 6 = Monday to Sunday, -1 = single day date, -2 = PH) * @constructor */ YoHours.OhDate = function (w, wt, wd) { /** Kind of wide date (month, week, day, holiday, always) **/ this._wideType = wt; /** Wide date **/ this._wide = w; /** Weekdays + PH **/ this._weekdays = wd.sort(); /** Overwritten days (to allow create simpler rules) **/ this._wdOver = []; if (w === null || wt === null || wd === null) { throw Error(i18n.translate('Missing parameter').withContext('yohours/notifications').fetch()); } }; /** * @return {string} The wide type */ YoHours.OhDate.prototype.getWideType = function () { return this._wideType.toString(); }; /** * @return {string} The month day, month, week, SH (depends of type) */ YoHours.OhDate.prototype.getWideValue = function () { return this._wide.toString(); }; /** * @return {int[]} The weekdays array */ YoHours.OhDate.prototype.getWd = function () { return this._weekdays; }; /** * @return {int[]} The overwritten weekdays array */ YoHours.OhDate.prototype.getWdOver = function () { return this._wdOver; }; /** * @param {Array} a The other weekdays array * @return {boolean} True if same weekdays as other object */ YoHours.OhDate.prototype.sameWd = function (a) { return a.equals(this._weekdays); }; /** * @return {string} The weekdays in opening_hours syntax */ YoHours.OhDate.prototype.getWeekdays = function () { let i; let result = ''; let wd = this._weekdays.concat(this._wdOver).sort(); //PH as weekday if (wd.length > 0 && parseInt(wd[0]) === YoHours.PH_WEEKDAY) { result = 'PH'; wd.shift(); } //Check if we should create a continuous interval for week-end if (wd.length > 0 && wd.contains(6) && wd.contains(0) && (wd.contains(5) || wd.contains(1))) { //Find when the week-end starts let startWE = 6; i = wd.length - 2; let stopLooking = false; while (!stopLooking && i >= 0) { if (wd[i] === wd[i + 1] - 1) { startWE = wd[i]; i--; } else { stopLooking = true; } } //Find when it stops i = 1; stopLooking = false; let endWE = 0; while (!stopLooking && i < wd.length) { if (wd[i - 1] === wd[i] - 1) { endWE = wd[i]; i++; } else { stopLooking = true; } } //If long enough, add it as first weekday interval let length = 7 - startWE + endWE + 1; if (length >= 3 && startWE > endWE) { if (result.length > 0) { result += ','; } result += YoHours.OSM_DAYS[startWE] + '-' + YoHours.OSM_DAYS[endWE]; //Remove processed days let j = 0; while (j < wd.length) { if (wd[j] <= endWE || wd[j] >= startWE) { wd.splice(j, 1); } else { j++; } } } } //Process only if not empty weekday list if (wd.length > 1 || (wd.length === 1 && wd[0] !== -1)) { result += (result.length > 0) ? ',' + YoHours.OSM_DAYS[wd[0]] : YoHours.OSM_DAYS[wd[0]]; let firstInRow = wd[0]; for (i = 1; i < wd.length; i++) { //When days aren't following if (wd[i - 1] !== wd[i] - 1) { //Previous day range length > 1 if (firstInRow !== wd[i - 1]) { //Two days if (wd[i - 1] - firstInRow === 1) { result += ',' + YoHours.OSM_DAYS[wd[i - 1]]; } else { result += '-' + YoHours.OSM_DAYS[wd[i - 1]]; } } //Add the current day result += ',' + YoHours.OSM_DAYS[wd[i]]; firstInRow = wd[i]; } else if (i === wd.length - 1) { if (wd[i] - firstInRow === 1) { result += ',' + YoHours.OSM_DAYS[wd[i]]; } else { result += '-' + YoHours.OSM_DAYS[wd[i]]; } } } } if (result === 'Mo-Su') { result = ''; } return result; }; /** * Is the given object of the same kind as this one * @param {YoHours.OhDate} d * @return {boolean} True if same weekdays and same wide type */ YoHours.OhDate.prototype.sameKindAs = function (d) { return this._wideType === d.getWideType() && d.sameWd(this._weekdays); }; /** * @param {Object} o * @return {boolean} True if this object is equal to the given one */ YoHours.OhDate.prototype.equals = function (o) { return o instanceof YoHours.OhDate && this._wideType === o.getWideType() && this._wide === o.getWideValue() && o.sameWd(this._weekdays); }; /** * Adds a new weekday in this date * @param {int} wd */ YoHours.OhDate.prototype.addWeekday = function (wd) { if (!this._weekdays.contains(wd) && !this._wdOver.contains(wd)) { this._weekdays.push(wd); this._weekdays = this._weekdays.sort(); } }; /** * Adds public holiday as a weekday of this date */ YoHours.OhDate.prototype.addPhWeekday = function () { this.addWeekday(YoHours.PH_WEEKDAY); }; /** * Adds an overwritten weekday, which can be included in this date and that will be overwritten in a following rule * @param {int} wd */ YoHours.OhDate.prototype.addOverwrittenWeekday = function (wd) { if (!this._wdOver.contains(wd) && !this._weekdays.contains(wd)) { this._wdOver.push(wd); this._wdOver = this._wdOver.sort(); } }; /** * An opening_hours rule, such as "Mo,Tu 08:00-18:00" * @constructor */ YoHours.OhRule = function () { /** The date selectors **/ this._date = []; /** The time selectors **/ this._time = []; }; /** * @return {YoHours.OhDate[]} The date selectors, as an array */ YoHours.OhRule.prototype.getDate = function () { return this._date; }; /** * @return {YoHours.OhTime[]} The time selectors, as an array */ YoHours.OhRule.prototype.getTime = function () { return this._time; }; /** * @return {string} The opening_hours value */ YoHours.OhRule.prototype.get = function () { let i; let l; let result = ''; //Create date part if (this._date.length > 1 || this._date[0].getWideValue() !== '') { //Add wide selectors i = 0; l = this._date.length; for (; i < l; i++) { if (i > 0) { result += ','; } result += this._date[i].getWideValue(); } } //Add weekdays if (this._date.length > 0) { let wd = this._date[0].getWeekdays(); if (wd.length > 0) { result += ' ' + wd; } } //Create time part if (this._time.length > 0) { result += ' '; i = 0; l = this._time.length; for (; i < l; i++) { if (i > 0) { result += ','; } result += this._time[i].get(); } } else { result += ' off'; } if (result.trim() === '00:00-24:00') { result = '24/7'; } return result.trim(); }; /** * @param {Object} o * @return {boolean} True if the given rule has the same time as this one */ YoHours.OhRule.prototype.sameTime = function (o) { if (o === undefined || o == null || o.getTime().length !== this._time.length) { return false; } else { let i = 0, l = this._time.length; for (; i < l; i++) { if (!this._time[i].equals(o.getTime()[i])) { return false; } } return true; } }; /** * Is this rule concerning off time ? * @return {boolean} */ YoHours.OhRule.prototype.isOff = function () { return this._time.length === 0 || (this._time.length === 1 && this._time[0].getStart() === null); }; /** * Does the rule have any overwritten weekday ? * @return {boolean} */ YoHours.OhRule.prototype.hasOverwrittenWeekday = function () { return this._date.length > 0 && this._date[0]._wdOver.length > 0; }; /** * Adds a weekday to all the dates * @param {int} wd */ YoHours.OhRule.prototype.addWeekday = function (wd) { for (let i = 0; i < this._date.length; i++) { this._date[i].addWeekday(wd); } }; /** * Adds public holidays as weekday to all dates */ YoHours.OhRule.prototype.addPhWeekday = function () { for (let i = 0; i < this._date.length; i++) { this._date[i].addPhWeekday(); } }; /** * Adds an overwritten weekday to all the dates * @param {int} wd */ YoHours.OhRule.prototype.addOverwrittenWeekday = function (wd) { for (let i = 0; i < this._date.length; i++) { this._date[i].addOverwrittenWeekday(wd); } }; /** * @param {YoHours.OhDate} d A new date selector */ YoHours.OhRule.prototype.addDate = function (d) { //Check param if (d === null || d === undefined || !(d instanceof YoHours.OhDate)) { throw Error(i18n.translate('Invalid parameter').withContext('yohours/notifications').fetch()); } //Check if date can be added if (this._date.length === 0 || (this._date[0].getWideType() !== 'always' && this._date[0].sameKindAs(d))) { this._date.push(d); } else { if (this._date.length !== 1 || this._date[0].getWideType() !== 'always' || !this._date[0].sameWd(d.getWd())) { throw Error(i18n.translate('This date can\'t be added to this rule').withContext('yohours/notifications').fetch()); } } }; /** * @param {YoHours.OhTime} t A new time selector */ YoHours.OhRule.prototype.addTime = function (t) { if ((this._time.length === 0 || this._time[0].get() !== 'off') && !this._time.contains(t)) { this._time.push(t); } else { throw Error(i18n.translate('This time can\'t be added to this rule').withContext('yohours/notifications').fetch()); } }; /** * Class OpeningHoursBuilder, creates opening_hours value from date range object * @constructor */ YoHours.OpeningHoursBuilder = function () { }; /** * Parses several date ranges to create an opening_hours string * @param {YoHours.DateRange[]} dateRanges The date ranges to parse * @return {string} The opening_hours string */ YoHours.OpeningHoursBuilder.prototype.build = function (dateRanges) { let rangeId; let rules = []; let dateRange, ohRules, ohRule, ohRuleAdded, ruleId, rangeGeneral, rangeGeneralFor; //Read each date range rangeId = 0; let l = dateRanges.length; for (; rangeId < l; rangeId++) { dateRange = dateRanges[rangeId]; if (dateRange !== undefined) { //Check if the defined typical week/day is not strictly equal to a previous wider rule rangeGeneral = null; rangeGeneralFor = null; let rangeGenId = rangeId - 1; while (rangeGenId >= 0 && rangeGeneral == null) { if (dateRanges[rangeGenId] !== undefined) { let generalFor = dateRanges[rangeGenId].isGeneralFor(dateRange); if (dateRanges[rangeGenId].hasSameTypical(dateRange) && (dateRanges[rangeGenId].getInterval().equals(dateRange.getInterval()) || generalFor)) { rangeGeneral = rangeGenId; } else if (generalFor && dateRanges[rangeGenId].definesTypicalWeek() && dateRange.definesTypicalWeek()) { rangeGeneralFor = rangeGenId; //Keep this ID to make differences in order to simplify result } } rangeGenId--; } if (rangeId === 0 || rangeGeneral == null) { //Get rules for this date range if (dateRange.definesTypicalWeek()) { if (rangeGeneralFor != null) { ohRules = this._buildWeekDiff(dateRange, dateRanges[rangeGeneralFor]); } else { ohRules = this._buildWeek(dateRange); } } else { ohRules = this._buildDay(dateRange); } //Process each rule let ohruleId = 0, orl = ohRules.length; for (; ohruleId < orl; ohruleId++) { ohRule = ohRules[ohruleId]; ohRuleAdded = false; ruleId = 0; //Try to add them to previously defined ones while (!ohRuleAdded && ruleId < rules.length) { //Identical one if (rules[ruleId].sameTime(ohRule)) { try { let dateId = 0, dl = ohRule.getDate().length; for (; dateId < dl; dateId++) { rules[ruleId].addDate(ohRule.getDate()[dateId]); } ohRuleAdded = true; } catch (e) { //But before, try to merge PH with always weekdays if (ohRule.getDate()[0].getWideType() === 'holiday' && ohRule.getDate()[0].getWideValue() === 'PH' && rules[ruleId].getDate()[0].getWideType() === 'always') { rules[ruleId].addPhWeekday(); ohRuleAdded = true; } else if (rules[ruleId].getDate()[0].getWideType() === 'holiday' && rules[ruleId].getDate()[0].getWideValue() === 'PH' && ohRule.getDate()[0].getWideType() === 'always') { ohRule.addPhWeekday(); rules[ruleId] = ohRule; ohRuleAdded = true; } else { ruleId++; } } } else { ruleId++; } } //If not, add as new rule if (!ohRuleAdded) { rules.push(ohRule); } //If some overwritten weekdays are still in last rule if (ohruleId === orl - 1 && ohRule.hasOverwrittenWeekday()) { let ohruleOWD = new YoHours.OhRule(); for (let ohruleDateId = 0; ohruleDateId < ohRule.getDate().length; ohruleDateId++) { ohruleOWD.addDate(new YoHours.OhDate(ohRule.getDate()[ohruleDateId].getWideValue(), ohRule.getDate()[ohruleDateId].getWideType(), ohRule.getDate()[ohruleDateId].getWdOver())); } ohruleOWD.addTime(new YoHours.OhTime()); ohRules.push(ohruleOWD); orl++; } } } } } //Create result string let result = ''; ruleId = 0; l = rules.length; for (; ruleId < l; ruleId++) { if (ruleId > 0) { result += '; '; } result += rules[ruleId].get(); } return result; }; /** * Creates rules for a given typical day * @param {YoHours.DateRange} dateRange The date range defining a typical day * @return {YoHours.OhRule[]} An array of OhRules */ YoHours.OpeningHoursBuilder.prototype._buildDay = function (dateRange) { let intervals = dateRange.getTypical().getIntervals(true); let interval; //Create rule let rule = new YoHours.OhRule(); let date = new YoHours.OhDate(dateRange.getInterval().getTimeSelector(), dateRange.getInterval().getType(), [-1]); rule.addDate(date); //Read time let i = 0, l = intervals.length; for (; i < l; i++) { interval = intervals[i]; if (interval !== undefined) { rule.addTime(new YoHours.OhTime(interval.getFrom(), interval.getTo())); } } return [rule]; }; /** * Create rules for a date range defining a typical week * Algorithm inspired by OpeningHoursEdit plugin for JOSM * @param {YoHours.DateRange} dateRange The date range defining a typical day * @return {YoHours.OhRule[]} An array of OhRules */ YoHours.OpeningHoursBuilder.prototype._buildWeek = function (dateRange) { let i; let result = []; let intervals = dateRange.getTypical().getIntervals(true); /* * Create time intervals per day */ let timeInterv