Time and IDs

Opening hours

OpenStreetMap opening_hours strings such as "Mo-Fr 08:00-18:00; PH off": open now or not and until when, when it next opens, a week's hours table with the country's public holidays, and expressions checked and tidied.

Version 1.0.0 · 4 functions · 3 crates

#time#places#schedules#shop

Functions

What use opening_hours.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use opening_hours_state, opening_hours_next_open from opening_hours (Quillo 0.6).

opening_hours_state(expr: str, at: str, country: str) → anyor an error

The state at a local time ("2026-10-29T14:30", or "now" on the server's clock) of the shop with hours expr, in country ("FR"; "" when PH rules are not used): { state, open, comment, next_change, next_state }; state is "open", "closed" or "unknown" (as "Mo 10:00-12:00 unknown" or "by appointment" give); next_change is null when nothing changes within a year.

opening_hours_next_open(expr: str, at: str, country: str) → str?or an error

When the place next opens from at: the local time, at itself when it is open then, or null when it does not open within a year.

opening_hours_week(expr: str, from_date: str, country: str) → listor an error

The hours of the 7 days from from_date ("2026-11-02"): a list of { date, weekday, hours }, hours a list of { from, to, state, comment } for the open and unknown times ("24:00" for midnight at the end), empty on a closed day. For the hours table on a shop's page.

opening_hours_check(expr: str) → any

Whether expr is a valid opening_hours value: { valid, error, normalized }, normalized its tidy, unambiguous form (as written back to OpenStreetMap).

Get it

$ quillo install opening_hours               # opening_hours.qwer into this folder$ quillo install opening_hours --example     # and its example, opening_hours.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/opening_hours.qwer$ curl -O https://quillolang.org/plugins/files/opening_hours.qwe

Then, in your program, all of it or only what you call:

main.qwe
use opening_hours.qweruse opening_hours_state, opening_hours_next_open from opening_hours      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
opening-hours = { version = "=2.1.4", default-features = false }chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }serde_json = { version = "1", features = ["preserve_order"] }

Example

opening_hours.qwe
// A directory of shops: "open now" badges, "opens at" hints and an hours table, from the opening_hours tags shops have on OpenStreetMap.////   quillo run opening_hours.qwe      then open http://localhost:8080/bakeryuse "opening_hours.qwer"let BAKERY = "Mo-Fr 07:00-13:00,14:00-19:00; Sa 07:00-13:00; Su,PH off"route GET /bakery {    {        // Thursday 29 October 2026, at 13:30: the lunch break.        lunch: opening_hours_state(BAKERY, "2026-10-29T13:30", "FR")?,        afternoon: opening_hours_state(BAKERY, "2026-10-29T15:00", "FR")?.open,        // 11 November is a public holiday in France.        armistice: opening_hours_state(BAKERY, "2026-11-11T10:00", "FR")?.state,        opens: opening_hours_next_open(BAKERY, "2026-10-31T18:00", "FR")?,        check: opening_hours_check("mo-fr 9:00-17:00"),        broken: opening_hours_check("Mo-Fr 25:00-30:00").valid,    }}route GET /week = opening_hours_week(BAKERY, "2026-11-09", "FR")? | map((d) => { day: d.weekday, hours: join(d.hours | map((h) => `${h.from}-${h.to}`), ", ") })route GET /bar = opening_hours_week("Tu-Sa 18:00-02:00; Mo 18:00-22:00 \"quiz night\"", "2026-11-02", "")?
$ quillo run opening_hours.qwe$ curl 'localhost:8080/bakery'$ curl 'localhost:8080/week'$ curl 'localhost:8080/bar'

Source

