Accounting for DST in Postgres, when selecting scheduled items - sql

I have a Postgres table of clock alarms (not really, but this is analogous, and easier to explain). Alarms are set by users with a 1 hour resolution, and users can be from many different timezones. The alarms are repeating daily. I want to reliably fetch the alarms that are supposed to go off at a particular hour of the day, and I am having problems with daylight saving time. How do I do this in the best way?
Example
Alfred and Lotta both live in Stockholm (+1 hour from UTC, but +2h
when it's DST). Sharon lives in Singapore (+8 hours from UTC, no
DST)
During winter, Alfred sets an alarm for 4 AM. The alarm should go off
at 4 AM local time, all year. During summer, Lotta sets an alarm
for 5 AM. Again, it should go off at 5 AM all year round.
Meanwhile, Sharon has set an alarm for 11 AM.
All of these can be stored in the database as 03:00 UTC.
If I query the database in the winter for alarms that should go off at
03:00 UTC, I want Alfred's and Sharon's alarms. Singapore is now +7h
from Sweden, so 11 AM in Singapore is 4 AM in Sweden. Lotta's alarm
should not go off for another hour.
Conversely, if I query the database in the summer for alarms that
should go off at 03:00 UTC, I want Lotta's and Sharon's alarms.
Singapore is +6h from Sweden now, so 11 AM in Singapore is 5 AM in
Sweden now. Sven's alarm went off an hour ago.
How do I store this, and query the database?
I can change the db schema if necessary. At the moment, we don't adjust for DST at all, and in fact just have an "hour" integer field (which seems dumb, a time field would be better).
It seems I need to store both a UTC time and timezone information, but I don't know how to best achieve this in Postgres. I've found that Postgres has some sort of concept of timezones, but no timezone field type as far as I can tell. Also, I guess I need to do some calculations in SQL to determine how to offset the UTC time in the select, based on the timezone data and the creation date. I'm not great with SQL…
I do want to solve this in Postgres, as there can be a lot of "alarms", and I want to avoid the performance issues that come with fetching all of them into Ruby and filter there. (Yes, this is a Rails app.)

Use timestamp with time zone (timestamptz) for calculations.
Times for alarms can be time [without time zone].
But you have to save the time zone explicitly for every row.
Never use time with time zone (timetz) It's a logically broken type, its use is discouraged by PostgreSQL. The manual:
The type time with time zone is defined by the SQL standard, but the
definition exhibits properties which lead to questionable usefulness.
In most cases, a combination of date, time, timestamp without timezone, and timestamp with time zone should provide a complete
range of date/time functionality required by any application.
Demo setup:
CREATE TABLE alarm(name text, t time, tz text);
INSERT INTO alarm VALUES
('Alfred', '04:00', 'Europe/Stockholm') -- Alfred sets an alarm for 4 AM.
, ('Lotta', '05:00', 'Europe/Stockholm') -- Lotta sets an alarm for 5 AM.
, ('Sharon', '11:00', 'Asia/Singapore'); -- Sharon has set an alarm for 11 AM.
It has to be time zone names (not abbreviations) to account for DST. Related:
Time zone names with identical properties yield different result when applied to timestamp
Get matching alarms for "today":
SELECT *
FROM alarm
WHERE (('2012-07-01'::date + t) AT TIME ZONE tz AT TIME ZONE 'UTC')::time
= '03:00'::time
('2012-7-1'::date + t) ... assemble timestamp [without time zone]
Could also just be now()::date + t for "today".
AT WITH TIME ZONE tz ... place timestamp at the saved time zone, resulting in timestamptz.
AT WITH TIME ZONE 'UTC' ... get according UTC timestamp
::time ... simplest way to extract the time component.
Here you can look up time zone names:
SELECT *
FROM pg_timezone_names
WHERE name ~~* '%sing%'
LIMIT 10;
db<>fiddle here - demonstrating summer / winter
Old sqlfiddle

You would do it by using a full time zone name, e.g. America/New_York rather than EDT/EST, and storing the hour in that time zone not UTC. You can then remain blissfully ignorant of the offset changes for daylight savings.
Something like the following should work:
-- CREATE TABLE time_test (
-- user_to_alert CHARACTER VARYING (30),
-- alarm_hour TIME,
-- user_timezone CHARACTER VARYING (30)
-- );
SELECT user_to_alert,
CASE
WHEN EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour) THEN TRUE
ELSE FALSE
END AS raise_alarm
FROM time_test;
Or:
SELECT user_to_alert
FROM time_test
WHERE EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour);

Given:
SET timezone = 'UTC';
CREATE TABLE tzdemo (
username text not null,
alarm_time_utc time not null,
alarm_tz_abbrev text not null,
alarm_tz text not null
);
INSERT INTO tzdemo (username, alarm_time_utc, alarm_tz_abbrev, alarm_tz) VALUES
('Alfred', TIME '04:00' AT TIME ZONE '+01:00', 'CET', 'Europe/Stockholm'),
('Lotta', TIME '05:00' AT TIME ZONE '+02:00', 'CEST', 'Europe/Stockholm'),
('Sharon', TIME '11:00' AT TIME ZONE '+08:00', 'SGT', 'Singapore');
Try:
SELECT username
FROM tzdemo
WHERE alarm_time_utc AT TIME ZONE alarm_tz_abbrev = TIME '03:00' AT TIME ZONE alarm_tz;
Result:
username
----------
Alfred
Sharon
(2 rows)
Principle:
Store the timezone offset the alarm was created in including whether it was or was not DST at the time
Also store the clock time converted to UTC
When querying, use the fact that full timezone names follow the current UTC rules for times to produce a time that's in the current time zone for the region. Compare to the stored timestamp in what the time zone was when that alarm was created.
This also allows you to cope with cases where the user changes location, and therefore changes timezone.
This approach can be extended by date-qualifying the timestamps when you want to do predictive querying, like "at what local time will alarm sound in location".
I'm not completely confident in this solution and would recommend careful testing.

Related

Convert UTC? timezone to EST in Oracle on select

Our company's Oracle server is hosted in the east coast of the US and I believe follows a default timezone of EST. I have a stored procedure which logs messages but puts the current timestamp into a field declared as timestamp.
describe log_messages;
Name Null? Type
--------- -------- --------------
ENTRY_ID NOT NULL NUMBER
SEVERITY VARCHAR2(1)
DATE_TIME TIMESTAMP(6)
MESSAGE VARCHAR2(2048)
Usually, I do something like
insert into log_messages(severity,date_time,message)
values('I',current_timestamp,'some message');
If I do select * from log_messages, the time stamps look like this:
28-MAY-20 01.50.15.747963000 AM
However, the above time is actually 4 hours later than my current timezone.
select entry_id,severity,cast(date_time as timestamp with time zone) as date_time, date_time AT TIME ZONE 'EST' AS est,message
from log_messages
order by date_time desc;
In the above, it thinks the timestamp in the timestamp field is in EST time, and then when it converts the other, it subtracts 4 hours from it. Effectively, it is as though it writes the time in UTC, but then reads it in thinking it is EST, and subtracts 4 hours from it. How can I get the real time in my local EST time?
CURRENT_TIMESTAMP returns the current date and time in your session time zone, i.e. location or time zone settings of your Oracle Database server is not relevant.
You can interrogate your session time zone with select SESSIONTIMEZONE from dual; and change it with ALTER SESSION SET TIME_ZONE=...
However, as the data type is TIMESTAMP(6), the time zone information is lost as soon as the data is inserted.
When you run cast(date_time as timestamp with time zone) then Oracle takes your SESSIONTIMEZONE for conversion, actually Oracle runs cast(FROM_TZ(date_time, SESSIONTIMEZONE) as timestamp with time zone)
Unless you don't alter your session time zone, the two queries should return the same value..
See also How to handle Day Light Saving in Oracle database

Convert sysdate to BST(British Summer Time)

