time_zone_scheduler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: