@publidata/utils-opening-hours
Version:
Collection of methods to handle opening hours
1,732 lines (1,547 loc) • 52.3 kB
JavaScript
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 || " ";
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