UNPKG

@publidata/utils-opening-hours

Version:

Collection of methods to handle opening hours

1,732 lines (1,547 loc) 52.3 kB
const moment = require("moment"); const OpeningHours = require("opening_hours"); const { getDate, getDaysBefore } = require("@publidata/utils-dates"); const nominatim = require("./nominatim"); const TODAY = new Date(); const MAX_ITERATION_DURATION = 24 * 60 * 60 * 1000; /* eslint-disable guard-for-in, no-nested-ternary, prettier/prettier */ const displayOpenings = (openingHours, shorten = false) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (cleanedOpeningHours === "24/7 off") return { string: { key: "unknown_schedules" }, open: false }; const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const state = oh.getState(); // we use current date const unknow = oh.getUnknown(); const comment = oh.getComment(); const nextchange = oh.getNextChange(); let output = []; if (unknow) { output.push({ key: "might_be_open" }); if (comment) { output.push({ key: "depends_on_+comment", params: { comment } }); } } else { oh.getState() ? output.push({ key: "open" }) : output.push({ key: "closed" }); if (comment) { output.push({ value: `, ${comment}` }); } } if (typeof nextchange === "undefined") { state ? output.push({ key: "but_will_never_close" }) : output.push({ key: "but_will_never_open" }); } else if (shorten) { const moment = moment(nextchange).format("HH[h]mm"); output.push({ key: "until", params: { moment } }); } else { state ? oh.getUnknown(nextchange) ? output.push({ key: "but_may_close" }) : output.push({ key: "but_will_close" }) : oh.getUnknown(nextchange) ? output.push({ key: "but_may_open" }) : output.push({ key: "but_will_open" }); output.push({ value: getDate(nextchange, true, "à") }); //utils-date } return { output, open: oh.getState() }; }; /* Display opening hours in a custom way * @param : openingHours object * @param : outputModel, object of type : * const outputModel = { open: { state: { display: true, string: "ouvert" }, next_change: { display: false, string: "", date: false, time: false } }, closed: { state: { display: true, string: "ouvrira" }, next_change: { display: true, undefined: "fermé définitivement", string: "", date: true, time: true } }, unknown: "Horaires non renseignés" }; * @return string, bool */ const displayCustomOpenings = (openingHours, outputModel) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (openingHours === "24/7 off") return { string: outputModel.unknown, open: false }; const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const state = oh.getState(); // we use current date const unknow = oh.getUnknown(); const comment = oh.getComment(); const nextchange = oh.getNextChange(); const { open, closed } = outputModel; const output = []; if (unknow) { output.push({ value: outputModel.unknown }); } else { if (oh.getState()) { if (open.state.display) output.push({ value: open.state.string }); } else { if (open.state.display) output.push({ value: closed.state.string }); } if (comment) output.push({ value: `, (${comment})` }); } if (typeof nextchange !== "undefined") { if (state && open.next_change.display) { output.push({ value: ` ${open.next_change.string}` }); open.next_change.date ? getDate(nextchange) === "aujourd'hui" ? output.push({ key: "today" }) : output.push({ key: "the_+date", params: { date: getDate(nextchange) } }) : output.push({ value: "" }); open.next_change.time ? output.push({ key: "at_+time", params: moment(nextchange).format("HH[h]mm") }) : output.push({ value: "" }); } else if (!state && closed.next_change.display) { output.push({ value: ` ${closed.next_change.string}` }); closed.next_change.date ? getDate(nextchange) === "aujourd'hui" ? output.push({ key: "today" }) : output.push({ key: "the_+date", params: { date: getDate(nextchange) } }) : output.push({ value: "" }); closed.next_change.time ? output.push({ key: "at_+time", params: { time: moment(nextchange).format("HH[h]mm") } }) : output.push({ value: "" }); } } else { output = state ? open.next_change.undefined : closed.next_change.undefined; } return { // string: output.charAt(0).toUpperCase() + output.slice(1), output, open: oh.getState() }; }; /* Get opening start hour and end hour * @param : openingHours object */ const getOpenIntervals = openingHours => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (openingHours === "24/7 off") return null; const from = new Date(); from.setHours("00", "00", "00"); const to = new Date(); to.setHours("23", "59", "59"); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); return [oh.getOpenIntervals(from, to)[0], oh.getOpenIntervals(from, to)[1]]; }; const OpeningHoursTable = { minMonths: [ "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" ], maxMonths: [ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december" ], minWeekdays: ["sun", "mon", "tue", "wed", "thu", "fri", "sat"], maxWeekdays: [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ], formatdate: (now, nextchange, from) => { const nowDaystart = new Date( now.getFullYear(), now.getMonth(), now.getDate() ); const nextdays = (nextchange.getTime() - nowDaystart.getTime()) / 1000 / 60 / 60 / 24; let timediff = []; let delta = Math.floor((nextchange.getTime() - now.getTime()) / 1000 / 60); // delta is minutes if (delta < 60) { timediff.push({ key: "+minutes_minutes", params: { count: delta } }); } const deltaminutes = delta % 60; delta = Math.floor(delta / 60); // delta is now hours if (delta < 24 && timediff === "") { timediff.push({ key: "in" }); timediff.push({ key: "+hours_hours", params: { count: delta, hours: delta } }); timediff.push({ key: "and" }); timediff.push({ key: "+minutes_minutes", params: { count: deltaminutes, minutes: OpeningHoursTable.pad(deltaminutes) } }); } const deltahours = delta % 24; delta = Math.floor(delta / 24); // delta is now days if (delta < 14 && timediff === "") { timediff.push({ key: "in" }); timediff.push({ key: "in_+days_days", params: { count: delta, days: delta } }); timediff.push({ key: "and" }); timediff.push({ key: "+hours_hours", params: { count: deltahours, hours: deltahours } }); } else if (timediff === "") { timediff.push({ key: "in" }); timediff.push({ key: "in_+days_days", params: { count: delta, days: delta } }); } let atday = []; if (from ? nextdays < 1 : nextdays <= 1) atday.push({ key: "today" }); else if (from ? nextdays < 2 : nextdays <= 2) atday.push({ key: "tomorrow" }); else if (from ? nextdays < 7 : nextdays <= 7) { atday.push({ key: "next_+day", params: { day: { key: OpeningHoursTable.maxWeekdays[nextchange.getDay()] } } }); } const monthName = OpeningHoursTable.maxMonths[nextchange.getMonth()]; const atdate = { key: "+month_+date", params: { date: nextchange.getDate(), month: { key: monthName } } }; const res = []; if (atday.length !== 0) res.push(atday); if (atday.length === 0 && atdate.length !== 0) res.push(atdate); if (timediff.length !== 0) res.push(timediff); return res; }, pad: n => (n < 10 ? `0${n}` : n), plural: (n, transBase) => (n > 1 ? `${transBase}s` : transBase), printDate: date => `${moment(date).locale("fr").format("DD/MM/YYYY")}`, printTime: date => moment(date).locale("fr").format("HH:mm"), drawTable: (it, currentDate, hasNextChange, firstDayOfWeek) => { const dateToday = new Date(currentDate); if ((dateToday.getDay() + 6) % 7 === 6) { dateToday.setDate(dateToday.getDate() - 7); } dateToday.setHours(0, 0, 0, 0); const date = new Date(dateToday); date.setDate(date.getDate() - (date.getDay() + !firstDayOfWeek)); // start at begin of the week const table = []; for (let row = 0; row < 7; row += 1) { date.setDate(date.getDate() + 1); it.setDate(date); let isOpen = it.getState(); let unknown = it.getUnknown(); let prevdate = date; let curdate = date; // const rowDate = new Date(date); table[row] = { display: { day: "", date: "" }, date: new Date(date), html: "", times: [], text: [] }; // Counter to check if the day is fully set up let filled = 0; while ( it.advance() && curdate.getTime() - date.getTime() < MAX_ITERATION_DURATION ) { curdate = it.getDate(); let fr = prevdate.getTime() - date.getTime(); let to = curdate.getTime() - date.getTime(); let text = []; let output = []; if (to > MAX_ITERATION_DURATION) to = MAX_ITERATION_DURATION; fr *= 100 / 1000 / 60 / 60 / 24; to *= 100 / 1000 / 60 / 60 / 24; // if from is not 23:59 if (fr !== (23 * 60 + 59) * 60 * 1000 * (100 / 1000 / 60 / 60 / 24)) { filled += to - fr; const html = `<div class="timebar ${ isOpen ? "open" : unknown ? "open" : "closed" }" style="width: calc(${to - fr}% - 2px)"></div>`; table[row].html += html; if (isOpen || unknown) { text.push({ key: "open_from_+time_to_+time2", params: { time: OpeningHoursTable.printTime(prevdate), time2: prevdate.getDay() !== curdate.getDay() ? "24:00" : OpeningHoursTable.printTime(curdate) } }); if ( prevdate.getDay() === curdate.getDay() && OpeningHoursTable.printTime(prevdate) === "00:00" && OpeningHoursTable.printTime(curdate) === "23:59" ) { output.push({ key: "all_day_long" }); } else { output.push({ key: "open" }); output.push({ key: "beginning_at" }); output.push({ value: OpeningHoursTable.printTime(prevdate) }); } table[row].text.push(text); } table[row].times.push({ isOpen, to: { value: curdate, percent: to }, from: { value: prevdate, percent: fr }, text, output, html, percent: to - fr }); } prevdate = curdate; isOpen = it.getState(); unknown = it.getUnknown(); } if (!hasNextChange) { isOpen = false; } if (!hasNextChange && table[row].text.length === 0) { // 24/7 const html = `<div class="timebar ${ isOpen ? "open" : unknown ? "open" : "closed" }" style="width:100%"></div>`; const text = isOpen ? ["Ouvert de 00:00 à 24:00"] : ["Fermé"]; table[row].times.push({ isOpen, to: { value: curdate, percent: 100 }, from: { value: prevdate, percent: 0 }, text, html, percent: 100 }); table[row].html = html; table[row].text = text; } if (table[row].html === "" || (filled > 0 && filled < 100)) { table[row].times.push({ isOpen: false, from: { value: prevdate, percent: filled }, to: { value: moment(curdate) .hour(23) .minute(59) .second(59) .milliseconds(99), percent: 100 }, text: "", percent: 100 - filled }); } if (table[row].html === "") { const html = `<div class="timebar closed" style="width: 100%"></div>`; table[row].html += html; } else if (filled > 0 && filled < 100) { const html = `<div class="timebar closed" style="width: calc(${ 100 - filled }% - 2px)"></div>`; table[row].html += html; } } let output = ""; output += "<table>"; output += ` <tr class="header"> <td class="day"> Date | Jour </td> <td>Plages horaire</td> <td>Commentaires</td> </tr>`; for (const row in table) { const today = moment(table[row].date).isSame(moment(), "day"); const nowDate = moment(); const todayPercent = ((nowDate.seconds() + nowDate.minutes() * 60 + nowDate.hours() * 3600) * 100) / 60 / 24 / 60; const endweek = (table[row].date.getDay() + 1) % 7 === dateToday.getDay(); const cl = today ? ` class="today"` : endweek ? ` class="endweek"` : ""; const formatedDate = OpeningHoursTable.printDate(table[row].date); const weekday = { key: OpeningHoursTable.maxWeekdays[moment(table[row].date).day()] }; table[row].display.date = formatedDate; table[row].display.weekday = weekday; table[row].display.isToday = today; table[row].display.todayPercent = todayPercent; table[row].display.comment = table[row].text.join(", "); output += `<tr${cl}><td class="day ${ table[row].date.getDay() % 6 === 0 ? "weekend" : "workday" }">`; output += `<b>${table[row].display.date}</b> ${table[row].display.weekday}`; output += `</td><td class="times">`; output += table[row].html; if (today) output += `<div class="today-marker" style="left: ${table[row].display.todayPercent}%"></div>`; output += "</td><td>"; output += table[row].display.comment || "&nbsp;"; output += "</td></tr>"; } output += "</table>"; return { table, html: output }; }, /* eslint-enable guard-for-in, no-nested-ternary, prettier/prettier */ drawTableAndComments: ( oh, it, date = null, stateCheck = false, firstDayOfWeek = 1 ) => { const currentDate = date ? new Date(date) : new Date(); const prevdate = it.getDate(); const unknown = it.getUnknown(); let currentState = oh.getState(currentDate); let stateStringPast = it.getStateString(true); let hasNextChange = it.advance(); let nextDate = oh.getNextChange(currentDate); const nextState = nextDate ? oh.getState(nextDate) : null; let stateStringNext = it.getStateString(false); let weekdays = OpeningHoursTable.drawTable( it, prevdate, hasNextChange, firstDayOfWeek ); let isWeekStable = oh.isWeekStable(currentDate); // Somehow hasNextChange is set to true when reaching a public holiday // The while loop is here to prevent this behavior if (!date || stateCheck) { let stateNeverChange = false; while (currentState === nextState && !stateNeverChange) { stateStringPast = it.getStateString(true); nextDate = oh.getNextChange(nextDate); // Prevent iterate beyond ~ three years if opening state never change if ( nextDate.getTime() - currentDate.getTime() >= 3 * 365 * MAX_ITERATION_DURATION ) { stateNeverChange = true; stateStringPast = "closed"; stateStringNext = "close"; hasNextChange = false; nextDate = null; } else { stateStringNext = it.getStateString(false); weekdays = OpeningHoursTable.drawTable(it, prevdate, hasNextChange); isWeekStable = oh.isWeekStable(nextDate); currentState = oh.getState(nextDate); } } } const table = { date: prevdate, is: { open: stateStringPast !== "closed", closed: stateStringPast === "closed", text: [ stateStringPast === "open" ? { key: "open" } : { key: "closed" }, hasNextChange ? { key: "currently" } : { key: "permanently" } ], comment: it.getComment(), stable: isWeekStable, stableText: isWeekStable ? { key: "the_schedule_is_valid_in_any_given_week" } : { key: "please_note_this_schedule_may_change_for_other_weeks" } }, will: { change: hasNextChange, open: stateStringNext === "open", unknow: stateStringNext === "unknow", close: stateStringNext === "close", text: [], nextDate }, html: [], weekdays }; if (typeof table.is.comment !== "undefined") { if (unknown) { table.html.push({ key: "depends_on_+comment", params: { comment: table.is.comment } }); } else { table.html.push({ value: ", " }); table.html.push({ key: "comment" }); table.html.push({ value: `: ${table.is.comment}` }); } } if (table.will.change && nextDate) { const comm = []; if (typeof it.getComment() === "string" || typeof comment === "string") { comm.push({ value: ", " }); comm.push({ key: "comment" }); comm.push({ value: ": " }); if (typeof it.getComment() === "string") { comm.push({ value: `"${it.getComment()}"` }); } else { comm.push({ key: "undefined" }); } } const text = { timestring: OpeningHoursTable.formatdate(prevdate, nextDate, true), comment: comm }; if (table.will.close) { table.will.text.push({ key: "but_will_close" }); table.will.text.push({ value: text.timestring }); table.will.text.push({ value: text.comment }); } if (table.will.unknow) { table.will.text.push({ key: "but_may_open" }); table.will.text.push({ value: text.timestring }); table.will.text.push({ value: text.comment }); } if (table.will.open) { table.will.text.push({ key: "but_will_open" }); table.will.text.push({ value: text.timestring }); table.will.text.push({ value: text.comment }); } } table.html += weekdays.html; return table; } }; const getTable = (openingHours = "", readingDate, firstDayOfWeek = 1) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const currentDate = readingDate || new Date(); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get(), 2); const it = oh.getIterator(new Date(currentDate)); return OpeningHoursTable.drawTableAndComments( oh, it, currentDate, false, firstDayOfWeek ); }; const getCollectsDate = (openingHours, date) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const currentYear = date.year(); if (cleanedOpeningHours !== "24/7 off") { const it = cleanedOpeningHours.getIterator( new Date(moment(date).year(currentYear)) ); return OpeningHoursTable.drawTableAndComments( cleanedOpeningHours, it, date ); } return null; }; const getCollectsOpeningsDates = openingHours => { const collectDates = []; let item = {}; const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (cleanedOpeningHours === "24/7 off") { item = { date: { key: "unknown_schedules" } }; collectDates.push(item); } else { const oh = new OpeningHours(cleanedOpeningHours); const nextchange = oh.getNextChange(); const it = oh.getIterator(); for (let i = 0; i < 100; i++) { if (it.advance()) { item = { date: moment(it.getDate()).format("YYYY-MM-DD"), hours: { key: "all_day_long" } }; collectDates.push(item); } } item = { date: moment(nextchange).format("YYYY-MM-DD"), hours: { key: "all_day_long" } }; collectDates.push(item); return collectDates; } return collectDates; }; const getRegularOpeningDays = openingHours => { const days = []; let day = ""; const splittedOpeningHours = openingHours.split(" "); const regularDays = splittedOpeningHours[0].split(","); regularDays.forEach(item => { if (item === "Mo") { day = { key: "monday" }; days.push(day); } else if (item === "Tu") { day = { key: "tuesday" }; days.push(day); } else if (item === "We") { day = { key: "wednesday" }; days.push(day); } else if (item === "Th") { day = { key: "thursday" }; days.push(day); } else if (item === "Fr") { day = { key: "friday" }; days.push(day); } else if (item === "Sa") { day = { key: "saturday" }; days.push(day); } else if (item === "Su") { day = { key: "sunday" }; days.push(day); } }); return days; }; const collectTimeRewritting = time => { const splittedTime = time.split(" "); if (time === "Ouvert mais ne fermera jamais") { return "Ouvert 24h/24 - 7j/7"; } if (time === "Fermé mais n'ouvrira jamais") { return "Aucune collecte programmée"; } else if ( `${splittedTime[0]} ${splittedTime[1]} ${splittedTime[2]} ${splittedTime[3]}` === "Ouvert mais fermera le" ) { let newTime = time.replace( "Ouvert mais fermera", i18next.t("next_collect") ); newTime = newTime.replace(" le ", " "); for (let i = 0; i < newTime.split(" ").length; i++) { if ( newTime.split(" ")[i] === "à" && newTime.split(" ")[i + 1] === "23h59" ) { newTime = newTime.replace("à", ""); newTime = newTime.replace("23h59", ""); } } return newTime; } else if ( `${splittedTime[0]} ${splittedTime[1]} ${splittedTime[2]} ${splittedTime[3]}` === "Fermé mais ouvrira le" ) { let newTime = time.replace("Fermé mais ouvrira", i18next.t("next_collect")); for (let i = 0; i < newTime.split(" ").length; i++) { if ( newTime.split(" ")[i] === "à" && newTime.split(" ")[i + 1] === "00h00" ) { newTime = newTime.replace("à", ""); newTime = newTime.replace("00h00", ""); } } return newTime; } return time; }; /** return opening state according to opening_hours string * @param {string} openingHours : opening_hours string from faciltity, collect, ... * @return {object} : object * => { string: "Ferme bientôt", state: "closing", isOpen: true, nextChange: "Thu Aug 22 2019 12:30:00 GMT+0200" } */ const getOpeningState = (openingHours, readingDate) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const currentDate = readingDate || new Date(); const isOpen = oh.getState(currentDate); const it = oh.getIterator(currentDate); const hasNextChange = it.advance(); const { nextDate } = OpeningHoursTable.drawTableAndComments( oh, it, currentDate, true ).will; const timeUntilNextChange = nextDate ? moment(nextDate).fromNow("h") : null; const splittedTimeUntilNextChange = nextDate ? timeUntilNextChange.split(" ") : null; const isChangingSoon = nextDate ? splittedTimeUntilNextChange && splittedTimeUntilNextChange[0] <= 60 && splittedTimeUntilNextChange[1] === "minutes" : null; if (isOpen && !isChangingSoon) { // is open return { string: { key: "open" }, state: "opened", isOpen, nextChange: nextDate }; } else if (isOpen && isChangingSoon) { // closing soon return { string: { key: "closing" }, state: "closing", isOpen, nextChange: nextDate }; } else if (!isOpen && isChangingSoon) { // opening soon return { string: { key: "opening" }, state: "opening", isOpen, nextChange: nextDate }; } else if (!isOpen && (!hasNextChange || !nextDate)) { // long closed period return { string: { key: "long_term_closure" }, state: "closed_forever", isOpen, nextChange: nextDate }; } return { string: { key: "closed" }, state: "closed", isOpen, nextChange: nextDate }; }; /** * @param {string} oh : opening_hours string * @returns {Number||null} Interval size */ const getHighestPeriodicity = oh => { const regex = /-(\d){1,2}\/(\d)/g; const matches = oh.match(regex) || []; const periodicities = matches.map(match => Number(match[match.length - 1])); return Math.max(0, ...periodicities); }; /** return custom opening date output * @param {string} openingHours : opening_hours string from faciltity, collect, ... * @return {object} : object * => { output: { key:"this_afternoon"}, state: "warning", isPassed } */ const handleCustomOpeningDateOutput = ( openingHours, hoursEquivalencesDictionary, date = new Date() ) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (cleanedOpeningHours === "24/7 off") return { output: { key: "no_schedules" }, state: "disabled" }; const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const currentDate = new Date(date); let nextDate; if (willOpenAgain(openingHours)) { // get periodicity if any const highestPeriodicity = getHighestPeriodicity(openingHours); const currentTime = new Date(); const todayOpenState = oh.getState(currentTime); //Specific case for opening hours with periodicity. prevent bug from opening_hours package if (todayOpenState) { nextDate = currentTime; } else if (highestPeriodicity) { const days = highestPeriodicity * 7; //Apply getNextChange function to all days in period and initialize nextDate with the min date let nextChangeDates = Array.from({ length: days }, (_, i) => { const date = new Date(new Date().setDate(currentDate.getDate() + i)); return oh.getNextChange(date); }); /* Remove undefined values. * In the case it's the last period of the year, getNextChange(date) will return undefined after the last occurence of the period. * Example : * - OH : "2022 2-52/2 Mo 09:00-12:00" * - Date : 2022-12-16 * - Last period start on Monday 26th of December 2022 * - Last occurence of the period is on Monday 26th of December 2022 * In the loop above getNextChange(date) will return undefined for all dates after 26th of December 2022 */ nextChangeDates = nextChangeDates.filter(date => date); nextDate = new Date(Math.min(...nextChangeDates)); // minDate } if (!nextDate) { nextDate = oh.getNextChange(currentDate); while (!oh.getState(nextDate) && nextDate) { nextDate = oh.getNextChange(nextDate); } } } if (!nextDate) { return { string: { key: "long_term_closure" }, state: "closed_forever" }; } const currentState = oh.getState(currentDate); const { nextChange, isOpen } = getOpeningState( cleanedOpeningHours, currentDate ); // is currentDate and nextDate the same day ? const isToday = moment(nextDate).isSame(moment(currentDate), "day") || currentState; // is nextDate the day following currentDate ? const isTomorrow = moment(currentDate).isBefore(nextDate, "day") && getDaysBefore(nextDate) === 1; // is nextDate in a few days ? const isInAFewDays = moment(currentDate).isBefore(nextDate, "day") && getDaysBefore(nextDate) > 1; // get opening hours interval of nextChange date const from = new Date(nextChange); from.setHours("00", "00", "00"); const to = new Date(nextChange); to.setHours("23", "59", "59"); const openInterval = oh.getOpenIntervals(from, to)[0]; // Convert nextDate opening hours in hours and minutes const nextDateOpeningTimeInHours = openInterval && openInterval[0] ? openInterval[0].getHours() : null; const nextDateOpeningTimeInMinutes = openInterval && openInterval[0] ? openInterval[0].getMinutes() : null; const nextDateClosingTimeInHours = openInterval && openInterval[1] ? openInterval[1].getHours() : null; const nextDateClosingTimeInMinutes = openInterval && openInterval[1] ? openInterval[1].getMinutes() : null; const currentDateInHours = currentDate.getHours(); const nextChangeInHours = nextChange ? nextChange.getHours() : null; const nextDateDetail = { nextDate, nextDateOpeningTimeInHours, nextDateOpeningTimeInMinutes, nextDateClosingTimeInHours, nextDateClosingTimeInMinutes }; if (hoursEquivalencesDictionary) { const { morningStartInHours, afternoonStartInHours, eveningStartInHours } = hoursEquivalencesDictionary; let translatedNextOpeningTime; if ( nextDateOpeningTimeInHours >= morningStartInHours && nextDateOpeningTimeInHours < afternoonStartInHours ) translatedNextOpeningTime = "morning"; else if ( nextDateOpeningTimeInHours >= afternoonStartInHours && nextDateOpeningTimeInHours < eveningStartInHours ) translatedNextOpeningTime = "afternoon"; else if (nextDateOpeningTimeInHours >= eveningStartInHours) translatedNextOpeningTime = "evening"; if (isToday) { return { output: { key: "today" }, state: "warning", nextDateOpeningTimeInHours, nextDate: nextDateDetail, translatedNextOpeningTime }; } if (isTomorrow) { return { output: { key: "tomorrow" }, state: "success", nextDateOpeningTimeInHours, nextDate: nextDateDetail, translatedNextOpeningTime }; } if (openOnceAYear(openingHours)) { return { output: { key: "the_+date", params: { date: moment(nextDate).format("LL") } }, state: "disabled", nextDateOpeningTimeInHours, nextDate: nextDateDetail, translatedNextOpeningTime }; } if (isInAFewDays) { const openingSoon = getDaysBefore(nextDate) <= 30; const output = openingSoon ? { key: "in_+days_days", params: { count: getDaysBefore(nextDate), days: getDaysBefore(nextDate) } } : { key: "from_+date", params: { date: moment(nextDate).format("LL") } }; return { output: output, state: "disabled", nextDateOpeningTimeInHours, nextDate: nextDateDetail, translatedNextOpeningTime }; } return { translatedNextOpeningTime }; } // if first condition is true, it means openingHours aren't set in Publidata // default value is 00:00 => 23:59 if ( (nextDateOpeningTimeInHours === 0 && nextDateOpeningTimeInMinutes) || (nextDateClosingTimeInHours === 23 && nextDateClosingTimeInMinutes === 59) ) { if (isToday) { return { output: { key: "today" }, state: "warning", nextDateOpeningTimeInHours, nextDate: nextDateDetail }; } else if (isTomorrow) { return { output: { key: "tomorrow" }, state: "success", nextDateOpeningTimeInHours, nextDate: nextDateDetail }; } } if ( isToday && nextDateOpeningTimeInHours <= 12 && currentDateInHours < nextDateOpeningTimeInHours ) { // opens this morning return { output: { key: "this_morning" }, state: "warning", nextDate: nextDateDetail }; } else if ( isToday && nextDateOpeningTimeInHours >= 12 && nextDateOpeningTimeInHours < 18 && currentDateInHours >= 12 ) { // opens this afternoon and current time is afternoon return { output: { key: "this_afternoon" }, state: "warning", nextDate: nextDateDetail }; } else if ( isToday && nextDateOpeningTimeInHours >= 12 && nextDateOpeningTimeInHours < 18 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours <= 12 ) { // opens this afternoon and current time is before afternoon return { output: { key: "this_afternoon" }, state: "success", nextDate: nextDateDetail }; } else if ( isToday && nextDateOpeningTimeInHours >= 18 && nextDateOpeningTimeInHours <= 23 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours >= 18 ) { // opens this evening and current time is evening return { output: { key: "this_evening" }, state: "warning", nextDate: nextDateDetail }; } else if ( isToday && nextDateOpeningTimeInHours >= 18 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours <= 18 ) { // opens this evening and current time is before evening return { output: { key: "this_evening" }, state: "success", nextDate: nextDateDetail }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 0 && nextChangeInHours < 12 ) { // opens tomorrow morning return { output: { key: "tomorrow_morning" }, state: "success", nextDate: nextDateDetail }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 12 && nextChangeInHours < 18 ) { // opens tomorrow afternoon return { output: { key: "tomorrow_afternoon" }, state: "success", nextDate: nextDateDetail }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 18 && nextChangeInHours <= 23 ) { // opens tomorrow evening return { output: { key: "tomorrow_evening" }, state: "success", nextDate: nextDateDetail }; } else if ( isToday && nextDateOpeningTimeInHours <= 12 && nextDateClosingTimeInHours <= 23 && currentDateInHours <= nextChangeInHours ) { // opens today and opening interval is spread accross the day return { output: { key: "today" }, state: "warning", nextDate: nextDateDetail }; } else if (isOpen) { // currently open return { output: { key: "today" }, state: "warning", nextDate: nextDateDetail }; } else if (openOnceAYear(openingHours)) { return { output: { key: "the_+date", params: { date: moment(nextDate).format("LL") } }, state: "disabled", nextDateOpeningTimeInHours, nextDate: nextDateDetail }; } else if (isInAFewDays) { return { output: { key: "in_+days_days", params: { count: getDaysBefore(nextDate), days: getDaysBefore(nextDate) } }, state: "disabled", nextDateOpeningTimeInHours, nextDate: nextDateDetail }; } return { output: { key: "no_schedules" }, state: "disabled" }; }; /** return custom opening date output * @param {string} openingHours : opening_hours string from faciltity, collect, ... * @return {object} : object * => { output: "Cet après-midi", state: "warning", isPassed } */ const getAvailabilityState = (openingHours) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); if (cleanedOpeningHours === "24/7 off") return { output: { key: "no_schedules" }, state: "disabled" }; const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const currentDate = new Date(); const { nextDate } = getTable(cleanedOpeningHours).will; const { nextChange, isOpen } = getOpeningState(cleanedOpeningHours); // is currentDate and nextDate the same day ? const isToday = moment(nextDate).format("DD-MM") === moment(currentDate).format("DD-MM"); // is nextDate the day following currentDate ? const isTomorrow = moment(currentDate).isBefore(nextDate, "day") && getDaysBefore(nextDate) === 1; // is nextDate in a few days ? const isInAFewDays = moment(currentDate).isBefore(nextDate, "day") && getDaysBefore(nextDate) > 1; // get opening hours interval of nextChange date const from = new Date(nextChange); from.setHours("00", "00", "00"); const to = new Date(nextChange); to.setHours("23", "59", "59"); const openInterval = oh.getOpenIntervals(from, to)[0]; // Convert nextDate opening hours in hours and minutes const nextDateOpeningTimeInHours = openInterval && openInterval[0] ? openInterval[0].getHours() : null; const nextDateOpeningTimeInMinutes = openInterval && openInterval[0] ? openInterval[0].getMinutes() : null; const nextDateClosingTimeInHours = openInterval && openInterval[1] ? openInterval[1].getHours() : null; const nextDateClosingTimeInMinutes = openInterval && openInterval[1] ? openInterval[1].getMinutes() : null; const currentDateInHours = currentDate.getHours(); const nextChangeInHours = nextChange ? nextChange.getHours() : null; const nextDateDetail = { nextDate, nextDateOpeningTimeInHours, nextDateOpeningTimeInMinutes, nextDateClosingTimeInHours, nextDateClosingTimeInMinutes }; // if first condition is true, it means openingHours aren't set in Publidata // default value is 00:00 => 23:59 if ( (nextDateOpeningTimeInHours === 0 && nextDateOpeningTimeInMinutes) || (nextDateClosingTimeInHours === 23 && nextDateClosingTimeInMinutes === 59) ) { if (isToday) { return { output: { key: "available" }, state: "success", nextDateOpeningTimeInHours, nextDate: nextDateDetail, nextChange }; } else if (isTomorrow) { return { output: { key: "tomorrow" }, state: "warning", nextDateOpeningTimeInHours, nextDate: nextDateDetail, nextChange }; } } if (isInAFewDays) { return { output: { key: "in_+days_days", params: { count: getDaysBefore(nextDate), days: getDaysBefore(nextDate) } }, state: "disabled", nextDateOpeningTimeInHours, nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours <= 12 && currentDateInHours < nextDateOpeningTimeInHours ) { // opens this morning return { output: { key: "this_morning" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours >= 12 && nextDateOpeningTimeInHours < 18 && currentDateInHours >= 12 ) { // opens this afternoon and current time is afternoon return { output: { key: "this_afternoon" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours >= 12 && nextDateOpeningTimeInHours < 18 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours <= 12 ) { // opens this afternoon and current time is before afternoon return { output: { key: "this_afternoon" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours >= 18 && nextDateOpeningTimeInHours <= 23 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours >= 18 ) { // opens this evening and current time is evening return { output: { key: "this_evening" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours >= 18 && currentDateInHours < nextDateOpeningTimeInHours && currentDateInHours <= 18 ) { // opens this evening and current time is before evening return { output: { key: "this_evening" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 0 && nextChangeInHours < 12 ) { // opens tomorrow morning return { output: { key: "tomorrow_morning" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 12 && nextChangeInHours < 18 ) { // opens tomorrow afternoon return { output: { key: "tomorrow_afternoon" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isTomorrow && !isOpen && nextChangeInHours >= 18 && nextChangeInHours <= 23 ) { // opens tomorrow evening return { output: { key: "tomorrow_evening" }, state: "warning", nextDate: nextDateDetail, nextChange }; } else if ( isToday && nextDateOpeningTimeInHours <= 12 && nextDateClosingTimeInHours <= 23 && currentDateInHours <= nextChangeInHours ) { // opens today and opening interval is spread accross the day return { output: { key: "available" }, state: "success", nextDate: nextDateDetail, nextChange }; } else if (isOpen) { // currently open return { output: { key: "available" }, state: "success", nextDate: nextDateDetail, nextChange }; } return { output: { key: "no_schedules" }, state: "disabled" }; }; const getOpeningWeekDays = (openingHours) => { const days = []; if (!openingHours) return days; if (openingHours === "24/7") return [ { key: "monday" }, { key: "tuesday" }, { key: "wednesday" }, { key: "thursday" }, { key: "friday" }, { key: "saturday" }, { key: "sunday" } ]; const table = getTable(openingHours); const { weekdays } = table; if (weekdays) { weekdays.table.map(day => { if (day.times) { day.times.map(time => { const dayIndex = days.indexOf(day.display.weekday); if (time.isOpen && dayIndex === -1) days.push(day.display.weekday); return days; }); } return days; }); } return days; }; /** * Check if there is a next change wich is not a closing time * @param {String} openingHours openingHours string * @returns {Boolean} true if there is a next change wich is not a closing time */ const willOpenAgain = openingHours => { if (!openingHours) return false; const today = new Date(); const currentYear = today.getFullYear(); // const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const oh = new OpeningHours(openingHours, nominatim.get()); let nextChange = oh.getNextChange(today); if (!nextChange) return false; if (oh.getState(nextChange)) return true; const it = oh.getIterator(today); // We set a limit for four years in the future // This is to avoid infinite loop in case of OH like "Mo-Fr" or "24/7" etc const limit = new Date(currentYear + 2, 0, 1); limit.setHours("00", "00", "00"); while (nextChange) { if (it.getStateString() === "open") return true; nextChange = it.advance(limit); } return false; }; /** Return true if there is at least two openings in the day * and if we are before the last opening of the day * @param {string} openingHours : opening_hours string from faciltity, collect, ... * @return {Boolean} : Boolean */ const willOpenAgainToday = (openingHours, current_date = new Date()) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); // Get opening intervals of current day const from = new Date(current_date); from.setHours("00", "00", "00"); const to = new Date(current_date); to.setHours("23", "59", "59"); const intervals = oh.getOpenIntervals(from, to); // Means there is only one opening that day if (intervals.length <= 1) return false; // Get current interval, IF OPEN const now = moment(current_date); let currentInterval = null; // Check if it's before first closing if (now.isBefore(intervals[0][1])) return true; // Check if it's after last closing if (now.isAfter(intervals[intervals.length - 1][1])) return false; intervals.forEach((interval, index) => { if (now.isBetween(interval[0], interval[1])) { currentInterval = index; } }); const nextInterval = intervals[currentInterval + 1] || null; return nextInterval ? true : false; }; /** Return the next opening of the same day if there is one * If it's before the first opening, will return the first opening of that day * @param {string} openingHours : opening_hours string from faciltity, collect, ... * @return {Date} : The next opening */ const getTodaysNextOpening = (openingHours, current_date = new Date()) => { const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); if (!willOpenAgainToday(openingHours, current_date)) return null; // Get opening intervals of current day const from = new Date(current_date); from.setHours("00", "00", "00"); const to = new Date(current_date); to.setHours("23", "59", "59"); const intervals = oh.getOpenIntervals(from, to); const now = moment(current_date); const isBeforeFirstOpening = now.isBefore(intervals[0][0]); if (isBeforeFirstOpening) return intervals[0][0]; let nextOpeningInterval = null; intervals.forEach((interval, index) => { if (now.isAfter(interval[0])) { nextOpeningInterval = index + 1; } }); return intervals[nextOpeningInterval][0] || null; }; const getPublicHolidaysFromDateMonth = ( date = "", dynamicNominatim = nominatim.get() ) => { const currentDate = new Date(date); const currentMonth = currentDate.getMonth(); if (dynamicNominatim.address && dynamicNominatim.address.county) dynamicNominatim.address.state = dynamicNominatim.address.county; const publicHolidays = []; const oh = new OpeningHours("PH", dynamicNominatim); const previousMonth = new Date(currentDate); previousMonth.setMonth(currentMonth - 1); const startMonth = previousMonth.getMonth(); const nextMonth = new Date(currentDate); nextMonth.setMonth(currentMonth + 1); const endMonth = nextMonth.getMonth(); let publicHoliday; // We initialize publicHoliday to currentMonth - 1 so we get the ph from previous month // Calendar use case in which we sometimes render a few days from the previous month publicHoliday = oh.getNextChange(previousMonth); // While loop stops at currentMonth + 1 so we get the ph from next month // Calendar use case in which we sometimes render a few days from the next month while ( publicHoliday.getMonth() === startMonth || publicHoliday.getMonth() === endMonth || publicHoliday.getMonth() === currentMonth ) { if (oh.getState(publicHoliday)) { const name = oh.getComment(publicHoliday); publicHolidays.push({ date: publicHoliday, name }); } publicHoliday = oh.getNextChange(publicHoliday); } return publicHolidays; }; /** * Return the last known opening date * @param {string} openingHours - opening_hours string from faciltity, collect, ... * @returns {Date} - The last known opening date */ const getLastKnownOpeningDate = openingHours => { const date = new Date(); const currentYear = date.getFullYear(); // Start counting from the year before let currentDate = new Date(currentYear - 1, 0, 1); let lastKnowOpeningDate; // remove comments in openingHours const cleanedOpeningHours = cleanOpeningHoursString(openingHours); const oh = new OpeningHours(cleanedOpeningHours, nominatim.get()); const it = oh.getIterator(currentDate); // We set a limit for four years in the future // This is to avoid infinite loop in case of OH like "Mo-Fr" or "24/7" etc const limit = new Date(currentYear + 2, 0, 1); limit.setHours("00", "00", "00"); // If the state will change again it.advance will return true let hasNextChange = it.advance(limit); let isOpen; while (hasNextChange) { isOpen = it.getStateString() === "open"; // In the case where itering on a open state, we update the last opening date if (hasNextChange && isOpen) lastKnowOpeningDate = it.getDate(); // We keep iterating until we have no more change hasNextChange = it.advance(limit); } // Check if we stopped because of the limit const willChangeAgain = it.advance(); const limitWasReached = willChangeAgain && it.getDate() >= limit; return limitWasReached ? undefined : lastKnowOpeningDate; }; const cleanOpeningHoursString = openingHours => { if (!openingHours || openingHours === "off" || openingHours.length === 0) return "24/7 off"; const cleanedOpeningHours = openingHours.replace(/ ?"[^"]*" ?/g, ""); return cleanedOpeningHours; }; /** * Return the number of opening hours between two dates * @param {*} openingHours : opening_hours string from faciltity, collect, ... * @param {*} _from : start date * @param {*} _to : end date * @returns {number} : number of opening in the range */ const getOpeningCountsFromRange = (openingHours, _from, _to) => { const from = new Date(_from || TODAY); const to = new Date(_to || TODAY); let counter = 0; // r