I have a Java api which gets the data from oracle database which is in ET timezone. I want to query that table using sysdate on 2 columns but the sysdate should be picked as current BST date value not as ET date value.
select * from customers where sysdate between mem_registered_date and mem_deregistered_date;
How this can be done? please help
The simplest way to convert between time zones is to use a data type that understands time zones, a TIMESTAMP. Then when you've converted to the time zone you want then CAST it back to a DATE data type:
SELECT *
FROM customers
WHERE CAST( SYSTIMESTAMP AT TIME ZONE 'Europe/London' AS DATE )
BETWEEN mem_registered_date AND mem_deregistered_date;
I'm assuming that you want the current time in the United Kingdom (BST in summer and GMT in winter), if you want the time zone UTC+1 then use:
CAST( SYSTIMESTAMP AT TIME ZONE '+01:00' AS DATE )
If querying a column with a date-only value, without any time-of-day nor any time zone, that is, a column of a type akin to the SQL-standard DATE type, then use Java class LocalDate.
Generally best to use half-open definition of a span of time, where the beginning is inclusive while the ending is exclusive. This allows spans to nearly abut one another without gaps or overlap. So never use the SQL command BETWEEN for date-time ranges, as it is fully-closed (both beginning and ending are inclusive).
Get today’s date as seen in the wall-clock time used by the people of a particular region (a time zone).
BST is not a real time zone. For British time, use Europe/London.
ZoneId z = ZoneId.of( "Europe/London" ) ;
LocalDate today = LocalDate.now( z ) ;
The SQL will look something like this:
SELECT *
FROM event_
WHERE ? >= start_
AND ? < end_
;
Fill in the placeholders.
myPreparedStatement.setObject( 1 , today ) ;
myPreparedStatement.setObject( 2 , today ) ;
Load the date values from database into Java.
LocalDate start = myResultSet.getObject( … , LocalDate.class ) ;
TIMESTAMP WITHOUT TIME ZONE
The misnamed DATE in the Oracle database actually represents a date with time-of-date without the context of a time zone or offset-from-UTC. As such, this type cannot represent a moment, a specific point on the timeline. It the value is noon on the 23rd of January next year, we cannot know if that is noon in Tokyo, Toulouse, or Toledo — all different moments several hours apart. This DATE type is akin to the SQL-standard type TIMESTAMP WITHOUT TIME ZONE.
So for this data type, your question asking about time zones makes no sense. Apple and oranges. Involving time zones means you are tracking moments, specific points on the timeline. But the Oracle DATE cannot represent moments as discussed above.
To track moments, your should be using a column of a type akin to the SQL-standard TIMESTAMP WITH TIME ZONE.

Informix - Convert datetime to integer

I have values stored as "datetime year to fraction (3)" in UTC which I'd like to offset to the timezone on my server. I'm trying to use the function DBINFO 'utc_to_datetime' to convert to local time, but the second argument is supposed to be an integer, and my values are in datetime. So, how would I convert a datetime to integer?
DBINFO('utc_to_datetime', …) isn't the correct tool
Using:
SELECT DBINFO('utc_to_datetime', 0)
FROM sysmaster:sysdual
yields:
1970-01-01 00:00:00
(at least, it does when the server's time zone is set via TZ=UTC0). Changing that to:
SELECT DBINFO('utc_to_datetime', 1506107444)
FROM sysmaster:sysdual
yields (with the same server running in UTC):
2017-09-22 19:10:44
Changing to the time zone on your server
There is some room to discuss the exact details, but basically what you need to do is add or subtract an INTERVAL representing the time zone to the DATETIME YEAR TO FRACTION(3) values.
For example:
DROP TABLE IF EXISTS datetime_values;
CREATE TABLE datetime_values
(
original DATETIME YEAR TO FRACTION(3) NOT NULL,
modified DATETIME YEAR TO FRACTION(3)
);
INSERT INTO datetime_values(original) VALUES('1970-01-01 00:00:00');
INSERT INTO datetime_values(original) VALUES('2017-09-22 12:31:46');
INSERT INTO datetime_values(original) VALUES('1066-10-21 14:20:04');
INSERT INTO datetime_values(original) VALUES('9989-01-20 00:00:00');
SELECT * FROM datetime_values;
UPDATE datetime_values
SET modified = original - INTERVAL(-5:30) HOUR TO MINUTE;
SELECT * FROM datetime_values;
When run, that gives:
1970-01-01 00:00:00.000
2017-09-22 12:31:46.000
1066-10-14 14:20:04.000
9989-01-20 00:00:00.000
1970-01-01 00:00:00.000 1970-01-01 05:30:00.000
2017-09-22 12:31:46.000 2017-09-22 18:01:46.000
1066-10-14 14:20:04.000 1066-10-14 19:50:04.000
9989-01-20 00:00:00.000 9989-01-20 05:30:00.000
There are multiple ways to represent the time zone offset. What's best may depend on what format you currently have it available in. If there's a table with the value in a column as an INTERVAL HOUR TO MINUTE value, that's probably easiest — it could be a temporary table created for the job. You can handle strings, or two integers (for hours and minutes), or one integer for hours (won't handle time zones for India (UTC+5:30), Nepal (UTC+5:45) or Newfoundland (UTC-4:30).
There's the interesting question of is a time zone offset east of UTC positive or negative: ISO 9945 says positive west, negative east; ISO 8601 (and ISO 9075, SQL) says positive east, negative west. Follow ISO 8601 by choice, but be aware of the issue.
Winter vs Summer Time — Daylight Saving vs Standard Time
My problem with interval is that we are sometimes on CST and sometimes CDT, ie. -6 hours and -5 hours. The server automatically updates its time, so I'd like to use that to my benefit. Can I dynamically choose the interval somehow?
This gets very tricky, very quickly. In some areas of the world, the rules about switching between winter and summer time (I'm going to use that term, though it isn't particularly standard, but then daylight saving isn't standard worldwide except to the extent US software has bludgeoned people into accepting it) change most years, sometimes on a political whim, sometimes because of interactions between observational lunar calendars and events such as Ramadan. The 'Olson Time Zone Database' is hosted by IANA at https://www.iana.org/time-zones (aka https://www.iana.org/tz; this redirects to the longer name). This year, the current release of the database is still 2017b; however, in years past, there were releases such as 2005r, 2006n, 2007k, 2008h, 2009r, 2010n, 2011i, 2012i, 2013i, 2014g, 2015g, 2016j — they're not necessarily the last release in those years, but they are the latest release I have for each of those years. I believe there will be (at least) one more release this year, somewhere close to the end of October. One problem is that there often isn't very much notice of the change of time zone rules.
Ignoring the issue of changing definitions of the switch between winter and summer time in some parts of the world, you have the problem of determining the time zone offset that the server uses. There is a DBINFO('get_tz') call that returns the server's time zone string; it does not, however, tell you the offset from UTC. You could retrieve that string and apply it locally (to code running in a client), but that is indubitably messy.
SELECT DBINFO('utc_current'), DBINFO('get_tz'),
CURRENT YEAR TO SECOND
FROM sysmaster:sysdual
At one point, I ran that query and got:
1506114539 UTC0 2017-09-22 21:08:59
Using GNU date, I could analyze that:
$ /opt/gnu/bin/date -u -d #1506114539
Fri Sep 22 21:08:59 UTC 2017
$ /opt/gnu/bin/date -d #1506114539
Fri Sep 22 14:08:59 PDT 2017
$
This shows that the number of seconds since 'The Epoch' was 1,506,114,539, which translates to 21:08:59 on 22nd September 2017 in the UTC (GMT, more or less) time zone. It was also 14:08:59 in the US/Pacific or America/Los_Angeles time zone, which has the abbreviation PDT (Pacific Daylight Time).
Determining that difference (7 hours during summer or daylight saving time) in the Informix server is tricky. Doubly so when dealing with times in the other time zone offset value, and ambiguously so when dealing with times in the hours between 01:00 and 02:00 on the morning when the clocks 'fall back' (there is no way to tell whether the time value was stored using the winter time zone offset or the summer time zone offset).
I'm going to need to meditate on this — and poke around previous answers, etc.

Insert time with timezone daylight savings

I would like to insert time data type in postgresql that includes the timezone and is aware of daylight savings time. This is what I have done:
CREATE TABLE mytable(
...
start_time time(0) with time zone,
end_time time(0) with time zone
)
INSERT INTO mytable(start_time, end_time)
VALUES(TIME '08:00:00 MST7MDT', TIME '18:00:00 MST7MDT')
I get the following error:
invalid input syntax for type time: "08:00:00 MST7MDT"
It works if I use 'MST' instead of 'MST7MDT', but I need it to be aware of DST. I also tried using 'America/Edmonton' as the timezone, but I got the same error.
What is the proper way to insert a time value (not timestamp) with timezone and DST?
EDIT:
I would actually like to use the 'America/Edmonton' syntax
The proper way is not to use time with time zone (note the space between time and zone) at all, since it is broken by design. It is in the SQL standard, so Postgres supports the type - but advises not to use it. More in this related answer:
Accounting for DST in Postgres, when selecting scheduled items
Since you are having problems with DST, timetz (short name) is a particularly bad choice. It is ill-equipped to deal with DST. It's impossible to tell whether 8:00:00 is in winter or summer time.
Use timestamp with time zone (timstamptz) instead. You can always discard the date part. Simply use start_time::time to get the local time from a timestamptz. Or use AT TIME ZONE to transpose to your time zone.
Generally, to take DST into account automatically, use a time zone name instead of a time zone abbreviation. More explanation in this related question & answer:
Time zone names with identical properties yield different result when applied to timestamp
In your particular case, you could probably use America/Los_Angeles (example with timestamptz):
INSERT INTO mytable(start_time, end_time)
VALUES
('1970-01-01 08:00:00 America/Los_Angeles'
, '1970-01-01 18:00:00 America/Los_Angeles')
I found this by checking:
SELECT * FROM pg_timezone_names
WHERE utc_offset = '-07:00'
AND is_dst;
Basics about time zone handling:
Ignoring time zones altogether in Rails and PostgreSQL
How about this?
INSERT INTO mytable(start_time, end_time)
VALUES('08:00:00'::time at time zone 'MST7MDT', '18:00:00'::time at time zone 'MST7MDT')

Get "time with time zone" from "time without time zone" and the time zone name

First off, I realize time with time zone is not recommended. I am going to use it because I'm comparing multiple time with time zone values to my current system time regardless of day. I.e. a user says start everyday at 08:00 and finish at 12:00 with THEIR time zone, not the system time zone. So, I have a time without time zone column in one table, let's call it SCHEDULES.time and I have a UNIX time zone name column in another table, let's call it USERS.tz.
My system time zone is 'America/Regina', which does not use DST and so the offset is always -06.
Given a time of '12:00:00' and a tz of 'America/Vancouver' I would like to select the data into a column of type time with time zone but I DO NOT want to convert the time to my time zone because the user has effectively said begin at when it is 12:00 in Vancouver, not in Regina.
Thus, doing:
SELECT SCHEDULES.time AT TIME ZONE USERS.tz
FROM SCHEDULES JOIN USERS on USERS.ID=SCHEDULES.USERID;
results (at the moment) in:
'10:00:00-08'
but I really want:
'12:00:00-08'
I can't find any documentation relating to applying a time zone to a time, other then AT TIME ZONE. Is there a way to accomplish this without character manipulation or other hacks?
UPDATE:
This can be accomplished by using string concatenation, casting, and the Postgres time zone view as such:
select ('12:00:00'::text || utc_offset::text)::timetz
from pg_timezone_names
where name = 'America/Vancouver';
However, this is fairly slow. There must be a better way, no?
UPDATE 2:
I apologize for the confusion. The SCHEDULES table DOES NOT use time with time zone, I am trying to SELECT a time with time zone by combining values from a time without time zone and a text time zone name.
UPDATE 3:
Thanks to all those involved for their (heated) discussion. :) I have been convinced to abandon my plan to use a time with time zone for my output and instead use a timestamp with time zone as it performs well, is more readable, and solves another problem that I was going to run into, time zones that roll into new dates. IE. '2011-11-21 23:59' in 'America/Vancouver' is '2011-11-22' in 'America/Regina'.
UPDATE 4:
As I said in my last update, I have chosen the answer that #MichaelKrelin-hacker first proposed and #JonSkeet finalized. That is, a timestamp with time zone as my final output is a better solution. I ended up using a query like:
SELECT timezone(USERS.tz, now()::date + SCHEDULES.time)
FROM SCHEDULES
JOIN USERS ON USERS.ID = SCHEDULES.USERID;
The timezone() format was rewritten by Postgres after I entered (current_date + SCHEDULES.time) AT TIME ZONE USERS.tz into my view.
WARNING: PostgreSQL newbie (see comments on the question!). I know a bit about time zones though, so I know what makes sense to ask.
It looks to me like this is basically an unsupported situation (unfortunately) when it comes to AT TIME ZONE. Looking at the AT TIME ZONE documentation it gives a table where the "input" value types are only:
timestamp without time zone
timestamp with time zone
time with time zone
We're missing the one you want: time without time zone. What you're asking is somewhat logical, although it does depend on the date... as different time zones can have different offsets depending on the date. For example, 12:00:00 Europe/London may mean 12:00:00 UTC, or it may mean 11:00:00 UTC, depending on whether it's winter or summer.
On my system, having set the system time zone to America/Regina, the query
SELECT ('2011-11-22T12:00:00'::TIMESTAMP WITHOUT TIME ZONE)
AT TIME ZONE 'America/Vancouver'
gives me 2011-11-22 14:00:00-06 as a result. That's not ideal, but it does at least give the instant point in time (I think). I believe that if you fetched that with a client library - or compared it with another TIMESTAMP WITH TIME ZONE - you'd get the right result. It's just the text conversion that then uses the system time zone for output.
Would that be good enough for you? Can you either change your SCHEDULES.time field to be a TIMESTAMP WITHOUT TIME ZONE field, or (at query time) combine the time from the field with a date to create a timestamp without time zone?
EDIT: If you're happy with the "current date" it looks like you can just change your query to:
SELECT (current_date + SCHEDULES.time) AT TIME ZONE USERS.tz
from SCHEDULES JOIN USERS on USERS.ID=SCHEDULES.USERID
Of course, the current system date may not be the same as the current date in the local time zone. I think this will fix that part...
SELECT ((current_timestamp AT TIME ZONE USERS.tz)::DATE + schedules.time)
AT TIME ZONE USERS.tz
from SCHEDULES JOIN USERS on USERS.ID=SCHEDULES.USERID
In other words:
Take the current instant
Work out the local date/time in the user's time zone
Take the date of that
Add the schedule time to that date to get a TIMESTAMP WITHOUT TIME ZONE
Use AT TIME ZONE to apply the time zone to that local date/time
I'm sure there's a better way, but I think it makes sense.
You should be aware that in some cases this could fail though:
What do you want the result to be for a time of 01:30 on a day when the clock skips from 01:00 to 02:00, so 01:30 doesn't occur at all?
What do you want the result to be for a time of 01:30 on a day when the clock goes back from 02:00 to 01:00, so 01:30 occurs twice?
Here is a demo how to calculate the times without casting to text:
CREATE TEMP TABLE schedule(t time, tz text);
INSERT INTO schedule values
('12:00:00', 'America/Vancouver')
,('12:00:00', 'US/Mountain')
,('12:00:00', 'America/Regina');
SELECT s.t AT TIME ZONE s.tz
- p.utc_offset
+ EXTRACT (timezone from now()) * interval '1s'
FROM schedule s
JOIN pg_timezone_names p ON s.tz = p.name;
Basically you have to subtract the UTC offset and add the offset of your local time zone to arrive at the given time zone.
You can speed up the calculation by hardcoding your local offset. In your case (America/Regina) that should be:
SELECT s.t AT TIME ZONE s.tz
- p.utc_offset
- interval '6h'
FROM schedule s
JOIN pg_timezone_names p ON s.tz = p.name;
As pg_timezone_names is a view and not actually a system table, it is rather slow - just like the demonstrated variant with casting to text representation and back.
I would store the time zone abbreviations and take the double cast via text without joining in pg_timezone_names for optimum performance.
FAST solution
The culprit that's slowing you down is pg_timezone_names. After some testing I found that pg_timezone_abbrevs is far superior. Of course, you have to save correct time zone abbreviations instead of time zone names to achieve this. Time zone names take DST into consideration automatically, time zone abbreviations are basically just codes for a time offset. The documentation:
A time zone abbreviation, for example PST. Such a specification merely
defines a particular offset from UTC, in contrast to full time zone names
which can imply a set of daylight savings transition-date rules as well.
Have a look at these test results or try yourself:
SELECT * FROM pg_timezone_names;
Total runtime: 541.007 ms
SELECT * FROM pg_timezone_abbrevs;
Total runtime: 0.523 ms
Factor 1000. Whether you go with your idea to cast to text and back to timetz or with my method to compute the time is not important. Both methods are very fast. Just don't use pg_timezone_names.
Actually, as soon as you save time zone abbreviations, you can take the casting route without any additional joins. Use the abbreviation instead of the utc_offset. Results are accurate as per your definition.
CREATE TEMP TABLE schedule(t time, abbrev text);
INSERT INTO schedule values
('12:00:00', 'PST') -- 'America/Vancouver'
,('12:00:00', 'MST') -- 'US/Mountain'
,('12:00:00', 'CST'); -- 'America/Regina'
-- calculating
SELECT s.t AT TIME ZONE s.abbrev
- a.utc_offset
+ EXTRACT (timezone from now()) * interval '1s'
FROM schedule s
JOIN pg_timezone_abbrevs a USING (abbrev);
-- casting (even faster!)
SELECT (t::text || abbrev)::timetz
FROM schedule s;