require 'date' class OCI8 module BindType module Util # :nodoc: @@datetime_fsec_base = (1 / ::DateTime.parse('0001-01-01 00:00:00.000000001').sec_fraction).to_i @@time_offset = ::Time.now.utc_offset @@datetime_offset = ::DateTime.now.offset @@default_timezone = :local def self.default_timezone @@default_timezone end # Determines default timezone of Time and DateTime retrived from Oracle. # This accepts :local or :utc. The default is :local. # # This parameter is used when both or either of Oracle server and client # version is Oracle 8i or lower. If both versions are Oracle 9i or upper, # the default timezone is determined by the session timezone. def self.default_timezone=(tz) if tz != :local and tz != :utc raise ArgumentError, "expected :local or :utc but #{tz}" end @@default_timezone = tz end private def datetime_to_array(val, full) # year year = val.year # month if val.respond_to? :mon month = val.mon elsif val.respond_to? :month month = val.month else raise "expect Time, Date or DateTime but #{val.class}" end # day if val.respond_to? :mday day = val.mday elsif val.respond_to? :day day = val.day else raise "expect Time, Date or DateTime but #{val.class}" end # hour if val.respond_to? :hour hour = val.hour else hour = 0 end # minute if val.respond_to? :min minute = val.min else minute = 0 end # second if val.respond_to? :sec sec = val.sec else sec = 0 end return [year, month, day, hour, minute, sec] unless full # sec_fraction if val.respond_to? :sec_fraction fsec = (val.sec_fraction * @@datetime_fsec_base).to_i else fsec = 0 end # time zone if val.respond_to? :offset # DateTime tz_min = (val.offset * 1440).to_i elsif val.respond_to? :utc_offset # Time tz_min = val.utc_offset / 60 else tz_hour = nil tz_min = nil end if tz_min if tz_min < 0 tz_min = - tz_min tz_hour = - (tz_min / 60) tz_min = (tz_min % 60) else tz_hour = tz_min / 60 tz_min = tz_min % 60 end end [year, month, day, hour, minute, sec, fsec, tz_hour, tz_min] end def ocidate_to_datetime(ary) year, month, day, hour, minute, sec = ary if @@default_timezone == :local offset = @@datetime_offset else offset = 0 end ::DateTime.civil(year, month, day, hour, minute, sec, offset) end def ocidate_to_time(ary) year, month, day, hour, minute, sec = ary if year >= 139 begin return ::Time.send(@@default_timezone, year, month, day, hour, minute, sec) rescue StandardError end end ocidate_to_datetime(ary) end if OCI8.oracle_client_version >= ORAVER_9_0 def ocitimestamp_to_datetime(ary) year, month, day, hour, minute, sec, fsec, tz_hour, tz_min = ary if sec >= 59 and fsec != 0 # convert to a DateTime via a String as a last resort. if tz_hour >= 0 && tz_min >= 0 sign = ?+ else sign = ?- tz_hour = - tz_hour tz_min = - tz_min end time_str = format("%04d-%02d-%02dT%02d:%02d:%02d.%09d%c%02d:%02d", year, month, day, hour, minute, sec, fsec, sign, tz_hour, tz_min) ::DateTime.parse(time_str) else sec += fsec.to_r / 1000000000 offset = tz_hour.to_r / 24 + tz_min.to_r / 1440 ::DateTime.civil(year, month, day, hour, minute, sec, offset) end end def ocitimestamp_to_time(ary) year, month, day, hour, minute, sec, fsec, tz_hour, tz_min = ary if tz_hour == 0 and tz_min == 0 timezone = :utc elsif @@time_offset == tz_hour * 3600 + tz_min * 60 timezone = :local end if timezone and year >= 139 begin # Ruby 1.9 Time class's resolution is nanosecond. # But the last argument type is millisecond. # 'fsec' is converted to a Float to pass sub-millisecond part. return ::Time.send(timezone, year, month, day, hour, minute, sec, fsec / 1000.0) rescue StandardError end end ocitimestamp_to_datetime(ary) end end end class DateTimeViaOCIDate < OCI8::BindType::OCIDate include OCI8::BindType::Util def set(val) # :nodoc: val &&= datetime_to_array(val, false) super(val) end def get() # :nodoc: val = super() val ? ocidate_to_datetime(val) : nil end end class TimeViaOCIDate < OCI8::BindType::OCIDate include OCI8::BindType::Util def set(val) # :nodoc: val &&= datetime_to_array(val, false) super(val) end def get() # :nodoc: val = super() val ? ocidate_to_time(val) : nil end end if OCI8.oracle_client_version >= ORAVER_9_0 class DateTimeViaOCITimestamp < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: val &&= datetime_to_array(val, true) super(val) end def get() # :nodoc: val = super() val ? ocitimestamp_to_datetime(val) : nil end end class TimeViaOCITimestamp < OCI8::BindType::OCITimestamp include OCI8::BindType::Util def set(val) # :nodoc: val &&= datetime_to_array(val, true) super(val) end def get() # :nodoc: val = super() val ? ocitimestamp_to_time(val) : nil end end end #-- # OCI8::BindType::DateTime #++ # This is a helper class to bind ruby's # DateTime[http://www.ruby-doc.org/core/classes/DateTime.html] # object as Oracle's TIMESTAMP WITH TIME ZONE datatype. # # == Select # # The fetched value for a DATE, TIMESTAMP, TIMESTAMP WITH # TIME ZONE or TIMESTAMP WITH LOCAL TIME ZONE column # is a DateTime[http://www.ruby-doc.org/core/classes/DateTime.html]. # The time zone part is a session time zone if the Oracle datatype doesn't # have time zone information. The session time zone is the client machine's # time zone by default. # # You can change the session time zone by executing the following SQL. # # ALTER SESSION SET TIME_ZONE='-05:00' # # == Bind # # To bind a DateTime[http://www.ruby-doc.org/core/classes/DateTime.html] # value implicitly: # # conn.exec("INSERT INTO lunar_landings(ship_name, landing_time) VALUES(:1, :2)", # 'Apollo 11', # DateTime.parse('1969-7-20 20:17:40 00:00')) # # The bind variable :2 is bound as TIMESTAMP WITH TIME ZONE on Oracle. # # To bind explicitly: # # cursor = conn.exec("INSERT INTO lunar_landings(ship_name, landing_time) VALUES(:1, :2)") # cursor.bind_param(':1', nil, String, 60) # cursor.bind_param(':2', nil, DateTime) # [['Apollo 11', DateTime.parse('1969-07-20 20:17:40 00:00'))], # ['Apollo 12', DateTime.parse('1969-11-19 06:54:35 00:00'))], # ['Apollo 14', DateTime.parse('1971-02-05 09:18:11 00:00'))], # ['Apollo 15', DateTime.parse('1971-07-30 22:16:29 00:00'))], # ['Apollo 16', DateTime.parse('1972-04-21 02:23:35 00:00'))], # ['Apollo 17', DateTime.parse('1972-12-11 19:54:57 00:00'))] # ].each do |ship_name, landing_time| # cursor[':1'] = ship_name # cursor[':2'] = landing_time # cursor.exec # end # cursor.close # # On setting a object to the bind variable, you can use any object # which has at least three instance methods _year_, _mon_ (or _month_) # and _mday_ (or _day_). If the object responses to _hour_, _min_, # _sec_ or _sec_fraction_, the responsed values are used for hour, # minute, second or fraction of a second respectively. # If not, zeros are set. If the object responses to _offset_ or # _utc_offset_, it is used for time zone. If not, the session time # zone is used. # # The acceptable value are listed below. # _year_:: -4712 to 9999 [excluding year 0] # _mon_ (or _month_):: 0 to 12 # _mday_ (or _day_):: 0 to 31 [depends on the month] # _hour_:: 0 to 23 # _min_:: 0 to 59 # _sec_:: 0 to 59 # _sec_fraction_:: 0 to (999_999_999.to_r / (24*60*60* 1_000_000_000)) [999,999,999 nanoseconds] # _offset_:: (-12.to_r / 24) to (14.to_r / 24) [-12:00 to +14:00] # _utc_offset_:: -12*3600 <= utc_offset <= 24*3600 [-12:00 to +14:00] # # The output value of the bind varible is always a # DateTime[http://www.ruby-doc.org/core/classes/DateTime.html]. # # cursor = conn.exec("BEGIN :ts := current_timestamp; END") # cursor.bind_param(:ts, nil, DateTime) # cursor.exec # cursor[:ts] # => a DateTime. # cursor.close # class DateTime if OCI8.oracle_client_version >= ORAVER_9_0 def self.create(con, val, param, max_array_size) if true # TODO: check Oracle server version DateTimeViaOCITimestamp.new(con, val, param, max_array_size) else DateTimeViaOCIDate.new(con, val, param, max_array_size) end end else def self.create(con, val, param, max_array_size) DateTimeViaOCIDate.new(con, val, param, max_array_size) end end end class Time if OCI8.oracle_client_version >= ORAVER_9_0 def self.create(con, val, param, max_array_size) if true # TODO: check Oracle server version TimeViaOCITimestamp.new(con, val, param, max_array_size) else TimeViaOCIDate.new(con, val, param, max_array_size) end end else def self.create(con, val, param, max_array_size) TimeViaOCIDate.new(con, val, param, max_array_size) end end end if OCI8.oracle_client_version >= ORAVER_9_0 #-- # OCI8::BindType::IntervalYM #++ # # This is a helper class to bind ruby's # Integer[http://www.ruby-doc.org/core/classes/Integer.html] # object as Oracle's INTERVAL YEAR TO MONTH datatype. # # == Select # # The fetched value for a INTERVAL YEAR TO MONTH column # is an Integer[http://www.ruby-doc.org/core/classes/Integer.html] # which means the months between two timestamps. # # == Bind # # You cannot bind as INTERVAL YEAR TO MONTH implicitly. # It must be bound explicitly with :interval_ym. # # # output bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :interval := (:ts1 - :ts2) YEAR TO MONTH; # END; # EOS # cursor.bind_param(:interval, nil, :interval_ym) # cursor.bind_param(:ts1, DateTime.parse('1969-11-19 06:54:35 00:00')) # cursor.bind_param(:ts2, DateTime.parse('1969-07-20 20:17:40 00:00')) # cursor.exec # cursor[:interval] # => 4 (months) # cursor.close # # # input bind variable # cursor = conn.parse(<<-EOS) # BEGIN # :ts1 := :ts2 + :interval; # END; # EOS # cursor.bind_param(:ts1, nil, DateTime) # cursor.bind_param(:ts2, Date.parse('1969-11-19')) # cursor.bind_param(:interval, 4, :interval_ym) # cursor.exec # cursor[:ts1].strftime('%Y-%m-%d') # => 1970-03-19 # cursor.close # class IntervalYM < OCI8::BindType::OCIIntervalYM def set(val) # :nodoc: unless val.nil? val = [val / 12, val % 12] end super(val) end def get() # :nodoc: val = super() return nil if val.nil? year, month = val year * 12 + month end end # OCI8::BindType::IntervalYM #-- # OCI8::BindType::IntervalDS #++ # # This is a helper class to bind ruby's # Rational[http://www.ruby-doc.org/core/classes/Rational.html] # object as Oracle's INTERVAL DAY TO SECOND datatype. # # == Select # # The fetched value for a INTERVAL DAY TO SECOND column # is a Rational[http://www.ruby-doc.org/core/classes/Rational.html] # or an Integer[http://www.ruby-doc.org/core/classes/Integer.html]. # The value is usable to apply to # DateTime[http://www.ruby-doc.org/core/classes/DateTime.html]#+ and # DateTime[http://www.ruby-doc.org/core/classes/DateTime.html]#-. # # == Bind # # You cannot bind as INTERVAL YEAR TO MONTH implicitly. # It must be bound explicitly with :interval_ds. # # # output # ts1 = DateTime.parse('1969-11-19 06:54:35 00:00') # ts2 = DateTime.parse('1969-07-20 20:17:40 00:00') # cursor = conn.parse(<<-EOS) # BEGIN # :itv := (:ts1 - :ts2) DAY TO SECOND; # END; # EOS # cursor.bind_param(:itv, nil, :interval_ds) # cursor.bind_param(:ts1, ts1) # cursor.bind_param(:ts2, ts2) # cursor.exec # cursor[:itv] # == ts1 - ts2 # cursor.close # # # input # ts2 = DateTime.parse('1969-07-20 20:17:40 00:00') # itv = 121 + 10.to_r/24 + 36.to_r/(24*60) + 55.to_r/(24*60*60) # # 121 days, 10 hours, 36 minutes, 55 seconds # cursor = conn.parse(<<-EOS) # BEGIN # :ts1 := :ts2 + :itv; # END; # EOS # cursor.bind_param(:ts1, nil, DateTime) # cursor.bind_param(:ts2, ts2) # cursor.bind_param(:itv, itv, :interval_ds) # cursor.exec # cursor[:ts1].strftime('%Y-%m-%d %H:%M:%S') # => 1969-11-19 06:54:35 # cursor.close # class IntervalDS < OCI8::BindType::OCIIntervalDS @@hour = 1 / 24.to_r @@minute = @@hour / 60 @@sec = @@minute / 60 @@fsec = @@sec / 1000000000 def set(val) # :nodoc: unless val.nil? if val < 0 is_minus = true val = -val else is_minus = false end day, val = val.divmod 1 hour, val = (val * 24).divmod 1 minute, val = (val * 60).divmod 1 sec, val = (val * 60).divmod 1 fsec, val = (val * 1000000000).divmod 1 if is_minus day = - day hour = - hour minute = - minute sec = - sec fsec = - fsec end val = [day, hour, minute, sec, fsec] end super(val) end def get() # :nodoc: val = super() return nil if val.nil? day, hour, minute, sec, fsec = val day + (hour * @@hour) + (minute * @@minute) + (sec * @@sec) + (fsec * @@fsec) end end # OCI8::BindType::IntervalDS end end # OCI8::BindType end # OCI8