icalendar-recurrence 0.0.1
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.
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +6 -0
- data/icalendar-recurrence.gemspec +31 -0
- data/lib/icalendar-recurrence.rb +1 -0
- data/lib/icalendar/recurrence.rb +12 -0
- data/lib/icalendar/recurrence/event_extensions.rb +36 -0
- data/lib/icalendar/recurrence/schedule.rb +148 -0
- data/lib/icalendar/recurrence/time_util.rb +94 -0
- data/lib/icalendar/recurrence/version.rb +5 -0
- data/lib/icalendar/recurrence/weekday_extensions.rb +13 -0
- data/spec/lib/recurrence_spec.rb +154 -0
- data/spec/lib/schedule_spec.rb +52 -0
- data/spec/lib/time_util_spec.rb +137 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/fixtures/daily_event.ics +30 -0
- data/spec/support/fixtures/embedded_timezone_event.ics +36 -0
- data/spec/support/fixtures/every_monday_event.ics +28 -0
- data/spec/support/fixtures/every_other_day_event.ics +29 -0
- data/spec/support/fixtures/every_weekday_daily_event.ics +26 -0
- data/spec/support/fixtures/everyday_for_four_days_event.ics +23 -0
- data/spec/support/fixtures/first_of_every_year_event.ics +31 -0
- data/spec/support/fixtures/first_saturday_of_month_event.ics +49 -0
- data/spec/support/fixtures/first_sunday_of_january_yearly_event.ics +26 -0
- data/spec/support/fixtures/monday_until_friday_event.ics +23 -0
- data/spec/support/fixtures/multi_day_weekly_event.ics +45 -0
- data/spec/support/fixtures/on_third_every_two_months_event.ics +28 -0
- data/spec/support/fixtures/one_day_a_month_for_three_months_event.ics +29 -0
- data/spec/support/fixtures/utc_event.ics +28 -0
- data/spec/support/helpers.rb +14 -0
- metadata +266 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jordan Raine
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# iCalendar Recurrence [](https://travis-ci.org/icalendar/icalendar-recurrence) [](https://codeclimate.com/github/icalendar/icalendar-recurrence)
|
2
|
+
|
3
|
+
Adds event recurrence to the [icalendar gem](https://github.com/icalendar/icalendar). This is helpful in cases where you'd like to parse an ICS and generate a series of event occurrences.
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
_Note: This only works against the 2.0beta release of the icalendar gem._
|
8
|
+
|
9
|
+
**Until icalendar 2.0beta is released, use git repos in your Gemfile:**
|
10
|
+
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "icalendar", git: "https://github.com/icalendar/icalendar", branch: "2.0beta"
|
14
|
+
gem "icalendar-recurrence", git: "https://github.com/icalendar/icalendar-recurrence"
|
15
|
+
```
|
16
|
+
|
17
|
+
and run `bundle install` from your shell.
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Show occurrences of event between dates
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'date' # for parse method
|
25
|
+
require 'icalendar/recurrence'
|
26
|
+
|
27
|
+
calendars = Icalendar.parse(File.read(path_to_ics)) # parse an ICS file
|
28
|
+
event = Array(calendars).first.events.first # retrieve the first event
|
29
|
+
event.occurrences_between(Date.parse("2014-01-01"), Date.parse("2014-02-01")) # get all occurrence for one month
|
30
|
+
```
|
31
|
+
|
32
|
+
### Working with occurrences
|
33
|
+
|
34
|
+
An event occurrence is a simple struct object with `start_time` and `end_time` methods.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
occurrence.start_time # => 2014-02-01 00:00:00 -0800
|
38
|
+
occurrence.end_time # => 2014-02-02 00:00:00 -0800
|
39
|
+
```
|
40
|
+
|
41
|
+
### Daily event with excluded date (inline ICS example)
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'date' # for parse method
|
45
|
+
require 'icalendar/recurrence'
|
46
|
+
|
47
|
+
ics_string = <<-EOF
|
48
|
+
BEGIN:VCALENDAR
|
49
|
+
X-WR-CALNAME:Test Public
|
50
|
+
X-WR-CALID:f512e378-050c-4366-809a-ef471ce45b09:101165
|
51
|
+
PRODID:Zimbra-Calendar-Provider
|
52
|
+
VERSION:2.0
|
53
|
+
METHOD:PUBLISH
|
54
|
+
BEGIN:VEVENT
|
55
|
+
UID:efcb99ae-d540-419c-91fa-42cc2bd9d302
|
56
|
+
RRULE:FREQ=DAILY;INTERVAL=1
|
57
|
+
SUMMARY:Every day, except the 28th
|
58
|
+
DTSTART;VALUE=DATE:20140101
|
59
|
+
DTEND;VALUE=DATE:20140102
|
60
|
+
STATUS:CONFIRMED
|
61
|
+
CLASS:PUBLIC
|
62
|
+
X-MICROSOFT-CDO-ALLDAYEVENT:TRUE
|
63
|
+
TRANSP:TRANSPARENT
|
64
|
+
LAST-MODIFIED:20140113T200625Z
|
65
|
+
DTSTAMP:20140113T200625Z
|
66
|
+
SEQUENCE:0
|
67
|
+
EXDATE;VALUE=DATE:20140128
|
68
|
+
END:VEVENT
|
69
|
+
END:VCALENDAR
|
70
|
+
EOF
|
71
|
+
|
72
|
+
# An event that occurs every day, starting January 1, 2014 with one excluded
|
73
|
+
# date. January 28, 2014 will not appear in the occurrences.
|
74
|
+
calendars = Icalendar.parse(ics_string)
|
75
|
+
every_day_except_jan_28 = Array(calendars).first.events.first
|
76
|
+
puts "Every day except January 28, 2014, occurrences from 2014-01-01 to 2014-02-01:"
|
77
|
+
puts every_day_except_jan_28.occurrences_between(Date.parse("2014-01-01"), Date.parse("2014-02-01"))
|
78
|
+
```
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
1. Fork it ( http://github.com/<my-github-username>/icalendar-recurrence/fork )
|
83
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
84
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
85
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
86
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'icalendar/recurrence/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "icalendar-recurrence"
|
8
|
+
spec.version = Icalendar::Recurrence::VERSION
|
9
|
+
spec.authors = ["Jordan Raine"]
|
10
|
+
spec.email = ["jnraine@gmail.com"]
|
11
|
+
spec.summary = %q{Provides recurrence to icalendar gem.}
|
12
|
+
spec.homepage = "https://github.com/icalendar/icalendar-recurrence"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency 'icalendar', '~> 2.0.0.beta.1'
|
21
|
+
spec.add_runtime_dependency 'ice_cube', '~> 0.11.1'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.2.1'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 2.14.1'
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'tzinfo', '~> 0.3'
|
27
|
+
spec.add_development_dependency 'timecop', '~> 0.6.3'
|
28
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.2.8'
|
29
|
+
spec.add_development_dependency 'activesupport', '~> 4.0.4'
|
30
|
+
spec.add_development_dependency 'rspec-nc'
|
31
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative './icalendar/recurrence'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "icalendar"
|
2
|
+
require "icalendar/recurrence/version"
|
3
|
+
require "icalendar/recurrence/event_extensions"
|
4
|
+
require "icalendar/recurrence/weekday_extensions"
|
5
|
+
require "icalendar/recurrence/schedule"
|
6
|
+
require "icalendar/recurrence/time_util"
|
7
|
+
|
8
|
+
module Icalendar
|
9
|
+
module Recurrence
|
10
|
+
# Your code goes here...
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Recurrence
|
3
|
+
module EventExtensions
|
4
|
+
def start
|
5
|
+
dtstart
|
6
|
+
end
|
7
|
+
|
8
|
+
def start_time
|
9
|
+
TimeUtil.to_time(start)
|
10
|
+
end
|
11
|
+
|
12
|
+
def end
|
13
|
+
dtend
|
14
|
+
end
|
15
|
+
|
16
|
+
def occurrences_between(begin_time, closing_time)
|
17
|
+
schedule.occurrences_between(begin_time, closing_time)
|
18
|
+
end
|
19
|
+
|
20
|
+
def schedule
|
21
|
+
@schedule ||= Schedule.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def tzid
|
25
|
+
ugly_tzid = dtstart.ical_params.fetch("tzid", nil)
|
26
|
+
return nil if ugly_tzid.nil?
|
27
|
+
|
28
|
+
Array(ugly_tzid).first.to_s.gsub(/^(["'])|(["'])$/, "")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Event
|
34
|
+
include Icalendar::Recurrence::EventExtensions
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'ice_cube'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Recurrence
|
5
|
+
class Occurrence < Struct.new(:start_time, :end_time)
|
6
|
+
end
|
7
|
+
|
8
|
+
class Schedule
|
9
|
+
attr_reader :event
|
10
|
+
|
11
|
+
def initialize(event)
|
12
|
+
@event = event
|
13
|
+
end
|
14
|
+
|
15
|
+
def timezone
|
16
|
+
event.tzid
|
17
|
+
end
|
18
|
+
|
19
|
+
def rrules
|
20
|
+
event.rrule
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_time
|
24
|
+
TimeUtil.to_time(event.start)
|
25
|
+
end
|
26
|
+
|
27
|
+
def end_time
|
28
|
+
TimeUtil.to_time(event.end)
|
29
|
+
end
|
30
|
+
|
31
|
+
def occurrences_between(begin_time, closing_time)
|
32
|
+
ice_cube_occurrences = ice_cube_schedule.occurrences_between(TimeUtil.to_time(begin_time), TimeUtil.to_time(closing_time))
|
33
|
+
|
34
|
+
ice_cube_occurrences.map do |occurrence|
|
35
|
+
convert_ice_cube_occurrence(occurrence)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def convert_ice_cube_occurrence(ice_cube_occurrence)
|
40
|
+
if timezone
|
41
|
+
begin
|
42
|
+
tz = TZInfo::Timezone.get(timezone)
|
43
|
+
start_time = tz.local_to_utc(ice_cube_occurrence.start_time)
|
44
|
+
end_time = tz.local_to_utc(ice_cube_occurrence.end_time)
|
45
|
+
rescue TZInfo::InvalidTimezoneIdentifier => e
|
46
|
+
warn "Unknown TZID specified in ical event (#{timezone.inspect}), ignoring (will likely cause event to be at wrong time!)"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
start_time ||= ice_cube_occurrence.start_time
|
51
|
+
end_time ||= ice_cube_occurrence.end_time
|
52
|
+
|
53
|
+
Icalendar::Recurrence::Occurrence.new(start_time, end_time)
|
54
|
+
end
|
55
|
+
|
56
|
+
def ice_cube_schedule
|
57
|
+
schedule = IceCube::Schedule.new
|
58
|
+
schedule.start_time = start_time
|
59
|
+
schedule.end_time = end_time
|
60
|
+
|
61
|
+
rrules.each do |rrule|
|
62
|
+
ice_cube_recurrence_rule = convert_rrule_to_ice_cube_recurrence_rule(rrule)
|
63
|
+
schedule.add_recurrence_rule(ice_cube_recurrence_rule)
|
64
|
+
end
|
65
|
+
|
66
|
+
event.exdate.each do |exception_date|
|
67
|
+
exception_date = Time.parse(exception_date) if exception_date.is_a?(String)
|
68
|
+
schedule.add_exception_time(TimeUtil.to_time(exception_date))
|
69
|
+
end
|
70
|
+
|
71
|
+
schedule
|
72
|
+
end
|
73
|
+
|
74
|
+
def transform_byday_to_hash(byday_entries)
|
75
|
+
hashable_array = Array(byday_entries).map {|byday| convert_byday_to_ice_cube_day_of_week_hash(byday) }.flatten(1)
|
76
|
+
hash = Hash[*hashable_array]
|
77
|
+
|
78
|
+
if hash.values.include?([0]) # byday interval not specified (e.g., BYDAY=SA not BYDAY=1SA)
|
79
|
+
hash.keys
|
80
|
+
else
|
81
|
+
hash
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# private
|
86
|
+
|
87
|
+
|
88
|
+
def convert_rrule_to_ice_cube_recurrence_rule(rrule)
|
89
|
+
ice_cube_recurrence_rule = base_ice_cube_recurrence_rule(rrule.frequency, rrule.interval)
|
90
|
+
|
91
|
+
ice_cube_recurrence_rule.tap do |r|
|
92
|
+
days = transform_byday_to_hash(rrule.by_day)
|
93
|
+
|
94
|
+
r.month_of_year(rrule.by_month) unless rrule.by_month.nil?
|
95
|
+
r.day_of_month(rrule.by_month_day.map(&:to_i)) unless rrule.by_month_day.nil?
|
96
|
+
r.day_of_week(days) if days.is_a?(Hash) and !days.empty?
|
97
|
+
r.day(days) if days.is_a?(Array) and !days.empty?
|
98
|
+
r.until(TimeUtil.to_time(rrule.until)) if rrule.until
|
99
|
+
r.count(rrule.count)
|
100
|
+
end
|
101
|
+
|
102
|
+
ice_cube_recurrence_rule
|
103
|
+
end
|
104
|
+
|
105
|
+
def base_ice_cube_recurrence_rule(frequency, interval)
|
106
|
+
if frequency == "DAILY"
|
107
|
+
IceCube::DailyRule.new(interval)
|
108
|
+
elsif frequency == "WEEKLY"
|
109
|
+
IceCube::WeeklyRule.new(interval)
|
110
|
+
elsif frequency == "MONTHLY"
|
111
|
+
IceCube::MonthlyRule.new(interval)
|
112
|
+
elsif frequency == "YEARLY"
|
113
|
+
IceCube::YearlyRule.new(interval)
|
114
|
+
else
|
115
|
+
raise "Unknown frequency: #{rrule.frequency}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def convert_byday_to_ice_cube_day_of_week_hash(ical_byday)
|
120
|
+
data = parse_ical_byday(ical_byday)
|
121
|
+
day_code = data.fetch(:day_code)
|
122
|
+
position = data.fetch(:position)
|
123
|
+
|
124
|
+
day_symbol = case day_code.to_s
|
125
|
+
when "SU" then :sunday
|
126
|
+
when "MO" then :monday
|
127
|
+
when "TU" then :tuesday
|
128
|
+
when "WE" then :wednesday
|
129
|
+
when "TH" then :thursday
|
130
|
+
when "FR" then :friday
|
131
|
+
when "SA" then :saturday
|
132
|
+
else
|
133
|
+
raise ArgumentError.new "Unexpected ical_day: #{ical_day.inspect}"
|
134
|
+
end
|
135
|
+
|
136
|
+
[day_symbol, Array(position)]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parses ICAL BYDAY value to day and position array
|
140
|
+
# 1SA => {day_code: "SA", position: 1}
|
141
|
+
# MO => {day_code: "MO", position: nil
|
142
|
+
def parse_ical_byday(ical_byday)
|
143
|
+
match = ical_byday.match(/(\d*)([A-Z]{2})/)
|
144
|
+
{day_code: match[2], position: match[1].to_i}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'tzinfo'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Recurrence
|
5
|
+
module TimeUtil
|
6
|
+
def datetime_to_time(datetime)
|
7
|
+
raise ArgumentError, "Unsupported DateTime object passed (must be Icalendar::Values::DateTime#{datetime.class} passed instead)" unless supported_datetime_object?(datetime)
|
8
|
+
offset = timezone_offset(datetime.ical_params["tzid"], moment: datetime.to_date)
|
9
|
+
offset ||= datetime.strftime("%:z")
|
10
|
+
|
11
|
+
Time.new(datetime.year, datetime.month, datetime.mday, datetime.hour, datetime.min, datetime.sec, offset)
|
12
|
+
end
|
13
|
+
|
14
|
+
def date_to_time(date)
|
15
|
+
raise ArgumentError, "Must pass a Date object (#{date.class} passed instead)" unless supported_date_object?(date)
|
16
|
+
Time.new(date.year, date.month, date.mday)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_time(time_object)
|
20
|
+
if supported_time_object?(time_object)
|
21
|
+
time_object
|
22
|
+
elsif supported_datetime_object?(time_object)
|
23
|
+
datetime_to_time(time_object)
|
24
|
+
elsif supported_date_object?(time_object)
|
25
|
+
date_to_time(time_object)
|
26
|
+
elsif time_object.is_a?(String)
|
27
|
+
Time.parse(time_object)
|
28
|
+
else
|
29
|
+
raise ArgumentError, "Unsupported time object passed: #{time_object.inspect}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Calculates offset for given timezone ID (tzid). Optional, specify a
|
34
|
+
# moment in time to calulcate this offset. If no moment is specified,
|
35
|
+
# use the current time.
|
36
|
+
#
|
37
|
+
# # If done before daylight savings:
|
38
|
+
# TimeUtil.timezone_offset("America/Los_Angeles") => -08:00
|
39
|
+
# # Or after:
|
40
|
+
# TimeUtil.timezone_offset("America/Los_Angeles", moment: Time.parse("2014-04-01")) => -07:00
|
41
|
+
#
|
42
|
+
def timezone_offset(tzid, options = {})
|
43
|
+
tzid = Array(tzid).first
|
44
|
+
options = {moment: Time.now}.merge(options)
|
45
|
+
moment = options.fetch(:moment)
|
46
|
+
utc_moment = to_time(moment.clone).utc
|
47
|
+
tzid = tzid.to_s.gsub(/^(["'])|(["'])$/, "")
|
48
|
+
utc_offset = TZInfo::Timezone.get(tzid).period_for_utc(utc_moment).utc_total_offset # this seems to work, but I feel like there is a lurking bug
|
49
|
+
hour_offset = utc_offset/60/60
|
50
|
+
hour_offset = "+#{hour_offset}" if hour_offset >= 0
|
51
|
+
match = hour_offset.to_s.match(/(\+|-)(\d+)/)
|
52
|
+
"#{match[1]}#{match[2].rjust(2, "0")}:00"
|
53
|
+
rescue TZInfo::InvalidTimezoneIdentifier => e
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# See #timezone_offset_at_moment
|
58
|
+
def timezone_to_hour_minute_utc_offset(tzid, moment = Time.now)
|
59
|
+
timezone_offset(tzid, moment: moment)
|
60
|
+
end
|
61
|
+
|
62
|
+
def supported_date_object?(time_object)
|
63
|
+
time_object.is_a?(Date) or time_object.is_a?(Icalendar::Values::Date)
|
64
|
+
end
|
65
|
+
|
66
|
+
def supported_datetime_object?(time_object)
|
67
|
+
time_object.is_a?(Icalendar::Values::DateTime)
|
68
|
+
end
|
69
|
+
|
70
|
+
def supported_time_object?(time_object)
|
71
|
+
time_object.is_a?(Time)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Replaces the existing offset with one associated with given TZID. Does
|
75
|
+
# not change hour of day, only the offset. For example, if given a UTC
|
76
|
+
# time of 8am, the returned time object will still be 8am but in another
|
77
|
+
# timezone. See test for working examples.
|
78
|
+
def force_zone(time, tzid)
|
79
|
+
offset = timezone_offset(tzid, moment: time)
|
80
|
+
raise ArgumentError.new("Unknown TZID: #{tzid}") if offset.nil?
|
81
|
+
Time.new(time.year, time.month, time.mday, time.hour, time.min, time.sec, offset)
|
82
|
+
end
|
83
|
+
|
84
|
+
extend self
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Should we move this to a module and extend time?
|
90
|
+
class Time
|
91
|
+
def force_zone(tzid)
|
92
|
+
TimeUtil.force_zone(self, tzid)
|
93
|
+
end
|
94
|
+
end
|