# frozen-string-literal: true module Sequel @application_timezone = nil @database_timezone = nil @typecast_timezone = nil # Sequel doesn't pay much attention to timezones by default, but you can set it # handle timezones if you want. There are three separate timezone settings, application_timezone, # database_timezone, and typecast_timezone. All three timezones have getter and setter methods. # You can set all three timezones to the same value at once via Sequel.default_timezone=. # # The only timezone values that are supported by default are :utc (convert to UTC), # :local (convert to local time), and +nil+ (don't convert). If you need to # convert to a specific timezone, or need the timezones being used to change based # on the environment (e.g. current user), you need to use the +named_timezones+ extension (and use # +DateTime+ as the +datetime_class+). Sequel also ships with a +thread_local_timezones+ extensions # which allows each thread to have its own timezone values for each of the timezones. module Timezones # The timezone you want the application to use. This is the timezone # that incoming times from the database and typecasting are converted to. attr_reader :application_timezone # The timezone for storage in the database. This is the # timezone to which Sequel will convert timestamps before literalizing them # for storage in the database. It is also the timezone that Sequel will assume # database timestamp values are already in (if they don't include an offset). attr_reader :database_timezone # The timezone that incoming data that Sequel needs to typecast # is assumed to be already in (if they don't include an offset). attr_reader :typecast_timezone %w'application database typecast'.each do |t| class_eval("def #{t}_timezone=(tz); @#{t}_timezone = convert_timezone_setter_arg(tz) end", __FILE__, __LINE__) end # Convert the given +Time+/+DateTime+ object into the database timezone, used when # literalizing objects in an SQL string. def application_to_database_timestamp(v) convert_output_timestamp(v, Sequel.database_timezone) end # Converts the object to the given +output_timezone+. def convert_output_timestamp(v, output_timezone) if output_timezone if v.is_a?(DateTime) case output_timezone when :utc v.new_offset(0) when :local v.new_offset(local_offset_for_datetime(v)) else convert_output_datetime_other(v, output_timezone) end else v.public_send(output_timezone == :utc ? :getutc : :getlocal) end else v end end # Converts the given object from the given input timezone to the # +application_timezone+ using +convert_input_timestamp+ and # +convert_output_timestamp+. def convert_timestamp(v, input_timezone) begin if v.is_a?(Date) && !v.is_a?(DateTime) # Dates handled specially as they are assumed to already be in the application_timezone if datetime_class == DateTime DateTime.civil(v.year, v.month, v.day, 0, 0, 0, application_timezone == :local ? Rational(Time.local(v.year, v.month, v.day).utc_offset, 86400) : 0) else Time.public_send(application_timezone == :utc ? :utc : :local, v.year, v.month, v.day) end else convert_output_timestamp(convert_input_timestamp(v, input_timezone), application_timezone) end rescue InvalidValue raise rescue => e raise convert_exception_class(e, InvalidValue) end end # Convert the given object into an object of Sequel.datetime_class in the # +application_timezone+. Used when converting datetime/timestamp columns # returned by the database. def database_to_application_timestamp(v) convert_timestamp(v, Sequel.database_timezone) end # Sets the database, application, and typecasting timezones to the given timezone. def default_timezone=(tz) self.database_timezone = tz self.application_timezone = tz self.typecast_timezone = tz end # Convert the given object into an object of Sequel.datetime_class in the # +application_timezone+. Used when typecasting values when assigning them # to model datetime attributes. def typecast_to_application_timestamp(v) convert_timestamp(v, Sequel.typecast_timezone) end private # Convert the given +DateTime+ to the given input_timezone, keeping the # same time and just modifying the timezone. def convert_input_datetime_no_offset(v, input_timezone) case input_timezone when :utc, nil v # DateTime assumes UTC if no offset is given when :local offset = local_offset_for_datetime(v) v.new_offset(offset) - offset else convert_input_datetime_other(v, input_timezone) end end # Convert the given +DateTime+ to the given input_timezone that is not supported # by default (i.e. one other than +nil+, :local, or :utc). Raises an +InvalidValue+ by default. # Can be overridden in extensions. def convert_input_datetime_other(v, input_timezone) raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}" end # Converts the object from a +String+, +Array+, +Date+, +DateTime+, or +Time+ into an # instance of Sequel.datetime_class. If given an array or a string that doesn't # contain an offset, assume that the array/string is already in the given +input_timezone+. def convert_input_timestamp(v, input_timezone) case v when String v2 = Sequel.string_to_datetime(v) if !input_timezone || Date._parse(v).has_key?(:offset) v2 else # Correct for potentially wrong offset if string doesn't include offset if v2.is_a?(DateTime) v2 = convert_input_datetime_no_offset(v2, input_timezone) else # Time assumes local time if no offset is given v2 = v2.getutc + v2.utc_offset if input_timezone == :utc end v2 end when Array y, mo, d, h, mi, s, ns, off = v if datetime_class == DateTime s += Rational(ns, 1000000000) if ns if off DateTime.civil(y, mo, d, h, mi, s, off) else convert_input_datetime_no_offset(DateTime.civil(y, mo, d, h, mi, s), input_timezone) end else Time.public_send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0)) end when Hash ary = [:year, :month, :day, :hour, :minute, :second, :nanos].map{|x| (v[x] || v[x.to_s]).to_i} if (offset = (v[:offset] || v['offset'])) ary << offset end convert_input_timestamp(ary, input_timezone) convert_input_timestamp(ary, input_timezone) when Time if datetime_class == DateTime v.to_datetime else v end when DateTime if datetime_class == DateTime v else v.to_time end else raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}" end end # Convert the given +DateTime+ to the given output_timezone that is not supported # by default (i.e. one other than +nil+, :local, or :utc). Raises an +InvalidValue+ by default. # Can be overridden in extensions. def convert_output_datetime_other(v, output_timezone) raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}" end # Convert the timezone setter argument. Returns argument given by default, # exists for easier overriding in extensions. def convert_timezone_setter_arg(tz) tz end # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included. def local_offset_for_datetime(dt) time_offset_to_datetime_offset Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec).utc_offset end # Caches offset conversions to avoid excess Rational math. def time_offset_to_datetime_offset(offset_secs) @local_offsets ||= {} @local_offsets[offset_secs] ||= Rational(offset_secs, 86400) end end extend Timezones end