PostegreSQL Combine columns and convert to timestamp with local time zone - ruby-on-rails

I'm creating a time slot table in Rails with PostegreSQL that contains columns like
slots
name | type
-----|-----
day | date
hour | int
min | int
hour would be like 11, 12, 13, 14 ...
min would be like 0, 5, 10, 15 ...
I'm trying to use these three columns and create a timestamp in order to compare against Time.now to pull records that's upcoming in the future.
Since PG's to_timestamp function creates timestamp with UTC as default timezone, I want to create time from the three columns to use server's timezone and my attempt is below.
Slot.select("
to_timestamp(
concat_ws(
' ',
day::text,
concat_ws(
':',
hour::text,
min::text),
'#{Time.now.zone}'),
'YYYY-MM-DD HH24:MI (TZ)')
AS t")
And it gives me the error:
PG::FeatureNotSupported: ERROR: "TZ"/"tz"/"OF" format patterns are not supported in to_date
Any suggestions or thoughts would be great.
Thanks

The to_timestamp() function returns a timestamp with time zone value. If you do not explicitly specify a time zone, then the time zone of the server is used. That seems to be all that you need, so you can safely forget about specifying anything beyond the simple date and time.
Seeing what you are trying to do, however, it would be much easier to use the make_time() function and add the resulting time to the day date to get the timestamp you need. This saves you lots of conversions to text and then back to a timestamp:
Slot.select("day + make_time(hour, min, 0.0::float) AS t");

Related

Postgres at time zone '+07' syntax [duplicate]

I am running PostgreSQL 9.6.6 on x86_64-pc-linux-gnu and my time zone is set to 'UTC'.
Does anyone know why the results of the following SELECT statements are different?
A)
SELECT timezone('EST', '2017-12-21');
timezone
---------------------
2017-12-20 19:00:00
B)
SELECT timezone('-05', '2017-12-21');
timezone
---------------------
2017-12-21 05:00:00
According to the pg_timezone_names table -05 should have the same offset as EST... Any thoughts? Thanks.
https://www.postgresql.org/docs/current/static/view-pg-timezone-names.html
The view pg_timezone_names provides a list of time zone names that are
recognized by SET TIMEZONE
and further:
utc_offset interval Offset from UTC (positive means east of Greenwich)
when you set timezone to 'EST' - you declare that your client is in EST time zone, thus returned time will be adjusted for your tz:
t=# select '2017-12-21'::timestamptz;
timestamptz
------------------------
2017-12-21 00:00:00-05
(1 row)
the interval match utc_offset from pg_timezone_names and isequal -05, so it works as expected. (indeed in EST will be 5 hours less then UTC) same result if you set timezone to '-05'.
Both -05 and EST give same result for SET TIMEZONE as described in docs.
Now you answer reconciles with docs on using interval: https://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-ZONECONVERT
In these expressions, the desired time zone zone can be specified
either as a text string (e.g., 'PST') or as an interval (e.g.,
INTERVAL '-08:00').
following these rules it works as well:
t=# select '2017-12-21'::timestamptz at time zone 'EST';
timezone
---------------------
2017-12-20 19:00:00
(1 row)
t=# select '2017-12-21'::timestamptz at time zone interval '-05:00';
timezone
---------------------
2017-12-20 19:00:00
(1 row)
but further, docs say:
In the text case, a time zone name can be specified in any of the ways
described in Section 8.5.3.
which is https://www.postgresql.org/docs/current/static/datatype-datetime.html#DATATYPE-TIMEZONES
PostgreSQL allows you to specify time zones in three different forms:
recognized time zone names are listed in the pg_timezone_names
recognized abbreviations are listed in the pg_timezone_abbrevs
POSIX-style time zone specifications of the form STDoffset or STDoffsetDST
(formatting mine)
and lastly:
One should be wary that the POSIX-style time zone feature can lead to
silently accepting bogus input...Another issue to keep in mind is that
in POSIX time zone names, positive offsets are used for locations west
of Greenwich. Everywhere else, PostgreSQL follows the ISO-8601
convention that positive timezone offsets are east of Greenwich.
TL;DR
So in short - when you define '-05' as text (not interval) input for timezone() function or AT TIME ZONE directive (effectively same) Postgres thinks this is an attempt to use POSIX style time zone and thus inverts sign, thus you get "opposite" result...
a simple demonstration of this documented inversion:
t=# select '2017-12-21'::timestamptz at time zone '05';
timezone
---------------------
2017-12-20 19:00:00
(1 row)
Okay I think I found an answer to my own question:
According to the PostgreSQL docs, section 9.9.3 at the following link https://www.postgresql.org/docs/9.6/static/functions-datetime.html
In these expressions, the desired time zone zone can be specified either as a text string (e.g., 'PST') or as an interval (e.g., INTERVAL '-08:00'). In the text case, a time zone name can be specified in any of the ways described in Section 8.5.3.
So using the INTERVAL syntax, the following appears to work:
SELECT timezone(INTERVAL '-05:00', '2017-12-21');
timezone
---------------------
2017-12-20 19:00:00
I think it is still curious, what exactly SELECT timezone('-05', '2017-12-21'); means, as the following also provides the expected result (with the addition of a TZ offset):
SELECT timezone('-05', '2017-12-21'::timestamp);
timezone
------------------------
2017-12-20 19:00:00+00

Get records created after a particular time of day

Say I have an Event model with a date_time field representing the date time the event is held, and I want to see all Events that are held, say, 'after 10pm', or 'before 7am' across multiple dates. How could I do this?
My first thought was something like this:
scope :after_time ->(time){ where("events.date_time::time between ?::time and '23:59'::time", time) }
But this doesn't work because dates are stored in UTC and converted to the app's timezone by ActiveRecord.
So let's say I'm searching for Events after 5pm, from my local Adelaide time. The eventual query is this:
WHERE (events.date_time::time between '2016-10-09 06:30:00.000000'::time and '23:59'::time)
That is, because my timezone is +10:30 (Adelaide time), it's now trying to calculate between 6:30am and midnight, where it really needs to be finding ones created between 6:30am and 1:30pm utc.
Now, for this example in particular I could probably hack something together to work out what the 'midnight' time needs to be given the time zone difference. But the between <given time> and <midnight in Adelaide> calculation isn't going to work if that period spans midnight utc. So that solution is bust.
UPDATE:
I think I've managed to get the result I want by trial and error, but I'm not sure I understand exactly what's going on.
scope :after_time, ->(time) {
time = time.strftime('%H:%M:%S')
where_clause = <<-SQL
(events.date_time at time zone 'UTC' at time zone 'ACDT')::time
between ? and '23:59:59'
SQL
joins(:performances).where(where_clause, time)
}
It's basically turning everything into the one time zone so the query for each row ends up looking something like WHERE '20:30:00' between '17:00:00' and '23:59:59', so I'm not having to worry about times spanning over midnight.
Even still, I feel like there's probably a proper way to do this, so I'm open to suggestions.
Check if this works for you,
s = DateTime.now.change(hour: 6, min: 30).utc
e = Date.today.end_of_day.utc
Event.where("date_time::time between ?::time and ?::time", s, e)
this may help you and then you need not to convert every date of DB, instead you can convert the parameterized timestamp into UTC time:
scope :after, ->(start_time) { where('created_at::time > :time', time: start_time.utc.strftime('%H:%M:%S')) }
Now,
for e.g. I do have 3 events for following timestamps(all in UTC):
2013-04-11 11:43:43
2013-04-11 15:10:40
2013-04-12 07:39:26
and then you can call:
start_time = Time.zone.parse('2016-01-01 20:00:00')
# => Fri, 01 Jan 2016 20:00:00 ACDT +10:30
Event.after(start_time) # this will return 2 events(1, 2)
query will be:
SELECT "events".* FROM "events" WHERE (created_at::time > '09:30:00')
Note: This will raise an error ActiveRecord::StatementInvalid: PG::AmbiguousColumn: ERROR: column reference "created_at" is ambiguous if you will use this query with any another model that will have created_at column

Group by date from a specific time to time in Rails

I'm trying to figure out if it's possible to group data by date from a specific time of a date to a time in next date.
I've a bills table in Postgresql database with the following columns:
id serial NOT NULL,
bill_amount double precision,
tax_amount double precision,
discount double precision,
grand_total double precision,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
I want group by clause like from date 2015-09-12 06:00:00.00000 to date 2015-09-13 06:00:00.00000, means all records less than 2015-09-13 06:00:00.00000 and greater than 2015-09-12 06:00:00.00000 will be considered as data of 12th September. So if I want to get the data of a month with group by date, is it possible?
In Postgresql you could use this as your grouping expression:
date_trunc('day',created_at-interval '6h')
(assuming created_at is the field you are wanting to group by).
I am no Rails expert, but glancing through the documentation I formed the opinion you might be able to get it to issue such SQL with something like
Bills.select("date_trunc('day',created_at-interval '6h') as day")
.group("date_trunc('day',created_at-interval '6h')")
Incidentally, I am guessing the reason you want the days to run from 06:00 to 06:00 is that the timezones of the date columns are not aligned - you would be better off to either ensure that the data stored in these columns is aligned so that days run from 00:00 - 00:00, or to use timestamp with timezone as the type of these columns if the table will store data from multiple time zones.

