Skip to content

Instantly share code, notes, and snippets.

@shartep
Forked from MaherSaif/pg_interval_support.rb
Last active December 7, 2016 12:29
Show Gist options
  • Save shartep/f279d7b80cdd971925e889bedb5ca510 to your computer and use it in GitHub Desktop.
Save shartep/f279d7b80cdd971925e889bedb5ca510 to your computer and use it in GitHub Desktop.
require 'active_support/duration'
# add a native DB type of :interval
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:interval] = { name: 'interval' }
# add the interval type to the simplified_type list. because this method is a case statement
# we can't inject anything into it, so we create an alias around it so calls to it will call
# our aliased method first, which (if appropriate) will return our type, otherwise passing
# it along to the original unaliased method (which has the normal case statement)
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.class_eval do
define_method("simplified_type_with_interval") do |field_type|
if field_type == 'interval'
:interval
else
send("simplified_type_without_interval", field_type)
end
end
alias_method_chain :simplified_type, 'interval'
end
# add a table definition for migrations, so rails will create 'interval' columns
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::TableDefinition.class_eval do
define_method('interval') do |*args|
options = args.extract_options!
column(args[0], 'interval', options)
end
end
# add a table definition for migrations, so rails will create 'interval' columns
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Table.class_eval do
define_method('interval') do |*args|
options = args.extract_options!
column(args[0], 'interval', options)
end
end
# make sure activerecord treats :intervals as 'text'. This won't provide any help with
# dealing with them, but we can handle that ourselves
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.alias_type 'interval', 'text'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
module OID # :nodoc:
class Interval < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Type # :nodoc:
# Converts PostgreSQL interval value (in +postgres+ format) to ActiveSupport::Duration
def type_cast(value)
return value if value.is_a?(::ActiveSupport::Duration)
time = ::Time.now
if value.kind_of?(::Numeric)
return ::ActiveSupport::Duration.new(time.advance(seconds: value) - time, seconds: value)
end
regex = / # Matches postgrs format: -1 year -2 mons +3 days -04:05:06
(?:(?<years>[\+\-]?\d+)\syear[s]?)?\s* # year part, like +3 years+
(?:(?<months>[\+\-]?\d+)\smon[s]?)?\s* # month part, like +2 mons+
(?:(?<days>[\+\-]?\d+)\sday[s]?)?\s* # day part, like +5 days+
(?:
(?<timesign>[\+\-])?
(?<hours>\d+):(?<minutes>\d+)(?::(?<seconds>\d+))?
)? # time part, like -00:00:00
/x
results = regex.match(value)
parts = {}
%i(years months days).each do |param|
next unless results[param]
parts[param] = results[param].to_i
end
%i(minutes seconds).each do |param|
next unless results[param]
parts[param] = "#{results[:timesign]}#{results[param]}".to_i
end
# As hours isn't part of Duration, convert it to seconds
if results[:hours]
parts[:minutes] ||= 0
parts[:minutes] += "#{results[:timesign]}#{results[:hours]}".to_i * 60
end
# we need this two lines as advance mothod modify parts attribute,
# it add :hours key into it and current Rails version of Duration class not work with :hours, it use :minutes instead
#
# Example: if we set interval column to '2 year 3 mons 8 days 12:04:35' after advance method we get
# parts == {:years=>2, :months=>3, :days=>8, :minutes=>724, :seconds=>35, :hours=>0}
value = time.advance(parts) - time
parts.delete(:hours)
::ActiveSupport::Duration.new(value, parts)
end
end
end
end
end
end
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.register_type 'interval', ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Interval.new
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ColumnMethods
def interval(name, options = {})
column(name, :interval, options)
end
end
end
end
end
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
module Quoting
alias_method :type_cast_without_interval, :type_cast
# Converts ActiveSupport::Duration to PostgreSQL interval value (in +Tradition PostgreSQL+ format)
def type_cast(value, column, array_member = false)
return super(value, column) unless column
case value
when ::ActiveSupport::Duration
if 'interval' == column.sql_type
value.parts.
reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
join ' '
else
super(value, column)
end
else
type_cast_without_interval(value, column, array_member)
end
end
end
end
end
end
module ActiveSupport
class Duration < ProxyObject
# number of seconds not included in minutes
# Example:
# model.interval_field = '2 year 3 mons 8 days 12:04:35'
# model.interval_field.full_seconds => 35
#
# model.interval_field = '2 year 3 mons 8 days 12:04:75'
# model.interval_field.full_seconds => 15
def full_seconds
((value.to_i % 3600) % 60)
end
# number of full minutes not included in hours with minutes form exceed seconds
# Example:
# model.interval_field = '2 year 3 mons 8 days 12:04:35'
# model.interval_field.full_minutes => 04
#
# model.interval_field = '2 year 3 mons 8 days 12:04:75'
# model.interval_field.full_minutes => 5
def full_minutes
(value.to_i % 3600) / 60
end
# number of full hours not included in days with hours form exceed minutes
# Example:
# model.interval_field = '2 year 3 mons 8 days 12:04:35'
# model.interval_field.full_hours => 12
#
# model.interval_field = '2 year 3 mons 8 days 12:59:75'
# model.interval_field.full_hours => 13
# model.interval_field = '2 year 3 mons 8 days 12:64:75'
# model.interval_field.full_hours => 13
def full_hours
(value.to_i / 3600) % 24
end
# total number of full hours (without minutes and seconds) in interval
# Example:
# model.interval_field = '2 year 3 mons 8 days 12:04:35'
# model.interval_field.total_hours => 19884
def total_hours
value.to_i / 3600
end
# total number of full minutes (without seconds) in interval
# Example:
# model.interval_field = '2 year 3 mons 8 days 12:04:35'
# model.interval_field.full_minutes => 19884
def total_minutes
total_hours * 60 + full_minutes
end
def to_time
hours = value.to_i / 3600
minutes = (value.to_i % 3600) / 60
seconds = ((value.to_i % 3600) % 60)
'%02d:%02d:%02d' % [hours, minutes, seconds]
end
def iso8601(n=nil)
# First, trying to normalize duration parts
parts = ::Hash.new(0)
self.parts.each {|k,v| parts[k] += v }
if parts[:seconds] >= 60
parts[:hours] += parts[:seconds].to_i / 3600
parts[:minutes] += (parts[:seconds].to_i % 3600) / 60
parts[:seconds] = parts[:seconds] % 60
end
if parts[:minutes] >= 60
parts[:hours] += parts[:minutes] / 60
parts[:minutes] = parts[:minutes] % 60
end
if parts[:hours] >= 24
parts[:days] += parts[:hours] / 24
parts[:hours] = parts[:hours] % 24
end
# Build ISO 8601 string parts
years = "#{parts[:years]}Y" if parts[:years].nonzero?
months = "#{parts[:months]}M" if parts[:months].nonzero?
days = "#{parts[:days]}D" if parts[:days].nonzero?
date = "#{years}#{months}#{days}"
hours = "#{parts[:hours]}H" if parts[:hours].nonzero?
minutes = "#{parts[:minutes]}M" if parts[:minutes].nonzero?
if parts[:seconds].nonzero?
sf = parts[:seconds].is_a?(::Float) ? '0.0f' : 'd'
seconds = "#{sprintf(n ? "%0.0#{n}f" : "%#{sf}", parts[:seconds])}S"
end
time = "T#{hours}#{minutes}#{seconds}" if hours || minutes || seconds
"P#{date}#{time}"
end
alias_method :to_s, :iso8601
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment