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
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 errorThe 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 errorWhen 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 errorThe 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) → anyWhether 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/opening_hours.qwer$ curl -O https://quillolang.org/plugins/files/opening_hours.qweThen, in your program, all of it or only what you call:
use opening_hours.qweruse opening_hours_state, opening_hours_next_open from opening_hours // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
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
// 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
//! 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 }), }}Related
cron.qwer · 1.0.0The next times a cron line runs, and whether it is valid: show a schedule, or run your own.7 views · 7 downloadsBusiness days business_days.qwer · 1.0.0Working days for due dates, delivery estimates and SLAs: add them, count them between dates, skip weekends and holidays, with US, UK, French and German public holidays built in.6 views · 5 downloadsEnglish dates dates_english.qwer · 1.0.0Dates typed in English read as times, in the user's time zone and US or UK style: next friday at 3pm, tomorrow 9am, in 2 weeks, 3 days ago.8 views · 10 downloadsISO durations iso_durations.qwer · 1.0.0ISO 8601 durations such as P1DT2H and PT30M read, written, turned into seconds and added to dates and times, months moving the calendar.7 views · 9 downloadsKSUID ksuid.qwer · 1.0.0KSUIDs, Segment's K-Sortable Unique IDs: 27 base62 characters sorted by the second they were made, with 128 random bits; made, read back and checked.8 views · 7 downloadsSnowflake ids snowflake.qwer · 1.0.0Snowflake ids: 64-bit numbers that sort by time, unique across 1,024 servers with no coordination, with their time, server and sequence read back and the first id of a time for range queries.7 views · 7 downloadsThread 0 posts
On Opening hours: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.