teasy 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 53f5f0b964d3834bca1c2b758e319f63d51f9d1b
4
- data.tar.gz: e29dd57251904a1ad26ce7d122fe66a87f54aac0
3
+ metadata.gz: 273a6716f2320256d8894fc5d59cdb5db6d199ab
4
+ data.tar.gz: 5a8b907999251a222f9bd474306c30293ed80b97
5
5
  SHA512:
6
- metadata.gz: d481cbb6b0966e20565b745b446d0703e19b6cf0238ea10b71994756167c84e68b3e158f1ce0f9d9841ce2c7e5cb1fbcbf7b7a1bca9d469b042bcad0d43be689
7
- data.tar.gz: 6de90fda2fea2a0dc8e95ed89e0711303e3c697082a2459ddde8971e79c8b1b731c2370df63e2ac2af58f589db2aa2c0b9f5843dc0d8b54cf7ec4caf61df2f74
6
+ metadata.gz: 2c371ca880d7a52a2a4bc5866744c17908e838e593b43e041e5d92a8c7826ea11ee45823731673a25a9403a198c945f1a4895065ab47bc68f3f950bd595f1195
7
+ data.tar.gz: d127720d95b025f5acbf27c147a63f31bc40248c52ed8457bd7be955268845e1cf21ef5ee83fc22be2766ebec029edbca5eff0e3dd042c939a809ef826becfdc
data/README.md CHANGED
@@ -96,6 +96,75 @@ calcutta_time == Time.utc(2042) # -> true
96
96
  calcutta_time.eql? Time.utc(2042) # -> false
97
97
  ```
98
98
 
99
+ #### Handling Period Not Found
100
+
101
+ Some periods do not exist. This is a problem you do not have to worry about as soon as you have successfully constructed a `Teasy::TimeWithZone` object, since all operations on existing `TimeWithZone` objects are safe in that way.
102
+ However, construction may fail when you choose invalid parameters. E.g., `Teasy::TimeWithZone.new(2014, 3, 30, 2, 30, 0, 0, 'Europe/Berlin')` does not exist since time advanced from 2 a.m. to 3 a.m. local time in the CET time zone on March the 30th, 2014 and thus no 2:30 a.m. exists.
103
+
104
+ By default we will raise a `TZInfo::PeriodNotFound` exception in this case. However, you may want to change this behaviour by defining a different period not found handler.
105
+
106
+ ```ruby
107
+ # the default handler
108
+ Teasy.period_not_found_handler = :raise
109
+ Teasy::TimeWithZone.new(2014, 3, 30, 2, 30, 0, 0, 'Europe/Berlin')
110
+ # => TZInfo::PeriodNotFound: TZInfo::PeriodNotFound
111
+
112
+ # tell teasy to default to the previous period
113
+ Teasy.period_not_found_handler = :next_period
114
+ Teasy::TimeWithZone.new(2014, 3, 30, 2, 30, 0, 0, 'Europe/Berlin')
115
+ # => 2014-03-30 03:00:00 +0200
116
+
117
+ # tell teasy to default to the previous period
118
+ Teasy.period_not_found_handler = :previous_period
119
+ Teasy::TimeWithZone.new(2014, 3, 30, 2, 30, 0, 0, 'Europe/Berlin')
120
+ # => 2014-03-30 02:00:00 +0100
121
+
122
+ # or define a custom handler, it has to be callable and will receive a time object without
123
+ # zone information (it says UTC but it's not!) and the zone information object
124
+ Teasy.period_not_found_handler = lambda do |time, zone|
125
+ warn "#{time} does not exist for #{zone}"
126
+ Teasy::TimeWithZone.new(time.year)
127
+ end
128
+ ```
129
+
130
+ If you want to change the behaviour for just one piece of code, the you can use the `Teasy::with_period_not_found_handler` method with a block.
131
+
132
+ #### Handling Ambiguous Time
133
+
134
+ Similarly to how some periods do not exist, sometimes time is ambiguous. Missing periods are the result from forward shifts in time, ambiguous time is due to backward shifts in time. E.g., `Teasy::TimeWithZone.new(2014, 10, 26, 2, 0, 0, 0, 'Europe/Berlin')` is ambiguous since the daylight savings time ended at 3 a.m. local time in central europe and the clocks were turned back to 2 a.m. Therefore it could be either CET (+1) or CEST (+2), you just cannot know.
135
+
136
+ By default we will raise a `TZInfo::AmbiguousTime` exception when this happens. However, you may, again, change the default behaviour in general or for a specific piece of code, like this:
137
+
138
+ ```ruby
139
+ # the default handler
140
+ Teasy.ambiguous_time_handler = :raise
141
+ Teasy::TimeWithZone.new(2014, 10, 26, 2, 0, 0, 0, 'Europe/Berlin')
142
+ # => TZInfo::AmbiguousTime: 2014-10-26 02:00:00 UTC is an ambiguous local time
143
+
144
+ # tell teasy to default to the daylight savings time
145
+ Teasy.ambiguous_time_handler = :daylight_savings_time
146
+ Teasy::TimeWithZone.new(2014, 10, 26, 2, 0, 0, 0, 'Europe/Berlin')
147
+ # => 2014-10-26 02:00:00 +0200
148
+
149
+ # conversely tell teasy to default to the standard time
150
+ Teasy.ambiguous_time_handler = :standard_time
151
+ Teasy::TimeWithZone.new(2014, 10, 26, 2, 0, 0, 0, 'Europe/Berlin')
152
+ # => 2014-10-26 02:00:00 +0100
153
+
154
+ # or define a custom handler, it has to be callable and will receive a time object without
155
+ # zone information (it says UTC but it's not!) and the candidate periods. The periods are
156
+ # sorted by time and the block has to return a single period to resolve the ambiguity.
157
+ Teasy.ambiguous_time_handler = lambda do |time, periods|
158
+ if time.minute < 30
159
+ periods.first
160
+ else
161
+ periods.last
162
+ end
163
+ end
164
+ ```
165
+
166
+ Of course there's also a `Teasy::with_ambiguous_time_handler` methods that accepts a block and will reset the handler after the block.
167
+
99
168
  ### FloatingTime
100
169
  #### Create a FloatingTime object
101
170
  ```ruby
@@ -136,6 +205,10 @@ floating_time == other_ny_time # -> false
136
205
  [Time.utc(2042), ny_time, other_ny_time].any? { |time| floating_time.eql? time } # -> false
137
206
  ```
138
207
 
208
+ #### Convert to a TimeWithZone
209
+
210
+ Simply call `in_time_zone` with a specific timezone to convert a floating time into a time with a zone, if the time exists in the given timezone, you shall get a `TimeWithZone` object.
211
+
139
212
  ## Contributing
140
213
 
141
214
  1. Fork it ( https://github.com/kaikuchn/teasy/fork )
@@ -3,21 +3,28 @@
3
3
  require 'teasy/version'
4
4
  require 'teasy/time_with_zone'
5
5
  require 'teasy/floating_time'
6
+ require 'teasy/ambiguous_time_handling'
7
+ require 'teasy/period_not_found_handling'
6
8
 
7
9
  module Teasy
8
- def self.default_zone
9
- Thread.current[:teasy_default_zone] ||= 'UTC'
10
- end
10
+ include AmbiguousTimeHandling
11
+ include PeriodNotFoundHandling
11
12
 
12
- def self.default_zone=(zone)
13
- Thread.current[:teasy_default_zone] = zone
14
- end
13
+ class << self
14
+ def default_zone
15
+ Thread.current[:teasy_default_zone] ||= 'UTC'
16
+ end
17
+
18
+ def default_zone=(zone)
19
+ Thread.current[:teasy_default_zone] = zone
20
+ end
15
21
 
16
- def self.with_zone(zone)
17
- old_zone = Thread.current[:teasy_default_zone]
18
- Thread.current[:teasy_default_zone] = zone
19
- yield
20
- ensure
21
- Thread.current[:teasy_default_zone] = old_zone
22
+ def with_zone(zone)
23
+ old_zone = default_zone
24
+ self.default_zone = zone
25
+ yield
26
+ ensure
27
+ self.default_zone = old_zone
28
+ end
22
29
  end
23
30
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teasy
4
+ module AmbiguousTimeHandling
5
+ UnknownAmbiguousTimeHandler = Class.new(StandardError)
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def ambiguous_time_handler
12
+ Thread.current[:teasy_ambiguous_time_handler] ||= HANDLER[:raise]
13
+ end
14
+
15
+ def ambiguous_time_handler=(name_or_callable)
16
+ if name_or_callable.respond_to?(:call)
17
+ Thread.current[:teasy_ambiguous_time_handler] = name_or_callable
18
+ else
19
+ Thread.current[:teasy_ambiguous_time_handler] = HANDLER.fetch(
20
+ name_or_callable.to_sym
21
+ ) do |key|
22
+ raise UnknownAmbiguousTimeHandler,
23
+ "Don't know an ambiguous time handler `#{key}`."
24
+ end
25
+ end
26
+ end
27
+
28
+ def with_ambiguous_time_handler(handler)
29
+ old_handler = ambiguous_time_handler
30
+ self.ambiguous_time_handler = handler
31
+ yield
32
+ ensure
33
+ self.ambiguous_time_handler = old_handler
34
+ end
35
+
36
+ HANDLER = {
37
+ # By returning nil TZInfo will raise TZInfo::AmbigousTime. It'd be
38
+ # better to raise our own error, but that would break the API that's out
39
+ # there. So that will have to wait for a 1.x release.
40
+ raise: ->(_time, _periods) {},
41
+ daylight_savings_time: ->(_time, periods) { periods.select(&:dst?) },
42
+ standard_time: ->(_time, periods) { periods.reject(&:dst?) }
43
+ }.freeze
44
+ end
45
+ end
46
+ end
@@ -25,6 +25,10 @@ module Teasy
25
25
  time.hour, time.min, time.sec, time.nsec / 1_000.0)
