icalendar-recurrence 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/icalendar/icalendar-recurrence.svg?branch=master)](https://travis-ci.org/icalendar/icalendar-recurrence) [![Code Climate](https://codeclimate.com/github/icalendar/icalendar-recurrence.png)](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
|