rails group by utc date

I have a time field in table "timestamp without time zone". When record is saved to database, the utc time might be a different day compared to the local time. However, I need to group the records by date. Hence, I am doing something like this:
result = transmissions.joins(:report).where('reports.time::timestamp::date = ?', record.time.to_date)
The problem is if the utc date is on a different date than local time, then that record is not included in result. Any ideas how to get the right result?
And apparently I cannot change the "without time zone" either:
Rails database-specific data type
It says:
"concluded that the default ActiveRecord datetime and timestamp column types in schema migrations cannot be modified to force PostgreSQL to use timestamp with time zone."
So I have no idea how to group by date, as obviously something like this is wrong:
Unit.where(id: 1100).first.reports.order("DATE(time)").group("DATE(time)").count
=> {"2013-12-14"=>19, "2013-12-15"=>5}
That return value is completely wrong. All 25 records should be on 2013-12-14 and 0 records on 2013-12-15.
Assuming your records are timestamped with a particular UTC offset, you can try passing in the start and end times of the date in question in UTC format to your query:
result = transmissions.joins(:report).where('reports.time >= ? AND reports.time < ?', record.time.midnight.utc, (record.time.midnight + 1.day).utc)
Explanation:
midnight is a Rails method on an instance of Time that returns the Time object that represents midnight on the date of the original Time object. Similarly, record.time.midnight + 1.day returns the Time object representing midnight of the following day. Then, converting both Time objects – which are presumably timestamped in a standard UTC offset – to UTC creates a time period representing midnight-to-midnight for the system timezone in UTC format (not midnight in UTC time), which is precisely what you're seeking to query.
How about something like result = transmissions.joins(:report).where('reports.time >= ? AND reports.time <= ?', record.time.beginning_of_day.utc, record.time.end_of_day.utc)
The .utc part may not be necessary.

Store the day of the week and time?

