time_zone_scheduler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 47bbf93e0e186627215c28e7e4daaca94a368ec1
4
+ data.tar.gz: a436d640921114695ccbb20d19c8e0a3c3d0077a
5
+ SHA512:
6
+ metadata.gz: fb5d0a1560de701b440b75f9eb5dfbf3648fa1aab2e7fa7af7229af1f7cbaf98a8c2c99c672436afeaadf791c1d8108de87d1751e1cae4ba28c654d472f90fbc
7
+ data.tar.gz: 7b03973320b7f210d603c169cb6d27417b53469229c47c2a2010129051f47d3a273d62f50f8a5ce8c9efccee893956e4681ecca46f012fc092888f5e1b745b8f
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3.0
7
+ before_install: gem install bundler
8
+ install: bundle install --without doc
9
+
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --no-private
2
+ --markup markdown
3
+ --main README.md
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :doc do
6
+ gem 'yard'
7
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Artsy, Eloy Durán <eloy.de.enige@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # TimeZoneScheduler
2
+
3
+ [![Build Status](https://travis-ci.org/alloy/time_zone_scheduler.svg?branch=master)](https://travis-ci.org/alloy/time_zone_scheduler)
4
+
5
+ A Ruby library that assists in scheduling events whilst taking time zones into account. E.g. when to best deliver
6
+ notifications such as push notifications or emails.
7
+
8
+ NOTE: _It is not yet battle-tested. This will all follow over the next few weeks._
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'time_zone_scheduler'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install time_zone_scheduler
25
+
26
+ ## Usage
27
+
28
+ See [the documentation](http://www.rubydoc.info/gems/time_zone_scheduler).
29
+
30
+ ## Contributing
31
+
32
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alloy/time_zone_scheduler.
33
+
34
+ ## License
35
+
36
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
37
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ desc "Install all dependencies"
2
+ task :bootstrap do
3
+ if system('which bundle')
4
+ sh "bundle install"
5
+ #sh "git submodule update --init"
6
+ else
7
+ $stderr.puts "\033[0;31m[!] Please install the bundler gem manually: $ [sudo] gem install bundler\e[0m"
8
+ exit 1
9
+ end
10
+ end
11
+
12
+ begin
13
+ require 'bundler/gem_tasks'
14
+
15
+ desc "Generate documentation"
16
+ task :doc do
17
+ sh "yard doc"
18
+ end
19
+
20
+ require "rake/testtask"
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << "test"
23
+ t.libs << "lib"
24
+ t.test_files = FileList['test/**/*_test.rb']
25
+ end
26
+
27
+ task :default => :test
28
+
29
+ rescue LoadError
30
+ $stderr.puts "\033[0;33m[!] Disabling rake tasks because the environment couldn’t be loaded. Be sure to run `rake bootstrap` first.\e[0m"
31
+ end
@@ -0,0 +1,245 @@
1
+ require "time_zone_scheduler/version"
2
+
3
+ require "active_support/core_ext/time/zones"
4
+ require 'active_support/duration'
5
+
6
+ # A Ruby library that assists in scheduling events whilst taking time zones into account. E.g. when to best deliver
7
+ # notifications such as push notifications or emails.
8
+ #
9
+ # It relies on ActiveSupport’s time and time zone functionality and expects a current system time zone to be specified
10
+ # through `Time.zone`.
11
+ #
12
+ # ### Terminology
13
+ #
14
+ # Consider a server sending notifications to a user:
15
+ #
16
+ # - **system time**: The local time of the server in the current time zone, as specified with `Time.zone`.
17
+ # - **reference time**: The time that needs to be e.g. converted into the user’s destination time zone.
18
+ # - **destination time zone**: The time zone that the user resides in.
19
+ # - **destination time**: The local time of the time zone that the user resides in.
20
+ #
21
+ class TimeZoneScheduler
22
+ VERSION = "0.1.0"
23
+
24
+ # @return [ActiveSupport::TimeZone]
25
+ # the destination time zone for the various calculations this class performs.
26
+ #
27
+ attr_reader :destination_time_zone
28
+
29
+ # @param [String, ActiveSupport::TimeZone] destination_time_zone
30
+ # the destination time zone that calculations will be performed in.
31
+ #
32
+ def initialize(destination_time_zone)
33
+ @destination_time_zone = Time.find_zone!(destination_time_zone)
34
+ end
35
+
36
+ # This calculation takes the local date and time of day of the reference time and converts that to the exact same date
37
+ # and time of day in the destination time zone and returns it in the system time. In other words, you’d use this to
38
+ # calculate the system time at which a specific date and time of day occurs in the destination time zone.
39
+ #
40
+ # For instance, you could use this to schedule notifications that should be sent to users on specific days of the week
41
+ # at times of the day that they are most likely to be good for the user. E.g. every Thursday at 10AM.
42
+ #
43
+ # @example Calculate the system time that corresponds to Sunday 2015-10-25 at 10AM in the Pacific/Niue time zone.
44
+ #
45
+ # Time.zone = "Pacific/Kiritimati" # Set the system time zone
46
+ # scheduler = TimeZoneScheduler.new("Pacific/Niue")
47
+ # reference_time = Time.parse("2015-10-25 10:00 UTC")
48
+ # system_time = scheduler.schedule_on_date(reference_time, false)
49
+ #
50
+ # p reference_time # => Sun, 25 Oct 2015 10:00:00 UTC +00:00
51
+ # p system_time # => Mon, 26 Oct 2015 11:00:00 LINT +14:00
52
+ #
53
+ # p system_time.sunday? # => false
54
+ # p system_time.hour # => 11
55
+ #
56
+ # p local_time = system_time.in_time_zone("Pacific/Niue")
57
+ # p local_time.sunday? # => true
58
+ # p local_time.hour # => 10
59
+ #
60
+ # @param [Time] reference_time
61
+ # the reference date and time of day that’s to be scheduled in the destination time zone.
62
+ #
63
+ # @param [Boolean] raise_if_time_has_passed
64
+ # whether or not to check if the time in the destination time zone has already passed.
65
+ #
66
+ # @raise [ArgumentError]
67
+ # in case the check is enabled, this is raised if the time in the destination time zone has already passed.
68
+ #
69
+ # @return [Time]
70
+ # the system time that corresponds to the time scheduled in the destination time zone.
71
+ #
72
+ def schedule_on_date(reference_time, raise_if_time_has_passed = true)
73
+ destination_time = @destination_time_zone.parse(reference_time.strftime('%F %T'))
74
+ system_time = destination_time.in_time_zone(Time.zone)
75
+ if raise_if_time_has_passed && system_time < Time.zone.now
76
+ raise ArgumentError, "The specified time has already passed in the #{@destination_time_zone.name} timezone."
77
+ end
78
+ system_time
79
+ end
80
+
81
+ # This calculation schedules the time to be at the same time as the reference time (real time), except when that time,
82
+ # in the destination time zone, falls _outside_ of the specified timeframe. In that case it delays the time until the
83
+ # next minimum time of the timeframe is reached.
84
+ #
85
+ # For instance, you could use this to schedule notifications about an event starting in either real-time, if that’s a
86
+ # good time for the user in their time zone, or otherwise delay it to the next good time.
87
+ #
88
+ # @example Return the real time, as the reference time falls in the specified timeframe in the Europe/Amsterdam time zone.
89
+ #
90
+ # Time.zone = "UTC" # Set the system time zone
91
+ # scheduler = TimeZoneScheduler.new("Europe/Amsterdam")
92
+ # reference_time = Time.parse("2015-10-25 12:00 UTC")
93
+ # system_time = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
94
+ # local_time = system_time.in_time_zone("Europe/Amsterdam")
95
+ #
96
+ # p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
97
+ # p system_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
98
+ # p local_time # => Sun, 25 Oct 2015 13:00:00 CET +01:00
99
+ #
100
+ # @example Delay the reference time so that it’s not scheduled before 10AM in the Pacific/Kiritimati time zone.
101
+ #
102
+ # Time.zone = "UTC" # Set the system time zone
103
+ # scheduler = TimeZoneScheduler.new("Pacific/Kiritimati")
104
+ # reference_time = Time.parse("2015-10-25 12:00 UTC")
105
+ # system_time = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
106
+ # local_time = system_time.in_time_zone("Pacific/Kiritimati")
107
+ #
108
+ # p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
109
+ # p system_time # => Sun, 25 Oct 2015 20:00:00 UTC +00:00
110
+ # p local_time # => Mon, 26 Oct 2015 10:00:00 LINT +14:00
111
+ #
112
+ # @example Delay the reference time so that it’s not scheduled after 2PM in the Europe/Moscow time zone.
113
+ #
114
+ # Time.zone = "UTC" # Set the system time zone
115
+ # scheduler = TimeZoneScheduler.new("Europe/Moscow")
116
+ # reference_time = Time.parse("2015-10-25 12:00 UTC")
117
+ # system_time = scheduler.schedule_in_timeframe(reference_time, "10:00".."14:00")
118
+ # local_time = system_time.in_time_zone("Europe/Moscow")
119
+ #
120
+ # p reference_time # => Sun, 25 Oct 2015 12:00:00 UTC +00:00
121
+ # p system_time # => Mon, 26 Oct 2015 07:00:00 UTC +00:00
122
+ # p local_time # => Mon, 26 Oct 2015 10:00:00 MSK +03:00
123
+ #
124
+ # @param [Time] reference_time
125
+ # the reference time that’s to be re-scheduled in the destination time zone if it falls outside the timeframe.
126
+ #
127
+ # @param [Range<String..String>] timeframe
128
+ # a range of times (of the day) in which the scheduled time should fall.
129
+ #
130
+ # @return [Time]
131
+ # either the original reference time, if it falls in the timeframe, or the delayed time.
132
+ #
133
+ def schedule_in_timeframe(reference_time, timeframe)
134
+ timeframe = TimeFrame.new(@destination_time_zone, reference_time, timeframe)
135
+ if timeframe.reference_before_timeframe?
136
+ timeframe.min
137
+ elsif timeframe.reference_after_timeframe?
138
+ timeframe.min.tomorrow
139
+ else
140
+ reference_time
141
+ end.in_time_zone(Time.zone)
142
+ end
143
+
144
+ # This checks if the reference time falls in the given timeframe in the destination time zone.
145
+ #
146
+ # For instance, you could use this to disable playing a sound for notifications that **have** to be scheduled in real
147
+ # time, but you don’t necessarily want to e.g. wake the user.
148
+ #
149
+ # @example Return that 1PM in the Europe/Amsterdam time zone falls in the timeframe.
150
+ #
151
+ # Time.zone = "UTC" # Set the system time zone
152
+ # scheduler = TimeZoneScheduler.new("Europe/Amsterdam")
153
+ # reference_time = Time.parse("2015-10-25 12:00 UTC")
154
+ #
155
+ # p scheduler.in_timeframe?(reference_time, "08:00".."14:00") # => true
156
+ #
157
+ # @example Return that 3PM in the Europe/Moscow time zone falls outside the timeframe.
158
+ #
159
+ # Time.zone = "UTC" # Set the system time zone
160
+ # scheduler = TimeZoneScheduler.new("Europe/Moscow")
161
+ # reference_time = Time.parse("2015-10-25 12:00 UTC")
162
+ #
163
+ # p scheduler.in_timeframe?(reference_time, "08:00".."14:00") # => true
164
+ #
165
+ # @param [Time] reference_time
166
+ # the reference time that’s to be checked if it falls in the timeframe in the destination time zone.
167
+ #
168
+ # @param [Range<String..String>] timeframe
169
+ # a range of times (of the day) in which the reference time should fall.
170
+ #
171
+ # @return [Boolean]
172
+ # whether or not the reference time falls in the specified timeframe in the destination time zone.
173
+ #
174
+ def in_timeframe?(reference_time, timeframe)
175
+ TimeFrame.new(@destination_time_zone, reference_time, timeframe).reference_in_timeframe?
176
+ end
177
+
178
+ # @!visibility private
179
+ #
180
+ # Assists in calculations regarding timeframes. It caches the results so the caller doesn’t need to worry about cost.
181
+ #
182
+ class TimeFrame
183
+ # @param [ActiveSupport::TimeZone] destination_time_zone
184
+ # @param [Time] reference_time
185
+ # @param [Range<String..String>] timeframe
186
+ #
187
+ def initialize(destination_time_zone, reference_time, timeframe)
188
+ @destination_time_zone, @reference_time, @timeframe = destination_time_zone, reference_time, timeframe
189
+ end
190
+
191
+ # @return [Time]
192
+ # the minimum time of the timeframe range in the destination time zone.
193
+ #
194
+ def min
195
+ @min ||= @destination_time_zone.parse("#{local_date} #{@timeframe.min}")
196
+ end
197
+
198
+ # @return [Time]
199
+ # the maximum time of the timeframe range in the destination time zone.
200
+ #
201
+ def max
202
+ @max ||= @destination_time_zone.parse("#{local_date} #{@timeframe.max}")
203
+ end
204
+
205
+ # @return [Boolean]
206
+ # whether the reference time falls before the timeframe.
207
+ #
208
+ def reference_before_timeframe?
209
+ local_time < min
210
+ end
211
+
212
+ # @return [Boolean]
213
+ # whether the reference time falls after the timeframe.
214
+ #
215
+ def reference_after_timeframe?
216
+ local_time > max
217
+ end
218
+
219
+ # @note First checks if the reference time falls before the timeframe, because if that fails {#max} never needs to
220
+ # be performed for {TimeZoneScheduler#schedule_in_timeframe} to be able to perform its work.
221
+ #
222
+ # @return [Boolean]
223
+ # whether the reference time falls in the timeframe.
224
+ #
225
+ def reference_in_timeframe?
226
+ !reference_before_timeframe? && !reference_after_timeframe?
227
+ end
228
+
229
+ private
230
+
231
+ # @return [Time]
232
+ # the reference time in the destination timezone.
233
+ #
234
+ def local_time
235
+ @local_time ||= @reference_time.in_time_zone(@destination_time_zone)
236
+ end
237
+
238
+ # @return [String]
239
+ # the date of the reference time in the destination timezone.
240
+ #
241
+ def local_date
242
+ @date ||= local_time.strftime('%F')
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,3 @@
1
+ class TimeZoneScheduler
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'time_zone_scheduler/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "time_zone_scheduler"
8
+ spec.version = TimeZoneScheduler::VERSION
9
+ spec.authors = ["Eloy Durán"]
10
+ spec.email = ["eloy.de.enige@gmail.com"]
11
+
12
+ spec.summary = "A library that assists in scheduling events whilst taking time zones into account."
13
+ spec.homepage = "https://github.com/alloy/time_zone_scheduler"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_runtime_dependency "activesupport"
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.11"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.0"
24
+ spec.add_development_dependency "timecop", "~> 0.8.0"
25
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time_zone_scheduler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eloy Durán
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.0
83
+ description:
84
+ email:
85
+ - eloy.de.enige@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - .travis.yml
92
+ - .yardopts
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - lib/time_zone_scheduler.rb
98
+ - lib/time_zone_scheduler/version.rb
99
+ - time_zone_scheduler.gemspec
100
+ homepage: https://github.com/alloy/time_zone_scheduler
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.5.0
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: A library that assists in scheduling events whilst taking time zones into
124
+ account.
125
+ test_files: []
126
+ has_rdoc: