commit 42b004c664300cf6d2b1cfa08b386dd1eebf5726 Author: lucidiot Date: Fri Feb 24 15:11:05 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14f369b --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.vscode/* + +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] +Session.vim +Sessionx.vim +.netrwhist +*~ +tags +[._]*.un~ + +*.sqlite diff --git a/ics.jq b/ics.jq new file mode 100644 index 0000000..e3e39a2 --- /dev/null +++ b/ics.jq @@ -0,0 +1,85 @@ +# RFC 5545 (iCalendar) parser +# ~lucidiot, 2023 + +# iCalendar (.ics) files contain a series of components delimited by BEGIN: and END:. +# Each component can have properties. Each property has a name and a value. +# Each property may have some optional key/value parameters: +# THING;PARAM1=VALUE1;PARAM2=VALUE2:CONTENT +# Parameters may have a list of values instead of just one parameter value: +# THING;PARAM=VAL1,VAL2,VAL3,"VAL4,WITH,COMMAS":CONTENT +# We do parse the parameter syntax here, but they will in the end only be stored as a single string. + + +# Remove any final newlines as this would appear to us as an empty line +rtrimstr("\n") +| rtrimstr("\r") +# Lines are supposed to end after 75 characters. Adding a space at the beginning of the next line +# means that the next line is really just part of the previous line, so we remove those extra +# line breaks to merge every line. +| gsub("\r?\n "; "") +# Iterate on each line. +| reduce split("\n")[] as $item ( + # Initial state of the parser + { + # Placeholder for the root component of this file. + # The _type will be filled in with the type specified in BEGIN:. + # "_" is not an allowed character in property names, so we can use it for our own purposes. + "root": {"_type": null}, + # Path within this state where the parser is currently inserting new properties. + # This is used to keep track of where we are in the hierarchy when parsing nested components. + "current_path": ["root"] + }; + . as $state + | ( + $item + # Parse a whole line as { name: "...", param: "..." (or null), value: "..." } + | capture("^(?'name'[a-zA-Z0-9-]+)(?:;(?'params'[a-zA-Z0-9-]+=(?:\"[^[:cntrl:]\"]*\"|[^[:cntrl:]\",;:]*)(?:,(?:\"[^[:cntrl:]\"]*\"|[^[:cntrl:]\",;:]*))*(?:;[a-zA-Z0-9-]+=(?:\"[^[:cntrl:]\"]*\"|[^[:cntrl:]\",;:]*)(?:,(?:\"[^[:cntrl:]\"]*\"|[^[:cntrl:]\",;:]*))*)*))?:(?'value'[^[:cntrl:]]*)\r?$") + ) as {$name, $params, $value} + # Property names should be case-insensitive, we will use lowercase everywhere + | ($name | ascii_downcase) as $name + | $state + | if .current_path[0] != "root" then + # If we get any line after an `END:` that was meant for the root component, + # the current_path will be set to []. We should not allow parsing anything else. + error("Unexpected end of root component") + elif getpath([.current_path[], "_type"]) == null then + # When the type was not yet filled in, we are expecting a BEGIN for the root component. + if $name == "begin" then + setpath([.current_path[], "_type"]; $value) + else + error("Expected BEGIN, got \($name)") + end + elif $name == "begin" then + # This BEGIN: declares a nested component. + # When we are somewhere other than the root component, we will never get a `null` type + # because we can set it as soon as we get here; so we know that the above branch only + # runs for the root component and we are always working with nested components. + # We will nest components under an array called `_components`. + # We therefore add to our paths the `_components` key, and then the index of this new + # component. The length of the array matches the last index of the array + 1, so this + # will append our new component at the end of the list. + .current_path += ["_components", ((getpath(.current_path)._components // []) | length)] + # Add the new component now at our new path with the type from the BEGIN:. + | setpath(.current_path; {"_type": $value}) + elif $name == "end" then + # Handle an END by checking that its type matches the type we are working with now, + # and going back up in the structure by removing the array index and the `_components` from the path. + if $value == getpath([.current_path[], "_type"]) then + .current_path |= .[:-2] + else + error("Unexpected end of \($value) component while in a \(getpath([.current_path[], "_type"])) component") + end + else + # This is not any special case, so we will just set a property. + # Since some properties could have multiple values for the same set of parameters + # and multiple sets of parameters for the same name, we structure the output like this: + # { "name": {"parameters": ["value1", "value2"] } } + # When there are no parameters, we will use the "" key. + setpath( + [.current_path[], $name, ($params // "")]; + (getpath([.current_path[], $name, ($params // "")]) // []) + [$value] + ) + end +) +# Return the parsed calendar from our parser's state. +| .root diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..dc55be0 --- /dev/null +++ b/init.sql @@ -0,0 +1,292 @@ +PRAGMA foreign_keys = OFF; +BEGIN TRANSACTION; + +INSERT INTO library (name) VALUES + ('Abbaye-les-Bains'), + ('Alliance'), + ('Arlequin'), + ('Bibliothèque d''étude et du patrimoine'), + ('Kateb Yacine'), + ('Bibliothèque municipale internationale'), + ('Centre-Ville'), + ('Eaux-Claires'), + ('Fonds commun'), + ('Jardin de Ville'), + ('Saint-Bruno'), + ('Tesseire-Malherbe'), + ('Archives municipales'), + ('Musée de Grenoble'); + +INSERT INTO weekdays (dow, name) VALUES + (0, 'Dimanche'), + (1, 'Lundi'), + (2, 'Mardi'), + (3, 'Mercredi'), + (4, 'Jeudi'), + (5, 'Vendredi'), + (6, 'Samedi'); + +INSERT INTO schedule (library, dow, holidays, start, end) VALUES + ('Bibliothèque d''étude et du patrimoine', 2, FALSE, '10:00:00', '19:00:00'), + ('Bibliothèque d''étude et du patrimoine', 3, FALSE, '10:00:00', '19:00:00'), + ('Bibliothèque d''étude et du patrimoine', 4, FALSE, '13:00:00', '19:00:00'), + ('Bibliothèque d''étude et du patrimoine', 5, FALSE, '10:00:00', '19:00:00'), + ('Bibliothèque d''étude et du patrimoine', 6, FALSE, '10:00:00', '18:00:00'), + ('Bibliothèque d''étude et du patrimoine', 2, TRUE, '13:00:00', '18:00:00'), + ('Bibliothèque d''étude et du patrimoine', 3, TRUE, '13:00:00', '18:00:00'), + ('Bibliothèque d''étude et du patrimoine', 4, TRUE, '13:00:00', '18:00:00'), + ('Bibliothèque d''étude et du patrimoine', 5, TRUE, '13:00:00', '18:00:00'), + ('Bibliothèque d''étude et du patrimoine', 6, TRUE, '13:00:00', '18:00:00'), + ('Bibliothèque municipale internationale', 2, FALSE, '17:00:00', '19:00:00'), + ('Bibliothèque municipale internationale', 3, FALSE, '13:00:00', '18:00:00'), + ('Bibliothèque municipale internationale', 4, FALSE, '17:00:00', '19:00:00'), + ('Bibliothèque municipale internationale', 5, FALSE, '17:00:00', '19:00:00'), + ('Bibliothèque municipale internationale', 6, NULL, '10:00:00', '13:00:00'), + ('Bibliothèque municipale internationale', 6, FALSE, '14:00:00', '17:00:00'), + ('Bibliothèque municipale internationale', 2, TRUE, '14:00:00', '18:00:00'), + ('Bibliothèque municipale internationale', 3, TRUE, '14:00:00', '18:00:00'), + ('Bibliothèque municipale internationale', 4, TRUE, '14:00:00', '18:00:00'), + ('Bibliothèque municipale internationale', 5, TRUE, '14:00:00', '18:00:00'), + ('Abbaye-les-Bains', 2, NULL, '13:00:00', '18:30:00'), + ('Abbaye-les-Bains', 3, NULL, '10:00:00', '13:00:00'), + ('Abbaye-les-Bains', 3, NULL, '14:00:00', '18:00:00'), + ('Abbaye-les-Bains', 4, NULL, '09:00:00', '12:00:00'), + ('Abbaye-les-Bains', 5, NULL, '13:00:00', '18:30:00'), + ('Abbaye-les-Bains', 6, NULL, '10:00:00', '13:00:00'), + ('Abbaye-les-Bains', 6, NULL, '14:00:00', '18:00:00'), + ('Archives municipales', 1, NULL, '13:00:00', '17:00:00'), + ('Archives municipales', 2, NULL, '13:00:00', '17:00:00'), + ('Archives municipales', 3, NULL, '13:00:00', '17:00:00'), + ('Archives municipales', 4, NULL, '09:00:00', '12:30:00'), + ('Archives municipales', 5, NULL, '09:00:00', '12:30:00'), + ('Musée de Grenoble', 1, NULL, '14:00:00', '18:00:00'), + ('Musée de Grenoble', 3, NULL, '14:00:00', '18:00:00'), + ('Musée de Grenoble', 4, NULL, '14:00:00', '18:00:00'), + ('Musée de Grenoble', 5, NULL, '14:00:00', '18:00:00'), + ('Kateb Yacine', 2, NULL, '11:00:00', '18:30:00'), + ('Kateb Yacine', 3, NULL, '11:00:00', '18:30:00'), + ('Kateb Yacine', 4, NULL, '11:00:00', '18:30:00'), + ('Kateb Yacine', 5, NULL, '11:00:00', '18:30:00'), + ('Kateb Yacine', 6, NULL, '11:00:00', '18:30:00'), + ('Centre-Ville', 2, NULL, '11:00:00', '18:30:00'), + ('Centre-Ville', 3, NULL, '11:00:00', '18:30:00'), + ('Centre-Ville', 4, NULL, '11:00:00', '18:30:00'), + ('Centre-Ville', 5, NULL, '11:00:00', '18:30:00'), + ('Centre-Ville', 6, NULL, '11:00:00', '18:30:00'), + ('Alliance', 2, NULL, '14:00:00', '19:00:00'), + ('Alliance', 3, NULL, '10:00:00', '13:00:00'), + ('Alliance', 3, NULL, '14:00:00', '18:00:00'), + ('Alliance', 6, NULL, '10:00:00', '13:00:00'), + ('Alliance', 6, NULL, '14:00:00', '18:00:00'), + ('Arlequin', 2, NULL, '13:00:00', '18:00:00'), + ('Arlequin', 3, NULL, '10:00:00', '13:00:00'), + ('Arlequin', 3, NULL, '14:00:00', '18:00:00'), + ('Arlequin', 5, NULL, '13:00:00', '18:00:00'), + ('Arlequin', 4, NULL, '09:00:00', '12:00:00'), + ('Arlequin', 6, NULL, '10:00:00', '13:00:00'), + ('Arlequin', 6, NULL, '14:00:00', '18:00:00'), + ('Eaux-Claires', 2, NULL, '13:00:00', '18:30:00'), + ('Eaux-Claires', 3, NULL, '10:00:00', '13:00:00'), + ('Eaux-Claires', 3, NULL, '14:00:00', '18:00:00'), + ('Eaux-Claires', 4, NULL, '09:00:00', '12:00:00'), + ('Eaux-Claires', 5, NULL, '13:00:00', '18:30:00'), + ('Eaux-Claires', 6, NULL, '10:00:00', '13:00:00'), + ('Eaux-Claires', 6, NULL, '14:00:00', '18:00:00'), + ('Jardin de Ville', 2, NULL, '16:00:00', '18:00:00'), + ('Jardin de Ville', 3, NULL, '10:00:00', '13:00:00'), + ('Jardin de Ville', 3, NULL, '14:00:00', '18:00:00'), + ('Jardin de Ville', 5, NULL, '16:00:00', '18:00:00'), + ('Jardin de Ville', 6, NULL, '10:00:00', '13:00:00'), + ('Jardin de Ville', 6, NULL, '14:00:00', '18:00:00'), + ('Tesseire-Malherbe', 2, NULL, '13:00:00', '18:30:00'), + ('Tesseire-Malherbe', 3, NULL, '10:00:00', '13:00:00'), + ('Tesseire-Malherbe', 3, NULL, '14:00:00', '18:00:00'), + ('Tesseire-Malherbe', 4, NULL, '09:00:00', '12:00:00'), + ('Tesseire-Malherbe', 5, NULL, '13:00:00', '18:30:00'), + ('Tesseire-Malherbe', 6, NULL, '10:00:00', '13:00:00'), + ('Tesseire-Malherbe', 6, NULL, '14:00:00', '18:00:00'), + ('Saint-Bruno', 2, NULL, '13:00:00', '18:30:00'), + ('Saint-Bruno', 3, NULL, '10:00:00', '13:00:00'), + ('Saint-Bruno', 3, NULL, '14:00:00', '18:00:00'), + ('Saint-Bruno', 4, NULL, '09:00:00', '12:00:00'), + ('Saint-Bruno', 5, NULL, '13:00:00', '18:30:00'), + ('Saint-Bruno', 6, NULL, '10:00:00', '13:00:00'), + ('Saint-Bruno', 6, NULL, '14:00:00', '18:00:00'); + +INSERT INTO holidays (start, end, closed) VALUES + ('2017-10-20', '2017-11-05', FALSE), + ('2017-12-22', '2018-01-07', FALSE), + ('2018-01-01', '2018-01-01', TRUE), + ('2018-02-09', '2018-02-25', FALSE), + ('2018-04-02', '2018-04-02', TRUE), + ('2018-04-06', '2018-04-22', FALSE), + ('2018-05-01', '2018-05-01', TRUE), + ('2018-05-08', '2018-05-08', TRUE), + ('2018-05-10', '2018-05-10', TRUE), + ('2018-05-21', '2018-05-21', TRUE), + ('2018-07-06', '2018-09-02', FALSE), + ('2018-07-14', '2018-07-14', TRUE), + ('2018-08-15', '2018-08-15', TRUE), + ('2018-10-19', '2018-11-04', FALSE), + ('2018-11-01', '2018-11-01', TRUE), + ('2018-11-11', '2018-11-11', TRUE), + ('2018-12-21', '2019-01-06', FALSE), + ('2018-12-25', '2018-12-25', TRUE), + ('2019-01-01', '2019-01-01', TRUE), + ('2019-02-15', '2019-03-03', FALSE), + ('2019-04-12', '2019-04-28', FALSE), + ('2019-04-22', '2019-04-22', TRUE), + ('2019-05-01', '2019-05-01', TRUE), + ('2019-05-08', '2019-05-08', TRUE), + ('2019-05-28', '2019-06-02', FALSE), + ('2019-05-30', '2019-05-30', TRUE), + ('2019-06-10', '2019-06-10', TRUE), + ('2019-07-05', '2019-09-01', FALSE), + ('2019-07-14', '2019-07-14', TRUE), + ('2019-08-15', '2019-08-15', TRUE), + ('2019-10-18', '2019-11-03', FALSE), + ('2019-11-01', '2019-11-01', TRUE), + ('2019-11-11', '2019-11-11', TRUE), + ('2019-12-20', '2020-01-05', FALSE), + ('2019-12-25', '2019-12-25', TRUE), + ('2020-01-01', '2020-01-01', TRUE), + ('2020-02-21', '2020-03-08', FALSE), + ('2020-04-13', '2020-04-13', TRUE), + ('2020-04-17', '2020-05-03', FALSE), + ('2020-05-01', '2020-05-01', TRUE), + ('2020-05-08', '2020-05-08', TRUE), + ('2020-05-19', '2020-05-24', FALSE), + ('2020-05-21', '2020-05-21', TRUE), + ('2020-06-01', '2020-06-01', TRUE), + ('2020-07-03', '2020-08-31', FALSE), + ('2020-07-14', '2020-07-14', TRUE), + ('2020-08-15', '2020-08-15', TRUE), + ('2020-10-16', '2020-11-01', FALSE), + ('2020-11-01', '2020-11-01', TRUE), + ('2020-11-11', '2020-11-11', TRUE), + ('2020-12-18', '2021-01-03', FALSE), + ('2020-12-25', '2020-12-25', TRUE), + ('2021-01-01', '2021-01-01', TRUE), + ('2021-02-05', '2021-02-21', FALSE), + ('2021-04-05', '2021-04-05', TRUE), + ('2021-04-09', '2021-04-25', FALSE), + ('2021-05-01', '2021-05-01', TRUE), + ('2021-05-08', '2021-05-08', TRUE), + ('2021-05-12', '2021-05-16', FALSE), + ('2021-05-13', '2021-05-13', TRUE), + ('2021-05-24', '2021-05-24', TRUE), + ('2021-07-05', '2021-09-01', FALSE), + ('2021-07-14', '2021-07-14', TRUE), + ('2021-08-15', '2021-08-15', TRUE), + ('2021-10-22', '2021-11-07', FALSE), + ('2021-11-01', '2021-11-01', TRUE), + ('2021-11-11', '2021-11-11', TRUE), + ('2021-12-17', '2022-01-02', FALSE), + ('2021-12-25', '2021-12-25', TRUE), + ('2022-01-01', '2022-01-01', TRUE), + ('2022-02-11', '2022-02-27', FALSE), + ('2022-04-15', '2022-05-01', FALSE), + ('2022-04-18', '2022-04-18', TRUE), + ('2022-05-01', '2022-05-01', TRUE), + ('2022-05-08', '2022-05-08', TRUE), + ('2022-05-25', '2022-05-27', FALSE), + ('2022-05-26', '2022-05-26', TRUE), + ('2022-06-06', '2022-06-06', TRUE), + ('2022-07-06', '2022-08-31', FALSE), + ('2022-07-14', '2022-07-14', TRUE), + ('2022-08-15', '2022-08-15', TRUE), + ('2022-10-21', '2022-11-06', FALSE), + ('2022-11-01', '2022-11-01', TRUE), + ('2022-11-11', '2022-11-11', TRUE), + ('2022-12-16', '2023-01-02', FALSE), + ('2022-12-25', '2022-12-25', TRUE), + ('2023-01-01', '2023-01-01', TRUE), + ('2023-02-03', '2023-02-19', FALSE), + ('2023-04-07', '2023-04-23', FALSE), + ('2023-04-10', '2023-04-10', TRUE), + ('2023-05-01', '2023-05-01', TRUE), + ('2023-05-08', '2023-05-08', TRUE), + ('2023-05-17', '2023-05-21', FALSE), + ('2023-05-18', '2023-05-18', TRUE), + ('2023-05-29', '2023-05-29', TRUE), + ('2023-07-07', '2023-09-03', FALSE), + ('2023-07-14', '2023-07-14', TRUE), + ('2023-08-15', '2023-08-15', TRUE), + ('2023-10-20', '2023-11-05', FALSE), + ('2023-11-01', '2023-11-01', TRUE), + ('2023-11-11', '2023-11-11', TRUE), + ('2023-12-22', '2024-01-07', FALSE), + ('2023-12-25', '2023-12-25', TRUE), + ('2024-01-01', '2024-01-01', TRUE), + ('2024-02-16', '2024-03-03', FALSE), + ('2024-04-01', '2024-04-01', TRUE), + ('2024-04-12', '2024-04-28', FALSE), + ('2024-05-01', '2024-05-01', TRUE), + ('2024-05-08', '2024-05-08', TRUE), + ('2024-05-09', '2024-05-09', TRUE), + ('2024-05-09', '2024-05-10', FALSE), + ('2024-05-20', '2024-05-20', TRUE), + ('2024-07-05', '2024-09-01', FALSE), + ('2024-07-14', '2024-07-14', TRUE), + ('2024-08-15', '2024-08-15', TRUE), + ('2024-10-18', '2024-11-03', FALSE), + ('2024-11-01', '2024-11-01', TRUE), + ('2024-11-11', '2024-11-11', TRUE), + ('2024-12-20', '2025-01-05', FALSE), + ('2024-12-25', '2024-12-25', TRUE), + ('2025-01-01', '2025-01-01', TRUE), + ('2025-02-21', '2025-03-09', FALSE), + ('2025-04-18', '2025-05-04', FALSE), + ('2025-04-21', '2025-04-21', TRUE), + ('2025-05-01', '2025-05-01', TRUE), + ('2025-05-08', '2025-05-08', TRUE), + ('2025-05-29', '2025-05-29', TRUE), + ('2025-05-29', '2025-05-30', FALSE), + ('2025-06-09', '2025-06-09', TRUE), + ('2025-07-04', '2025-08-31', FALSE), + ('2025-07-14', '2025-07-14', TRUE), + ('2025-08-15', '2025-08-15', TRUE), + ('2025-10-17', '2025-11-02', FALSE), + ('2025-11-01', '2025-11-01', TRUE), + ('2025-11-11', '2025-11-11', TRUE), + ('2025-12-19', '2026-01-04', FALSE), + ('2025-12-25', '2025-12-25', TRUE), + ('2026-01-01', '2026-01-01', TRUE), + ('2026-02-06', '2026-02-22', FALSE), + ('2026-04-03', '2026-04-19', FALSE), + ('2026-04-06', '2026-04-06', TRUE), + ('2026-05-01', '2026-05-01', TRUE), + ('2026-05-08', '2026-05-08', TRUE), + ('2026-05-14', '2026-05-14', TRUE), + ('2026-05-14', '2026-05-15', FALSE), + ('2026-05-25', '2026-05-25', TRUE), + ('2026-07-03', '2026-07-03', FALSE), + ('2026-07-14', '2026-07-14', TRUE), + ('2026-08-15', '2026-08-15', TRUE), + ('2026-11-01', '2026-11-01', TRUE), + ('2026-11-11', '2026-11-11', TRUE), + ('2026-12-25', '2026-12-25', TRUE), + ('2027-01-01', '2027-01-01', TRUE), + ('2027-03-29', '2027-03-29', TRUE), + ('2027-05-01', '2027-05-01', TRUE), + ('2027-05-06', '2027-05-06', TRUE), + ('2027-05-08', '2027-05-08', TRUE), + ('2027-05-17', '2027-05-17', TRUE), + ('2027-07-14', '2027-07-14', TRUE), + ('2027-08-15', '2027-08-15', TRUE), + ('2027-11-01', '2027-11-01', TRUE), + ('2027-11-11', '2027-11-11', TRUE), + ('2027-12-25', '2027-12-25', TRUE), + ('2028-01-01', '2028-01-01', TRUE), + ('2028-04-17', '2028-04-17', TRUE), + ('2028-05-01', '2028-05-01', TRUE), + ('2028-05-08', '2028-05-08', TRUE), + ('2028-05-25', '2028-05-25', TRUE), + ('2028-06-05', '2028-06-05', TRUE), + ('2028-07-14', '2028-07-14', TRUE), + ('2028-08-15', '2028-08-15', TRUE), + ('2028-11-01', '2028-11-01', TRUE), + ('2028-11-11', '2028-11-11', TRUE), + ('2028-12-25', '2028-12-25', TRUE); + +COMMIT; diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..be2881d --- /dev/null +++ b/schema.sql @@ -0,0 +1,60 @@ +BEGIN TRANSACTION; + +CREATE TABLE library ( + name TEXT PRIMARY KEY NOT NULL +); + +CREATE TABLE work ( + ark TEXT NOT NULL PRIMARY KEY + CHECK (ark LIKE 'ark:/%'), + author TEXT NOT NULL, + title TEXT NOT NULL +); + +CREATE TABLE book ( + ark TEXT NOT NULL + REFERENCES work (ark) ON UPDATE CASCADE ON DELETE CASCADE, + library TEXT NOT NULL + REFERENCES library (name) ON UPDATE CASCADE ON DELETE CASCADE, + location TEXT NOT NULL, + dewey TEXT NOT NULL, + borrowable INTEGER NOT NULL + DEFAULT TRUE + CHECK (borrowable IS TRUE OR borrowable IS FALSE), + PRIMARY KEY (ark, library) +); + +CREATE TABLE weekdays ( + dow INTEGER NOT NULL PRIMARY KEY + CHECK (dow BETWEEN 0 AND 6), + name TEXT NOT NULL +); + +CREATE TABLE schedule ( + library TEXT NOT NULL + REFERENCES library (name) ON UPDATE CASCADE ON DELETE CASCADE, + dow INTEGER NOT NULL + REFERENCES weekdays (dow) ON UPDATE CASCADE ON DELETE RESTRICT, + holidays INTEGER + CHECK (holidays IS NULL OR holidays IS TRUE OR holidays IS FALSE), + start TEXT NOT NULL + CHECK (start GLOB '[01][0-9]:[0-5][0-9]:[0-5][0-9]' OR start GLOB '2[0-3]:[0-5][0-9]:[0-5][0-9]'), + end TEXT NOT NULL + CHECK (end GLOB '[01][0-9]:[0-5][0-9]:[0-5][0-9]' OR end GLOB '2[0-3]:[0-5][0-9]:[0-5][0-9]'), + UNIQUE (library, dow, start, end), + CONSTRAINT start_before_end CHECK (julianday(start) < julianday(end)) +); + +CREATE TABLE holidays ( + start TEXT NOT NULL + CHECK (start GLOB '[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]'), + end TEXT NOT NULL + CHECK (end GLOB '[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]'), + closed INTEGER NOT NULL + DEFAULT FALSE + CHECK (closed IS FALSE OR closed IS TRUE), + PRIMARY KEY (start, end), + CONSTRAINT start_before_end CHECK (julianday(start) <= julianday(end)) +); + +COMMIT;