I have a two-part question about storing days of the week and time in a database. I'm using Rails 4.0, Ruby 2.0.0, and Postgres.
I have certain events, and those events have a schedule. For the event "Skydiving", for example, I might have Tuesday and Wednesday and 3 pm.
Is there a way for me to store the record for Tuesday and Wednesday in one row or should I have two records?
What is the best way to store the day and time? Is there a way to store day of week and time (not datetime) or should these be separate columns? If they should be separate, how would I store the day of the week? I was thinking of storing them as integer values, 0 for Sunday, 1 for Monday, since that's how the wday method for the Time class does it.
Any suggestions would be super helpful.
Is there a way for me to store the the record for Tuesday and
Wednesday in one row or do should I have two records?
There are several ways to store multiple time ranges in a single row. #bma already provided a couple of them. That might be useful to save disk space with very simple time patterns. The clean, flexible and "normalized" approach is to store one row per time range.
What is the best way to store the day and time?
Use a timestamp (or timestamptz if multiple time zones may be involved). Pick an arbitrary "staging" week and just ignore the date part while using the day and time aspect of the timestamp. Simplest and fastest in my experience, and all date and time related sanity-checks are built-in automatically. I use a range starting with 1996-01-01 00:00 for several similar applications for two reasons:
The first 7 days of the week coincide with the day of the month (for sun = 7).
It's the most recent leap year (providing Feb. 29 for yearly patterns) at the same time.
Range type
Since you are actually dealing with time ranges (not just "day and time") I suggest to use the built-in range type tsrange (or tstzrange). A major advantage: you can use the arsenal of built-in Range Functions and Operators. Requires Postgres 9.2 or later.
For instance, you can have an exclusion constraint building on that (implemented internally by way of a fully functional GiST index that may provide additional benefit), to rule out overlapping time ranges. Consider this related answer for details:
Preventing adjacent/overlapping entries with EXCLUDE in PostgreSQL
For this particular exclusion constraint (no overlapping ranges per event), you need to include the integer column event_id in the constraint, so you need to install the additional module btree_gist. Install once per database with:
CREATE EXTENSION btree_gist; -- once per db
Or you can have one simple CHECK constraint to restrict the allowed time period using the "range is contained by" operator <#.
Could look like this:
CREATE TABLE event (event_id serial PRIMARY KEY, ...);
CREATE TABLE schedule (
event_id integer NOT NULL REFERENCES event(event_id)
ON DELETE CASCADE ON UPDATE CASCADE
, t_range tsrange
, PRIMARY KEY (event_id, t_range)
, CHECK (t_range <# '[1996-01-01 00:00, 1996-01-09 00:00)') -- restrict period
, EXCLUDE USING gist (event_id WITH =, t_range WITH &&) -- disallow overlap
);
For a weekly schedule use the first seven days, Mon-Sun, or whatever suits you. Monthly or yearly schedules in a similar fashion.
How to extract day of week, time, etc?
#CDub provided a module to deal with it on the Ruby end. I can't comment on that, but you can do everything in Postgres as well, with impeccable performance.
SELECT ts::time AS t_time -- get the time (practically no cost)
SELECT EXTRACT(DOW FROM ts) AS dow -- get day of week (very cheap)
Or in similar fashion for range types:
SELECT EXTRACT(DOW FROM lower(t_range)) AS dow_from -- day of week lower bound
, EXTRACT(DOW FROM upper(t_range)) AS dow_to -- same for upper
, lower(t_range)::time AS time_from -- start time
, upper(t_range)::time AS time_to -- end time
FROM schedule;
db<>fiddle here
Old sqliddle
ISODOW instead of DOW for EXTRACT() returns 7 instead of 0 for sundays. There is a long list of what you can extract.
This related answer demonstrates how to use range type operator to compute a total duration for time ranges (last chapter):
Calculate working hours between 2 dates in PostgreSQL
Check out the ice_cube gem (link).
It can create a schedule object for you which you can persist to your database. You need not create two separate records. For the second part, you can create schedule based on any rule and you need not worry on how that will be saved in the database. You can use the methods provided by the gem to get whatever information you want from the persisted schedule object.
Depending how complex your scheduling needs are, you might want to have a look at RFC 5545, the iCalendar scheduling data format, for ideas on how to store the data.
If you needs are pretty simple, than that is probably overkill. Postgresql has many functions to convert date and time to whatever format you need.
For a simple way to store relative dates and times, you could store the day of week as an integer as you suggested, and the time as a TIME datatype. If you can have multiple days of the week that are valid, you might want to use an ARRAY.
Eg.
ARRAY[2,3]::INTEGER[] = Tues, Wed as Day of Week
'15:00:00'::TIME = 3pm
[EDIT: Add some simple examples]
/* Custom the time and timetz range types */
CREATE TYPE timerange AS RANGE (subtype = time);
--drop table if exists schedule;
create table schedule (
event_id integer not null, /* should be an FK to "events" table */
day_of_week integer[],
time_of_day time,
time_range timerange,
recurring text CHECK (recurring IN ('DAILY','WEEKLY','MONTHLY','YEARLY'))
);
insert into schedule (event_id, day_of_week, time_of_day, time_range, recurring)
values
(1, ARRAY[1,2,3,4,5]::INTEGER[], '15:00:00'::TIME, NULL, 'WEEKLY'),
(2, ARRAY[6,0]::INTEGER[], NULL, '(08:00:00,17:00:00]'::timerange, 'WEEKLY');
select * from schedule;
event_id | day_of_week | time_of_day | time_range | recurring
----------+-------------+-------------+---------------------+-----------
1 | {1,2,3,4,5} | 15:00:00 | | WEEKLY
2 | {6,0} | | (08:00:00,17:00:00] | WEEKLY
The first entry could be read as: the event is valid at 3pm Mon - Fri, with this schedule occurring every week.
The second entry could be read as: the event is valid Saturday and Sunday between 8am and 5pm, occurring every week.
The custom range type "timerange" is used to denote the lower and upper boundaries of your time range.
The '(' means "inclusive", and the trailing ']' means "exclusive", or in other words "greater than or equal to 8am and less than 5pm".
Why not just store the datestamp then use the built in functionality for Date to get the day of the week?
2.0.0p247 :139 > Date.today
=> Sun, 10 Nov 2013
2.0.0p247 :140 > Date.today.strftime("%A")
=> "Sunday"
strftime sounds like it can do everything for you. Here are the specific docs for it.
Specifically for what you're talking about, it sounds like you'd need an Event table that has_many :schedules, where a Schedule would have a start_date timestamp...

Resources