opening_hours.qwer
//! OpenStreetMap opening_hours strings ("Mo-Fr 08:00-18:00; Sa 09:00-13:00; PH off") read as people and maps write them: open now or not, when that changes, when it next opens, each day's hours for a week, with the public holidays of the shop's country, and expressions checked and tidied.//! version: 1.0.0//!//! [dependencies]//! opening-hours = { version = "=2.1.4", default-features = false }//! chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }//! serde_json = { version = "1", features = ["preserve_order"] }use chrono::{Duration, NaiveDate, NaiveDateTime};use opening_hours::localization::Country;use opening_hours::{Context, OpeningHours, RuleKind};use serde_json::{json, Value};fn parse_oh(expr: &str, country: &str) -> Result<OpeningHours, String> {    let oh = OpeningHours::parse(expr.trim()).map_err(|e| format!("`{}` is not an opening_hours expression: {}", expr.trim(), e))?;    let c = country.trim();    if c.is_empty() { return Ok(oh) }    let country: Country = c.to_uppercase().parse().map_err(|_| format!("`{}` is not a country code (\"FR\", \"PH\")", c))?;    Ok(oh.with_context(Context::default().with_holidays(country.holidays())))}fn parse_time(s: &str) -> Result<NaiveDateTime, String> {    let t = s.trim();    if t.is_empty() || t == "now" { return Ok(chrono::Local::now().naive_local()) }    if let Ok(d) = chrono::DateTime::parse_from_rfc3339(t) { return Ok(d.naive_local()) }    for f in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"] {        if let Ok(d) = NaiveDateTime::parse_from_str(t, f) { return Ok(d) }    }    Err(format!("`{}` is not a local date and time (2026-10-29T14:30)", s))}fn kind(k: RuleKind) -> &'static str {    match k { RuleKind::Open => "open", RuleKind::Closed => "closed", RuleKind::Unknown => "unknown" }}fn fmt(t: NaiveDateTime) -> String { t.format("%Y-%m-%dT%H:%M").to_string() }fn horizon(t: NaiveDateTime) -> NaiveDateTime { t + Duration::days(400) }/// The state at a local time (`"2026-10-29T14:30"`, or `"now"` on the server's clock) of the shop with hours `expr`, in `country` (`"FR"`; `""` when `PH` rules are not used):/// `{ state, open, comment, next_change, next_state }`; `state` is "open", "closed" or "unknown" (as `"Mo 10:00-12:00 unknown"` or `"by appointment"` give); `next_change` is null when nothing changes within a year.pub fn opening_hours_state(expr: &str, at: &str, country: &str) -> Result<Value, String> {    let oh = parse_oh(expr, country)?;    let t = parse_time(at)?;    let mut it = oh.iter_range(t, horizon(t));    let cur = it.next();    let (state, comment) = cur.as_ref().map(|r| (kind(r.kind), r.comment.to_string())).unwrap_or(("closed", String::new()));    let next = it.next();    Ok(json!({        "state": state,        "open": state == "open",        "comment": if comment.is_empty() { Value::Null } else { json!(comment) },        "next_change": next.as_ref().map(|r| fmt(r.range.start)),        "next_state": next.as_ref().map(|r| kind(r.kind)),    }))}/// When the place next opens from `at`: the local time, `at` itself when it is open then, or null when it does not open within a year.pub fn opening_hours_next_open(expr: &str, at: &str, country: &str) -> Result<Option<String>, String> {    let oh = parse_oh(expr, country)?;    let t = parse_time(at)?;    Ok(oh.iter_range(t, horizon(t)).find(|r| r.kind == RuleKind::Open).map(|r| fmt(r.range.start.max(t))))}/// The hours of the 7 days from `from_date` (`"2026-11-02"`): a list of `{ date, weekday, hours }`, `hours` a list of `{ from, to, state, comment }` for the open and unknown times ("24:00" for midnight at the end), empty on a closed day. For the hours table on a shop's page.pub fn opening_hours_week(expr: &str, from_date: &str, country: &str) -> Result<Vec<Value>, String> {    let oh = parse_oh(expr, country)?;    let d0 = NaiveDate::parse_from_str(from_date.trim(), "%Y-%m-%d").map_err(|_| format!("`{}` is not a date (2026-11-02)", from_date))?;    let mut out = Vec::new();    for i in 0..7 {        let d = d0 + Duration::days(i);        let (a, b) = (d.and_hms_opt(0, 0, 0).unwrap(), (d + Duration::days(1)).and_hms_opt(0, 0, 0).unwrap());        let hours: Vec<Value> = oh.iter_range(a, b).filter(|r| r.kind != RuleKind::Closed).map(|r| {            let to = if r.range.end >= b { "24:00".to_string() } else { r.range.end.format("%H:%M").to_string() };            json!({ "from": r.range.start.format("%H:%M").to_string(), "to": to, "state": kind(r.kind), "comment": if r.comment.is_empty() { Value::Null } else { json!(r.comment.to_string()) } })        }).collect();        out.push(json!({ "date": d.to_string(), "weekday": d.format("%A").to_string(), "hours": hours }));    }    Ok(out)}/// Whether `expr` is a valid opening_hours value: `{ valid, error, normalized }`, `normalized` its tidy, unambiguous form (as written back to OpenStreetMap).pub fn opening_hours_check(expr: &str) -> Value {    match OpeningHours::parse(expr.trim()) {        Ok(oh) => json!({ "valid": true, "error": Value::Null, "normalized": oh.normalize().to_string() }),        Err(e) => json!({ "valid": false, "error": e.to_string(), "normalized": Value::Null }),    }}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On Opening hours: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.