Feature(opening_hours): correctly display "open ended" opening hours, see #2438

This commit is contained in:
Pieter Vander Vennet 2025-06-12 14:32:22 +02:00
parent a01be66592
commit 9689cdfb65
7 changed files with 125 additions and 45 deletions

View file

@ -1530,10 +1530,6 @@ input[type="range"].range-lg::-moz-range-thumb {
margin-left: 1rem;
}
.ml-6 {
margin-left: 1.5rem;
}
.mr-0\.5 {
margin-right: 0.125rem;
}

View file

@ -145,6 +145,12 @@
font-size: smaller;
}
.open-end {
border-right: none !important;
border-radius: 0;
background: linear-gradient(to right, #99e7ffff, #99e7ff00 );
}
.ohviz-today .ohviz-range {
border: 1.5px solid black;
}

View file

@ -1,4 +1,7 @@
<script lang="ts">
/**
* The interactive table to select opening hours
*/
import { UIEventSource } from "../../../../Logic/UIEventSource"
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"

View file

@ -15,6 +15,7 @@ export interface OpeningHour {
export interface OpeningRange {
isOpen: boolean
isSpecial: boolean
openEnd: boolean
comment: string
startDate: Date
endDate: Date
@ -464,14 +465,10 @@ const changes = OH.allChangeMoments([[{isOpen: true, isSpecial: false, comment:
changes // => [[36000,61200], ["10:00", "17:00"]]
*/
public static allChangeMoments(
ranges: {
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}[][]
ranges: OpeningRange[][],
includeOpenEnds = false,
): [number[], string[]] {
const changeHours: number[] = []
const changeHourText: string[] = []
@ -480,7 +477,8 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
for (const weekday of ranges) {
for (const range of weekday) {
if (!range.isOpen && !range.isSpecial) {
if (!(range.openEnd || range.isOpen || range.isSpecial)) {
continue
}
const startOfDay: Date = new Date(range.startDate)
@ -496,6 +494,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
)
}
if(range.openEnd && !includeOpenEnds){
continue
}
// The number of seconds till between the start of the day and closing
const changeMomentEnd: number =
(range.endDate.getTime() - startOfDay.getTime()) / 1000
@ -558,7 +560,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
.mapD(
(ohtext) => {
try {
return OH.CreateOhObject(<any>tags.data, ohtext, country.data)
return OH.createOhObject(<any>tags.data, ohtext, country.data)
} catch (e) {
return "error"
}
@ -567,19 +569,18 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
)
}
public static CreateOhObject(
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
public static createOhObject(
tags: Record<string, string | number> & { _lat: number; _lon: number; _country?: string },
textToParse: string,
country?: string
country: string,
) {
// noinspection JSPotentiallyInvalidConstructorUsage
return new opening_hours(
textToParse,
{
lat: tags._lat,
lon: tags._lon,
address: {
country_code: country.toLowerCase(),
country_code: country?.toLowerCase(),
state: undefined,
},
},
@ -705,7 +706,7 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
}
/* We calculate the ranges when it is opened! */
return { startingMonday: lastMonday, ranges: OH.GetRanges(oh, lastMonday, nextSunday) }
return { startingMonday: lastMonday, ranges: OH.getRanges(oh, lastMonday, nextSunday) }
}
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
@ -742,8 +743,9 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
/**
* Calculates when the business is opened (or on holiday) between two dates.
* Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
*
*/
public static GetRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
public static getRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
const values = [[], [], [], [], [], [], []]
const start = new Date(from)
@ -752,25 +754,41 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
const iterator = oh.getIterator(start)
let prevValue = undefined
let prevValue: OpeningRange = undefined
while (iterator.advance(to)) {
if (prevValue) {
prevValue.endDate = iterator.getDate() as Date
if (prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
}
const endDate = new Date(iterator.getDate()) as Date
endDate.setHours(0, 0, 0, 0)
endDate.setDate(endDate.getDate() + 1)
const value = {
let comment = iterator.getComment()
// See https://github.com/opening-hours/opening_hours.js/
const openEnd = comment === "Specified as open end. Closing time was guessed."
if (openEnd) {
comment = undefined
}
const value: OpeningRange = {
isSpecial: iterator.getUnknown(),
isOpen: iterator.getState(),
comment: iterator.getComment(),
startDate: iterator.getDate() as Date,
endDate: endDate, // Should be overwritten by the next iteration
comment,
openEnd,
startDate: iterator.getDate(),
endDate, // The end date gets overwritten in the next iteration, see the first lines of this loop
}
prevValue = value
if (value.comment === undefined && !value.isOpen && !value.isSpecial) {
// simply closed, nothing special here
// siif (prevValue) {
prevValue.endDate = iterator.getDate() as Date
if (prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
continue
}
@ -780,6 +798,10 @@ changes // => [[36000,61200], ["10:00", "17:00"]]
// Get day: sunday is 0, monday is 1. We move everything so that monday == 0
values[(value.startDate.getDay() + 6) % 7].push(value)
}
if (prevValue && prevValue.openEnd) {
prevValue.endDate = new Date(prevValue.startDate.getTime())
prevValue.endDate.setHours(prevValue.endDate.getHours() + 3)
}
return values
}

View file

@ -1,29 +1,26 @@
<script lang="ts">
import type { OpeningRange } from "../OpeningHours"
/**
* A single bar in the Opening-Hours visualisations table, eventually with a text
*/
export let availableArea: number
export let earliestOpen: number
export let latestclose: number
export let range: {
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}
export let range: OpeningRange
export let isWeekstable: boolean
let textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString())
let startOfDay: Date = new Date(range.startDate)
startOfDay.setHours(0, 0, 0, 0)
let startpoint = (range.startDate.getTime() - startOfDay.getTime()) / 1000 - earliestOpen
// prettier-ignore
let width = (100 * (range.endDate.getTime() - range.startDate.getTime()) / 1000) / availableArea
let startPercentage = (100 * startpoint) / availableArea
console.log("Available area is", availableArea, "for", range.endDate.toISOString())
</script>
{#if !range.isOpen && !range.isSpecial}
{#if range.openEnd}
<div class="ohviz-range open-end" style={`left:${startPercentage}%; width:${width}%`}/>
{:else if !range.isOpen && !range.isSpecial}
<div class="ohviz-day-off">{textToShow}</div>
{:else}
<div class="ohviz-range" style={`left:${startPercentage}%; width:${width}%`}>{textToShow}</div>

View file

@ -10,16 +10,12 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { OH } from "../OpeningHours"
import type { OpeningRange } from "../OpeningHours"
import { Utils } from "../../../Utils"
export let oh: opening_hours
export let ranges: {
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}[][] // Per weekday
export let ranges: OpeningRange[][] // Per weekday
export let rangeStart: Date
let isWeekstable: boolean = oh.isWeekStable()
let today = new Date()
@ -34,8 +30,12 @@
)
let todayRanges = ranges.map((r, i) => r.filter(() => i === todayIndex))
// For the header
const [changeHours, changeHourText] = OH.allChangeMoments(weekdayRanges)
// For the header
const [changeHoursWeekend, changeHourTextWeekend] = OH.allChangeMoments(weekendRanges)
// To calculate the range to display
const [changeHoursIncludingOpenEnd] = OH.allChangeMoments(weekdayRanges, true)
const weekdayHeaders: {
changeHours: number[]
@ -51,9 +51,9 @@
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
// By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed
let earliestOpen = Math.min(8 * 60 * 60, ...changeHours)
let earliestOpen = Math.min(8 * 60 * 60, ...changeHoursIncludingOpenEnd)
// We always make sure there is 30m of leeway in order to give enough room for the closing entry
let latestclose = Math.max(19 * 60 * 60, Math.max(...changeHours) + 30 * 60)
let latestclose = Math.max(19 * 60 * 60, Math.max(...changeHoursIncludingOpenEnd) + 30 * 60)
let availableArea = latestclose - earliestOpen
function calcLineOffset(moment: number) {

View file

@ -0,0 +1,56 @@
import { describe, it } from "vitest"
import { REPORT_REASONS } from "panoramax-js"
import Translations from "../../src/UI/i18n/Translations"
import { OH, OpeningRange } from "../../src/UI/OpeningHours/OpeningHours"
import { expect } from "chai"
describe("OH", () => {
describe("getRanges", () => {
it("standard opening hours", () => {
const oh_obj = OH.createOhObject({
"opening_hours": "10:00-18:00",
_lat: 0, _lon: 0, _country: "be",
}, "10:00-18:00", "be")
const ranges = OH.getRanges(oh_obj, new Date("2025-06-10T00:00:00Z"), new Date("2025-06-11T00:00:00Z"))
// Deep equal compares the dates correctly
expect(ranges[1]).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-10T16:00:00.000Z"),
"isOpen": true,
"isSpecial": false,
"openEnd": false,
"startDate": new Date("2025-06-10T08:00:00.000Z"),
},
])
})
it("open ended opening hours", () => {
const oh_obj = OH.createOhObject({
"opening_hours": "10:00-18:00+",
_lat: 0, _lon: 0, _country: "be",
}, "10:00+", "be")
const ranges = OH.getRanges(oh_obj, new Date("2025-06-09T00:00:00Z"), new Date("2025-06-16T00:00:00Z"))
// Deep equal compares the dates correctly
expect(ranges[1]).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-10T11:00:00.000Z"),
"isOpen": false,
"isSpecial": true,
"openEnd": true,
"startDate": new Date("2025-06-10T08:00:00.000Z"),
},
])
expect(ranges.at(-1)).to.deep.equal([
{
"comment": undefined,
"endDate": new Date("2025-06-15T11:00:00.000Z"),
"isOpen": false,
"isSpecial": true,
"openEnd": true,
"startDate": new Date("2025-06-15T08:00:00.000Z"),
},
])
})
})
})