Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems with infinite time range in Rails

On a table with both time range (Postgres type tsrange) and date range (Postgres type daterange) fields...

Saving and then retrieving an unbound/infinite time range from DB causes an error:

object.a_time_range = Time.now.utc..DateTime::Infinity.new
object.save!
object.reload

=> ArgumentError: bad value for range

However, doing the same with date range works fine:

object.a_date_range = Date.today..Date::Infinity.new
object.save!
object.reload

Is there a way to use infinite time ranges in Rails?

ruby: 2.3.0, rails: 4.2.6

(See also ruby - Is there a way to express 'infinite time' - Stack Overflow)

like image 221
Yarin Avatar asked Apr 11 '19 22:04

Yarin


1 Answers

You cannot store Infinity as part of a time range in Rails. I believe this is because Infinity is going to be inserted as a string value and interpreted as a float when pulled out of the native PSQL oid. So, any date range from Date -> Float will not be viable. But you can manage to create your own range with pseudo (1 million years from now) dates or you can just use two separate date fields and interpret them appropriately in the model. Begin date, end date.

In Rails 4.2+, you can store a Float::INFINITY value inside your datetime type. Example.

User.first.update(begin_date: DateTime.now, end_date: 'infinity')
User.first.end_date # => Infinity

However, end_date will not be a valid date. You're just storing the string in the database and you're pulling out a float when your call it.

Here's the actual (Rails 4.2) code that handles that:

module ActiveRecord
  module ConnectionAdapters
    module PostgreSQL
      module OID # :nodoc:
        class DateTime < Type::DateTime # :nodoc:
          include Infinity

          def type_cast_for_database(value)
            if has_precision? && value.acts_like?(:time) && value.year <= 0
              bce_year = format("%04d", -value.year + 1)
              super.sub(/^-?\d+/, bce_year) + " BC"
            else
              super
            end
          end

          def cast_value(value)
            if value.is_a?(::String)
              case value
              when 'infinity' then ::Float::INFINITY
              when '-infinity' then -::Float::INFINITY
              when / BC$/
                astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
                super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
              else
                super
              end
            else
              value
            end
          end
        end
      end
    end
  end
end

Again, you will not be able to do date time comparisons with a float. But, it's probably simple enough to have a special case for these two values -::Float::INFINITY and ::Float::INFINITY

like image 100
Veridian Dynamics Avatar answered Oct 17 '22 01:10

Veridian Dynamics