26
26
  end
27
27
 
28
+ def in_time_zone(zone)
29
+ Teasy.with_zone(zone) { TimeWithZone.from_time(self) }
30
+ end
31
+
28
32
  def round!(*args)
29
33
  @time = time.round(*args)
30
34
  self
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teasy
4
+ module PeriodNotFoundHandling
5
+ UnknownPeriodNotFoundHandler = Class.new(StandardError)
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def period_not_found_handler
13
+ Thread.current[:teasy_period_not_found_handler] ||= HANDLER[:raise]
14
+ end
15
+
16
+ def period_not_found_handler=(name_or_callable)
17
+ if name_or_callable.respond_to?(:call)
18
+ Thread.current[:teasy_period_not_found_handler] = name_or_callable
19
+ else
20
+ Thread.current[:teasy_period_not_found_handler] = HANDLER.fetch(
21
+ name_or_callable.to_sym
22
+ ) do |key|
23
+ raise UnknownPeriodNotFoundHandler,
24
+ "Don't know a PeriodNotFound handler `#{key}`."
25
+ end
26
+ end
27
+ end
28
+
29
+ def with_period_not_found_handler(handler)
30
+ old_handler = period_not_found_handler
31
+ self.period_not_found_handler = handler
32
+ yield
33
+ ensure
34
+ self.period_not_found_handler = old_handler
35
+ end
36
+
37
+ HANDLER = {
38
+ raise: ->(_time, _zone) { raise },
39
+ # the biggest change in offsets known to me is when Samoa went from -11
40
+ # to +13 (a full day!) so hopefully we're sure to leave the unknown
41
+ # period by adding/subtracting 3 days
42
+ next_period: lambda do |time, zone|
43
+ period = zone.period_for_local(time + 3 * 86_400)
44
+ [period, period.start_transition.time + period.utc_total_offset]
45
+ end,
46
+ previous_period: lambda do |time, zone|
47
+ period = zone.period_for_local(time - 3 * 86_400)
48
+ [period, period.end_transition.time + period.utc_total_offset]
49
+ end
50
+ }.freeze
51
+ end
52
+ end
53
+ end
@@ -24,7 +24,7 @@ module Teasy
24
24
  zone = Teasy.default_zone)
25
25
  @zone = TZInfo::Timezone.get(zone)
26
26
  @time = Time.utc(year, month, day, hour, minute, second, usec_with_frac)
27
- @period = @zone.period_for_local(@time)
27
+ @period = determine_period(@time, @zone)
28
28
  end
29
29
  # rubocop:enable Metrics/ParameterLists
30
30
 
@@ -148,6 +148,16 @@ module Teasy
148
148
 
149
149
  private
150
150
 
151
+ def determine_period(time, zone = Teasy.default_zone)
152
+ zone.period_for_local(time) do |results|
153
+ Teasy.ambiguous_time_handler.call(time, results)
154
+ end
155
+ rescue TZInfo::PeriodNotFound
156
+ period, time = Teasy.period_not_found_handler.call(time, zone)
157
+ @time = time
158
+ period
159
+ end
160
+
151
161
  def utc_time
152
162
  @utc_time ||= @zone.local_to_utc(@time)
153
163
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teasy
4
- VERSION = '0.1.3'
4
+ VERSION = '0.2.0'.freeze
5
5
  end
