drop type if exists opening_hours cascade; create type opening_hours as ( human_readable text, is_24 boolean, is_valid boolean, week_mask bit(10080) ); create or replace function _opening_hours_from_text( txt text ) returns opening_hours as $$ declare oh opening_hours; tmp_oh opening_hours; token text; arr text []; minute int; weekday int; day_mask bit(10080); weekday_mask bit(8); begin txt = trim(txt); oh.human_readable = txt; oh.is_24 = false; oh.is_valid = false; begin if txt = '24/7' then -- 24/7 simplest case ever oh.is_24 = true; oh.is_valid = true; elseif txt not like '%;%' then -- single interval set oh.week_mask = '0' :: bit(10080); day_mask = '0' :: bit(10080); weekday_mask = '0' :: bit(8); for token in (select regexp_split_to_table(txt, '[\s|,]')) loop if token ~ '\d.*' then -- if it starts from digit, it's a time intervals group if token like '%-%' then arr = regexp_split_to_array(token, '[-|:]'); for minute in ( select generate_series( 60 * arr [1] :: int + arr [2] :: int, greatest(-- 14:00-15:00 shouldn't include 15:00 but 14:00-14:00 should include 14:00 (60 * arr [3] :: int + arr [4] :: int) - 1, 60 * arr [1] :: int + arr [2] :: int ) ) ) loop day_mask = set_bit(day_mask, minute, 1); end loop; else raise exception 'not implemented'; end if; else -- it's weekday token = replace( replace(replace(replace(replace(replace(replace(token, 'Mo', '1'), 'Tu', '2'), 'We', '3'), 'Th', '4'), 'Fr', '5'), 'Sa', '6'), 'Su', '7'); if token like '%-%' then arr = regexp_split_to_array(token, '[-]'); for weekday in (select generate_series(arr [1] :: char :: int, arr [2] :: char :: int)) loop weekday_mask = set_bit(weekday_mask, weekday, 1); end loop; else weekday_mask = set_bit(weekday_mask, token :: char :: int, 1); end if; end if; end loop; for weekday in (select generate_series(1, 7)) loop if get_bit(weekday_mask, weekday) then oh.week_mask = oh.week_mask | (day_mask >> (1440 * (weekday - 1))); end if; end loop; oh.is_valid = true; else -- multiple interval set - a combination of singles oh.week_mask = '0' :: bit(10080); oh.is_valid = true; oh.is_24 = false; for token in (select regexp_split_to_table(txt, '[;]')) loop tmp_oh = _opening_hours_from_text(token); oh.is_valid = oh.is_valid and tmp_oh.is_valid; oh.week_mask = oh.week_mask | tmp_oh.week_mask; end loop; end if; exception when others then end; return oh; end; $$ language plpgsql immutable strict parallel safe; drop cast if exists ( text as opening_hours ); create cast ( text as opening_hours ) with function _opening_hours_from_text(text); create function _opening_hours_from_timestamp( ts timestamp ) returns opening_hours as $$ select to_char(ts, 'Dy HH24:MM-HH24:MM') :: text :: opening_hours; $$ language sql immutable strict parallel safe; drop cast if exists ( timestamp as opening_hours ); create cast ( timestamp as opening_hours ) with function _opening_hours_from_timestamp(timestamp); create or replace function overlaps(oh1 opening_hours, oh2 opening_hours) returns boolean as $$ begin if not (oh1.is_valid or oh2.is_valid) then return false; end if; if oh1.is_24 or oh2.is_24 then return true; end if; if (oh1.week_mask & oh2.week_mask) != ('0' :: bit(10080)) then return true; end if; return false; end; $$ language plpgsql immutable strict parallel safe; create or replace function overlaps(oh1 opening_hours, ts timestamp) returns boolean as $$ select oh1.is_24 or overlaps (oh1, ts :: opening_hours); $$ language sql immutable strict parallel safe; create or replace function overlaps(ts timestamp, oh1 opening_hours) returns boolean as $$ select oh1.is_24 or overlaps (oh1, ts :: opening_hours); $$ language sql immutable strict parallel safe; create or replace function contains(oh1 opening_hours, ts timestamp) returns boolean as $$ select oh1.is_24 or overlaps (oh1, ts :: opening_hours); $$ language sql immutable strict parallel safe;