-
-
Save MaherSaif/17a2f3c8e6d33dcc0ca1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
::ActiveSupport::Duration.new(time.advance(parts) - time, 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 | |
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