File: named_timezones.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (184 lines) | stat: -rw-r--r-- 7,543 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# frozen-string-literal: true
#
# Allows the use of named timezones via TZInfo (requires tzinfo).
# Forces the use of DateTime as Sequel's datetime_class, since
# historically, Ruby's Time class doesn't support timezones other
# than local and UTC. To continue using Ruby's Time class when using
# the named_timezones extension:
#
#   # Load the extension
#   Sequel.extension :named_timezones
#
#   # Set Sequel.datetime_class back to Time
#   Sequel.datetime_class = Time
#
# This allows you to either pass strings or TZInfo::Timezone
# instance to Sequel.database_timezone=, application_timezone=, and
# typecast_timezone=.  If a string is passed, it is converted to a
# TZInfo::Timezone using TZInfo::Timezone.get.
#
# Let's say you have the database server in New York and the
# application server in Los Angeles.  For historical reasons, data
# is stored in local New York time, but the application server only
# services clients in Los Angeles, so you want to use New York
# time in the database and Los Angeles time in the application.  This
# is easily done via:
#
#   Sequel.database_timezone = 'America/New_York'
#   Sequel.application_timezone = 'America/Los_Angeles'
#
# Then, before data is stored in the database, it is converted to New
# York time.  When data is retrieved from the database, it is
# converted to Los Angeles time.
#
# If you are using database specific timezones, you may want to load
# this extension into the database in order to support similar API:
#
#   DB.extension :named_timezones
#   DB.timezone = 'America/New_York'
#
# Note that typecasting from the database timezone to the application
# timezone when fetching rows is dependent on the database adapter,
# and only works on adapters where Sequel itself does the conversion.
# It should work with the mysql, postgres, sqlite, ibmdb, and jdbc
# adapters.
#
# Related module: Sequel::NamedTimezones

require 'tzinfo'

#
module Sequel
  self.datetime_class = DateTime
  
  module NamedTimezones
    module DatabaseMethods
      def timezone=(tz)
        super(Sequel.send(:convert_timezone_setter_arg, tz))
      end
    end

    # Handles TZInfo::AmbiguousTime exceptions automatically by providing a
    # proc called with both the datetime value being converted as well as
    # the array of TZInfo::TimezonePeriod results. Example:
    #
    #   Sequel.tzinfo_disambiguator = proc{|datetime, periods| periods.first}
    attr_accessor :tzinfo_disambiguator

    private 
    
    if RUBY_VERSION >= '2.6'
      # Whether Time.at with :nsec and :in is broken.  True on JRuby < 9.3.9.0.
      BROKEN_TIME_AT_WITH_NSEC = defined?(JRUBY_VERSION) && (JRUBY_VERSION < '9.3' || (JRUBY_VERSION < '9.4' && JRUBY_VERSION.split('.')[2].to_i < 9))
      private_constant :BROKEN_TIME_AT_WITH_NSEC

      # Convert the given input Time (which must be in UTC) to the given input timezone,
      # which should be a TZInfo::Timezone instance.
      def convert_input_time_other(v, input_timezone)
        Time.new(v.year, v.mon, v.day, v.hour, v.min, (v.sec + Rational(v.nsec, 1000000000)), input_timezone)
      rescue TZInfo::AmbiguousTime
        raise unless disamb = tzinfo_disambiguator_for(v)
        period = input_timezone.period_for_local(v, &disamb)
        offset = period.utc_total_offset
        # :nocov:
        if BROKEN_TIME_AT_WITH_NSEC
          Time.at(v.to_i - offset, :in => input_timezone) + v.nsec/1000000000.0
        # :nocov:
        else
          Time.at(v.to_i - offset, v.nsec, :nsec, :in => input_timezone)
        end
      end

      # Convert the given input Time to the given output timezone,
      # which should be a TZInfo::Timezone instance.
      def convert_output_time_other(v, output_timezone)
        # :nocov:
        if BROKEN_TIME_AT_WITH_NSEC
          Time.at(v.to_i, :in => output_timezone) + v.nsec/1000000000.0
        # :nocov:
        else
          Time.at(v.to_i, v.nsec, :nsec, :in => output_timezone)
        end
      end
      # :nodoc:
      # :nocov:
    else
      def convert_input_time_other(v, input_timezone)
        local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
        Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 
      end

      if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2'
        def convert_output_time_other(v, output_timezone)
          v = output_timezone.utc_to_local(v.getutc)
          local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
          Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 + local_offset
        end
      else
        def convert_output_time_other(v, output_timezone)
          v = output_timezone.utc_to_local(v.getutc)
          local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
          Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0
        end
      end
      # :nodoc:
      # :nocov:
    end

    # Handle both TZInfo 1 and TZInfo 2
    if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2'
      def convert_input_datetime_other(v, input_timezone)
        local_offset = Rational(input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset, 86400)
        (v - local_offset).new_offset(local_offset)
      end

      def convert_output_datetime_other(v, output_timezone)
        v = output_timezone.utc_to_local(v.new_offset(0))

        # Force DateTime output instead of TZInfo::DateTimeWithOffset
        DateTime.jd(v.jd, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
      end
      # :nodoc:
      # :nocov:
    else
      # Assume the given DateTime has a correct time but a wrong timezone.  It is
      # currently in UTC timezone, but it should be converted to the input_timezone.
      # Keep the time the same but convert the timezone to the input_timezone.
      # Expects the input_timezone to be a TZInfo::Timezone instance.
      def convert_input_datetime_other(v, input_timezone)
        local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational
        (v - local_offset).new_offset(local_offset)
      end

      # Convert the given DateTime to use the given output_timezone.
      # Expects the output_timezone to be a TZInfo::Timezone instance.
      def convert_output_datetime_other(v, output_timezone)
        # TZInfo 1 converts times, but expects the given DateTime to have an offset
        # of 0 and always leaves the timezone offset as 0
        v = output_timezone.utc_to_local(v.new_offset(0))
        local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational
        # Convert timezone offset from UTC to the offset for the output_timezone
        (v - local_offset).new_offset(local_offset)
      end
      # :nodoc:
      # :nocov:
    end
    
    # Returns TZInfo::Timezone instance if given a String.
    def convert_timezone_setter_arg(tz)
      tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super
    end

    # Return a disambiguation proc that provides both the datetime value
    # and the periods, in order to allow the choice of period to depend
    # on the datetime value.
    def tzinfo_disambiguator_for(v)
      if pr = @tzinfo_disambiguator
        proc{|periods| pr.call(v, periods)}
      end
    end
  end
  
  extend NamedTimezones
  Database.register_extension(:named_timezones, NamedTimezones::DatabaseMethods)
end