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 +4 -4
- data/README.md +73 -0
- data/lib/teasy.rb +19 -12
- data/lib/teasy/ambiguous_time_handling.rb +46 -0
- data/lib/teasy/floating_time.rb +4 -0
- data/lib/teasy/period_not_found_handling.rb +53 -0
- data/lib/teasy/time_with_zone.rb +11 -1
- data/lib/teasy/version.rb +1 -1
- data/test/teasy/floating_time_test.rb +14 -0
- data/test/teasy/time_with_zone_test.rb +39 -5
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 273a6716f2320256d8894fc5d59cdb5db6d199ab
|
4
|
+
data.tar.gz: 5a8b907999251a222f9bd474306c30293ed80b97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 )
|
data/lib/teasy.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
end
|
10
|
+
include AmbiguousTimeHandling
|
11
|
+
include PeriodNotFoundHandling
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
data/lib/teasy/floating_time.rb
CHANGED
@@ -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
|
data/lib/teasy/time_with_zone.rb
CHANGED
@@ -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 =
|
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
|
data/lib/teasy/version.rb
CHANGED
@@ -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
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
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-
|
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
|