@@ -23,6 +23,20 @@ class FloatingTimeTest < Minitest::Test
23
23
  assert_equal 0, timestamp.hour
24
24
  end
25
25
 
26
+ def test_in_time_zone
27
+ time = Teasy::FloatingTime.new(2014, 1, 1, 12)
28
+ assert_equal Time.utc(2014, 1, 1, 11), time.in_time_zone('Europe/Berlin')
29
+ assert_equal Time.utc(2014, 1, 1, 12), time.in_time_zone('Europe/London')
30
+ assert_equal Time.utc(2014, 1, 1, 6, 30), time.in_time_zone('Asia/Calcutta')
31
+ assert_equal Time.utc(2014, 1, 1, 17), time.in_time_zone('America/New_York')
32
+ assert_raises(TZInfo::AmbiguousTime) do
33
+ Teasy::FloatingTime.new(2014, 10, 26, 2).in_time_zone('Europe/Berlin')
34
+ end
35
+ assert_raises(TZInfo::PeriodNotFound) do
36
+ Teasy::FloatingTime.new(2014, 3, 30, 2, 30).in_time_zone('Europe/Berlin')
37
+ end
38
+ end
39
+
26
40
  def test_addition
27
41
  assert_equal 45, @timestamp.sec
28
42
  @timestamp += 5
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'test_helper'
2
4
 
3
5
  class TimeWithZoneTest < Minitest::Test
@@ -76,17 +78,49 @@ class TimeWithZoneTest < Minitest::Test
76
78
  assert dst_start.dst?
77
79
  end
78
80
 
79
- def test_raises_on_ambiguous_time
81
+ def test_call_ambiguous_time_handler_on_ambiguous_time
80
82
  dst_end = [2014, 10, 26, 2, 0, 0, 0, 'Europe/Berlin']
83
+ # raise is default
81
84
  assert_raises(TZInfo::AmbiguousTime) do
82
85
  Teasy::TimeWithZone.new(*dst_end)
83
86
  end
87
+
88
+ Teasy.with_ambiguous_time_handler(:daylight_savings_time) do
89
+ time = Teasy::TimeWithZone.new(*dst_end)
90
+ assert time.dst?
91
+ assert_equal 2, time.hour
92
+ end
93
+
94
+ Teasy.with_ambiguous_time_handler(:standard_time) do
95
+ time = Teasy::TimeWithZone.new(*dst_end)
96
+ refute time.dst?
97
+ assert_equal 2, time.hour
98
+ end
84
99
  end
85
100
 
86
- def test_raises_when_period_does_not_exist
87
- dst_start = [2014, 3, 30, 2, 30, 0, 0, 'Europe/Berlin']
88
- assert_raises(TZInfo::PeriodNotFound) do
89
- Teasy::TimeWithZone.new(*dst_start)
101
+ def test_call_period_not_found_handler_when_period_does_not_exist
102
+ Teasy.with_zone('Europe/Berlin') do
103
+ dst_start = [2014, 3, 30, 2, 30, 0, 0]
104
+ # raise is default
105
+ assert_raises(TZInfo::PeriodNotFound) do
106
+ Teasy::TimeWithZone.new(*dst_start)
107
+ end
108
+
109
+ Teasy.with_period_not_found_handler(:next_period) do
110
+ time = Teasy::TimeWithZone.new(*dst_start)
111
+ assert time.dst?
112
+ assert_equal(
113
+ Teasy::TimeWithZone.new(2014, 3, 30, 3), time
114
+ )
115
+ end
116
+
117
+ Teasy.with_period_not_found_handler(:previous_period) do
118
+ time = Teasy::TimeWithZone.new(*dst_start)
119
+ refute time.dst?
120
+ assert_equal(
121
+ Teasy::TimeWithZone.new(2014, 3, 30, 2), time
122
+ )
123
+ end
90
124
  end
91
125
  end
92
126
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teasy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Kuchenbecker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-31 00:00:00.000000000 Z
11
+ date: 2017-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo
@@ -127,7 +127,9 @@ files:
127
127
  - README.md
128
128
  - Rakefile
129
129
  - lib/teasy.rb
130
+ - lib/teasy/ambiguous_time_handling.rb
130
131
  - lib/teasy/floating_time.rb
132
+ - lib/teasy/period_not_found_handling.rb
131
133
  - lib/teasy/time_with_zone.rb
132
134
  - lib/teasy/version.rb
133
135
  - teasy.gemspec