vpim2 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.
- checksums.yaml +7 -0
- data/CHANGES +504 -0
- data/COPYING +58 -0
- data/README +182 -0
- data/lib/atom.rb +728 -0
- data/lib/plist.rb +22 -0
- data/lib/vpim.rb +13 -0
- data/lib/vpim/address.rb +219 -0
- data/lib/vpim/attachment.rb +102 -0
- data/lib/vpim/date.rb +222 -0
- data/lib/vpim/dirinfo.rb +277 -0
- data/lib/vpim/duration.rb +119 -0
- data/lib/vpim/enumerator.rb +32 -0
- data/lib/vpim/field.rb +614 -0
- data/lib/vpim/icalendar.rb +381 -0
- data/lib/vpim/maker/vcard.rb +16 -0
- data/lib/vpim/property/base.rb +193 -0
- data/lib/vpim/property/common.rb +315 -0
- data/lib/vpim/property/location.rb +38 -0
- data/lib/vpim/property/priority.rb +43 -0
- data/lib/vpim/property/recurrence.rb +69 -0
- data/lib/vpim/property/resources.rb +24 -0
- data/lib/vpim/repo.rb +181 -0
- data/lib/vpim/rfc2425.rb +367 -0
- data/lib/vpim/rrule.rb +591 -0
- data/lib/vpim/vcard.rb +1430 -0
- data/lib/vpim/version.rb +18 -0
- data/lib/vpim/vevent.rb +187 -0
- data/lib/vpim/view.rb +90 -0
- data/lib/vpim/vjournal.rb +58 -0
- data/lib/vpim/vpim.rb +65 -0
- data/lib/vpim/vtodo.rb +103 -0
- data/samples/README.mutt +93 -0
- data/samples/ab-query.rb +57 -0
- data/samples/cmd-itip.rb +156 -0
- data/samples/ex_cpvcard.rb +55 -0
- data/samples/ex_get_vcard_photo.rb +22 -0
- data/samples/ex_mkv21vcard.rb +34 -0
- data/samples/ex_mkvcard.rb +64 -0
- data/samples/ex_mkyourown.rb +29 -0
- data/samples/ics-dump.rb +210 -0
- data/samples/ics-to-rss.rb +84 -0
- data/samples/mutt-aliases-to-vcf.rb +45 -0
- data/samples/osx-wrappers.rb +86 -0
- data/samples/reminder.rb +203 -0
- data/samples/rrule.rb +71 -0
- data/samples/tabbed-file-to-vcf.rb +390 -0
- data/samples/vcf-dump.rb +86 -0
- data/samples/vcf-lines.rb +61 -0
- data/samples/vcf-to-ics.rb +22 -0
- data/samples/vcf-to-mutt.rb +121 -0
- data/test/test_all.rb +17 -0
- data/test/test_date.rb +120 -0
- data/test/test_dur.rb +41 -0
- data/test/test_field.rb +156 -0
- data/test/test_ical.rb +415 -0
- data/test/test_repo.rb +158 -0
- data/test/test_rrule.rb +1030 -0
- data/test/test_vcard.rb +973 -0
- data/test/test_view.rb +79 -0
- metadata +117 -0
data/lib/plist.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#--
|
2
|
+
##############################################################
|
3
|
+
# Copyright 2006, Ben Bleything <ben@bleything.net> and #
|
4
|
+
# Patrick May <patrick@hexane.org> #
|
5
|
+
# #
|
6
|
+
# Distributed under the MIT license. #
|
7
|
+
##############################################################
|
8
|
+
#++
|
9
|
+
# = Plist
|
10
|
+
#
|
11
|
+
# This is the main file for plist. Everything interesting happens in Plist and Plist::Emit.
|
12
|
+
|
13
|
+
require 'base64'
|
14
|
+
require 'cgi'
|
15
|
+
require 'stringio'
|
16
|
+
|
17
|
+
require 'plist/generator'
|
18
|
+
require 'plist/parser'
|
19
|
+
|
20
|
+
module Plist
|
21
|
+
VERSION = '3.0.0'
|
22
|
+
end
|
data/lib/vpim.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2008 Sam Roberts
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
# the existence of this file is a hack to support users or rubygems
|
10
|
+
|
11
|
+
require 'vpim/icalendar'
|
12
|
+
require 'vpim/vcard'
|
13
|
+
|
data/lib/vpim/address.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2008 Sam Roberts
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
=begin
|
10
|
+
|
11
|
+
Notes on a CAL-ADDRESS
|
12
|
+
|
13
|
+
When used with ATTENDEE, the parameters are:
|
14
|
+
CN
|
15
|
+
CUTYPE
|
16
|
+
DELEGATED-FROM
|
17
|
+
DELEGATED-TO
|
18
|
+
DIR
|
19
|
+
LANGUAGE
|
20
|
+
MEMBER
|
21
|
+
PARTSTAT
|
22
|
+
ROLE
|
23
|
+
RSVP
|
24
|
+
SENT-BY
|
25
|
+
|
26
|
+
When used with ORGANIZER, the parameters are:
|
27
|
+
CN
|
28
|
+
DIR
|
29
|
+
LANGUAGE
|
30
|
+
SENT-BY
|
31
|
+
|
32
|
+
|
33
|
+
What I've seen in Notes invitations, and iCal responses:
|
34
|
+
ROLE
|
35
|
+
PARTSTAT
|
36
|
+
RSVP
|
37
|
+
CN
|
38
|
+
|
39
|
+
Support these last 4, for now.
|
40
|
+
|
41
|
+
=end
|
42
|
+
|
43
|
+
module Vpim
|
44
|
+
class Icalendar
|
45
|
+
# Used to represent calendar fields containing CAL-ADDRESS values.
|
46
|
+
# The organizer or the attendees of a calendar event are examples of such
|
47
|
+
# a field.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
#
|
51
|
+
# ORGANIZER;CN="A. Person":mailto:a_person@example.com
|
52
|
+
#
|
53
|
+
# ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION
|
54
|
+
# ;CN="Sam Roberts";RSVP=TRUE:mailto:SRoberts@example.com
|
55
|
+
#
|
56
|
+
class Address
|
57
|
+
|
58
|
+
# Create a copy of Address. If the original Address was frozen, this one
|
59
|
+
# won't be.
|
60
|
+
def copy
|
61
|
+
#Marshal.load(Marshal.dump(self))
|
62
|
+
self.dup.dirty
|
63
|
+
end
|
64
|
+
|
65
|
+
def dirty #:nodoc:
|
66
|
+
@field = nil
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Addresses in a CAL-ADDRESS are represented as a URI, usually a mailto URI.
|
71
|
+
attr_accessor :uri
|
72
|
+
# The common or displayable name associated with the calendar address, or
|
73
|
+
# nil if there is none.
|
74
|
+
attr_accessor :cn
|
75
|
+
# The participation role for the calendar user specified by the address.
|
76
|
+
#
|
77
|
+
# The standard roles are:
|
78
|
+
# - CHAIR Indicates chair of the calendar entity
|
79
|
+
# - REQ-PARTICIPANT Indicates a participant whose participation is required
|
80
|
+
# - OPT-PARTICIPANT Indicates a participant whose participation is optional
|
81
|
+
# - NON-PARTICIPANT Indicates a participant who is copied for information purposes only
|
82
|
+
#
|
83
|
+
# The default role is REQ-PARTICIPANT, returned if no ROLE parameter was
|
84
|
+
# specified.
|
85
|
+
attr_accessor :role
|
86
|
+
# The participation status for the calendar user specified by the
|
87
|
+
# property PARTSTAT, a String.
|
88
|
+
#
|
89
|
+
# These are the participation statuses for an Event:
|
90
|
+
# - NEEDS-ACTION Event needs action
|
91
|
+
# - ACCEPTED Event accepted
|
92
|
+
# - DECLINED Event declined
|
93
|
+
# - TENTATIVE Event tentatively accepted
|
94
|
+
# - DELEGATED Event delegated
|
95
|
+
#
|
96
|
+
# Default is NEEDS-ACTION.
|
97
|
+
#
|
98
|
+
# FIXME - make the default depend on the component type.
|
99
|
+
attr_accessor :partstat
|
100
|
+
# The value of the RSVP field, either +true+ or +false+. It is used to
|
101
|
+
# specify whether there is an expectation of a reply from the calendar
|
102
|
+
# user specified by the property value.
|
103
|
+
attr_accessor :rsvp
|
104
|
+
|
105
|
+
def initialize(field=nil) #:nodoc:
|
106
|
+
@field = field
|
107
|
+
@uri = ''
|
108
|
+
@cn = ''
|
109
|
+
@role = "REQ-PARTICIPANT"
|
110
|
+
@partstat = "NEEDS-ACTION"
|
111
|
+
@rsvp = false
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a new Address. It will encode as a +name+ property.
|
115
|
+
def self.create(uri='')
|
116
|
+
adr = new
|
117
|
+
adr.uri = uri.to_str
|
118
|
+
adr
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.decode(field)
|
122
|
+
adr = new(field)
|
123
|
+
adr.uri = field.value
|
124
|
+
|
125
|
+
cn = field.param('CN')
|
126
|
+
|
127
|
+
if cn
|
128
|
+
adr.cn = cn.first
|
129
|
+
end
|
130
|
+
|
131
|
+
role = field.param('ROLE')
|
132
|
+
|
133
|
+
if role
|
134
|
+
adr.role = role.first.strip.upcase
|
135
|
+
end
|
136
|
+
|
137
|
+
partstat = field.param('PARTSTAT')
|
138
|
+
|
139
|
+
if partstat
|
140
|
+
adr.partstat = partstat.first.strip.upcase
|
141
|
+
end
|
142
|
+
|
143
|
+
rsvp = field.param('RSVP')
|
144
|
+
|
145
|
+
if rsvp
|
146
|
+
adr.rsvp = case rsvp.first
|
147
|
+
when /TRUE/i then true
|
148
|
+
when /FALSE/i then false
|
149
|
+
else raise InvalidEncodingError, "RSVP param value not TRUE/FALSE: #{rsvp}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
adr.freeze
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return a representation of this Address as a DirectoryInfo::Field.
|
157
|
+
def encode(name) #:nodoc:
|
158
|
+
if @field
|
159
|
+
# FIXME - set the field name, it could be different from cached
|
160
|
+
return @field
|
161
|
+
end
|
162
|
+
|
163
|
+
value = uri.to_str.strip
|
164
|
+
|
165
|
+
if value.empty?
|
166
|
+
raise Uencodeable, "Address#uri is zero-length"
|
167
|
+
end
|
168
|
+
|
169
|
+
params = {}
|
170
|
+
|
171
|
+
if cn.length > 0
|
172
|
+
params['CN'] = Vpim::encode_paramvalue(cn)
|
173
|
+
end
|
174
|
+
|
175
|
+
# FIXME - default value is different for non-vEvent
|
176
|
+
if role.length > 0 && role != 'REQ-PARTICIPANT'
|
177
|
+
params['ROLE'] = Vpim::encode_paramtext(role)
|
178
|
+
end
|
179
|
+
|
180
|
+
# FIXME - default value is different for non-vEvent
|
181
|
+
if partstat.length > 0 && partstat != 'NEEDS-ACTION'
|
182
|
+
params['PARTSTAT'] = Vpim::encode_paramtext(partstat)
|
183
|
+
end
|
184
|
+
|
185
|
+
if rsvp
|
186
|
+
params['RSVP'] = 'true'
|
187
|
+
end
|
188
|
+
|
189
|
+
Vpim::DirectoryInfo::Field.create(name, value, params)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return true if the +uri+ is == to this address' URI. The comparison
|
193
|
+
# is case-insensitive (because email addresses and domain names are).
|
194
|
+
def ==(uri)
|
195
|
+
# TODO - could I use a URI library?
|
196
|
+
Vpim::Methods.casecmp?(self.uri.to_str, uri.to_str)
|
197
|
+
end
|
198
|
+
|
199
|
+
# A string representation of an address, using the common name, and the
|
200
|
+
# URI. The URI protocol is stripped if it's "mailto:".
|
201
|
+
def to_s
|
202
|
+
u = uri
|
203
|
+
u = u.gsub(/^mailto: */i, '')
|
204
|
+
|
205
|
+
if cn.length > 0
|
206
|
+
"#{cn.inspect} <#{uri}>"
|
207
|
+
else
|
208
|
+
uri
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def inspect #:nodoc:
|
213
|
+
"#<Vpim::Icalendar::Address:cn=#{cn.inspect} status=#{partstat} rsvp=#{rsvp} #{uri.inspect}>"
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2008 Sam Roberts
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'vpim/icalendar'
|
10
|
+
|
11
|
+
module Vpim
|
12
|
+
|
13
|
+
# Attachments are used by both iCalendar and vCard. They are either a URI or
|
14
|
+
# inline data, and their decoded value will be either a Uri or a Inline, as
|
15
|
+
# appropriate.
|
16
|
+
#
|
17
|
+
# Besides the methods specific to their class, both kinds of object implement
|
18
|
+
# a set of common methods, allowing them to be treated uniformly:
|
19
|
+
# - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
|
20
|
+
# - Uri#to_s, Inline#to_s: return the value as a String.
|
21
|
+
# - Uri#format, Inline#format: the format of the value. This is supposed to
|
22
|
+
# be an "iana defined" identifier (like "image/jpeg"), but could be almost
|
23
|
+
# anything (or nothing) in practice. Since the parameter is optional, it may
|
24
|
+
# be "".
|
25
|
+
#
|
26
|
+
# The objects can also be distinguished by their class, if necessary.
|
27
|
+
module Attachment
|
28
|
+
|
29
|
+
# TODO - It might be possible to autodetect the format from the first few
|
30
|
+
# bytes of the value, and return the appropriate MIME type when format
|
31
|
+
# isn't defined.
|
32
|
+
#
|
33
|
+
# iCalendar and vCard put the format in different parameters, and the
|
34
|
+
# default kind of value is different.
|
35
|
+
def Attachment.decode(field, defkind, fmtparam) #:nodoc:
|
36
|
+
format = field.pvalue(fmtparam) || ''
|
37
|
+
kind = field.kind || defkind
|
38
|
+
case kind
|
39
|
+
when 'text'
|
40
|
+
Inline.new(Vpim.decode_text(field.value), format)
|
41
|
+
when 'uri'
|
42
|
+
Uri.new(field.value_raw, format)
|
43
|
+
when 'binary'
|
44
|
+
Inline.new(field.value, format)
|
45
|
+
else
|
46
|
+
raise InvalidEncodingError, "Attachment of type #{kind} is not allowed"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Extends a String to support some of the same methods as Uri.
|
51
|
+
class Inline < String
|
52
|
+
def initialize(s, format) #:nodoc:
|
53
|
+
@format = format
|
54
|
+
super(s)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return an IO object for the inline data. See +stringio+ for more
|
58
|
+
# information.
|
59
|
+
def to_io
|
60
|
+
StringIO.new(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
# The format of the inline data.
|
64
|
+
# See Attachment.
|
65
|
+
attr_reader :format
|
66
|
+
end
|
67
|
+
|
68
|
+
# Encapsulates a URI and implements some methods of String.
|
69
|
+
class Uri
|
70
|
+
def initialize(uri, format) #:nodoc:
|
71
|
+
@uri = uri
|
72
|
+
@format = format
|
73
|
+
end
|
74
|
+
|
75
|
+
# The URI value.
|
76
|
+
attr_reader :uri
|
77
|
+
|
78
|
+
# The format of the data referred to by the URI.
|
79
|
+
# See Attachment.
|
80
|
+
attr_reader :format
|
81
|
+
|
82
|
+
# Return an IO object from opening the URI. See +open-uri+ for more
|
83
|
+
# information.
|
84
|
+
def to_io
|
85
|
+
open(@uri)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return the String from reading the IO object to end-of-data.
|
89
|
+
def to_s
|
90
|
+
to_io.read(nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
def inspect #:nodoc:
|
94
|
+
s = "<#{self.class.to_s}: #{uri.inspect}>"
|
95
|
+
s << ", #{@format.inspect}" if @format
|
96
|
+
s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
data/lib/vpim/date.rb
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2008 Sam Roberts
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'date'
|
10
|
+
|
11
|
+
# Extensions to the standard library Date.
|
12
|
+
class Date
|
13
|
+
|
14
|
+
TIME_START = Date.new(1970, 1, 1)
|
15
|
+
SECS_PER_DAY = 24 * 60 * 60
|
16
|
+
|
17
|
+
# Converts this object to a Time object, or throws an ArgumentError if
|
18
|
+
# conversion is not possible because it is before the start of epoch.
|
19
|
+
def vpim_to_time
|
20
|
+
raise ArgumentError, 'date is before the start of system time' if self < TIME_START
|
21
|
+
days = self - TIME_START
|
22
|
+
|
23
|
+
Time.at((days * SECS_PER_DAY).to_i)
|
24
|
+
end
|
25
|
+
|
26
|
+
# If wday responds to to_str, convert it to the wday number by searching for
|
27
|
+
# a wday that matches, using as many characters as are in wday to do the
|
28
|
+
# comparison. wday must be 2 or more characters long in order to be a unique
|
29
|
+
# match, other than that, "mo", "Mon", and "MonDay" are all valid strings
|
30
|
+
# for wday 1.
|
31
|
+
#
|
32
|
+
# This method can be called on a valid wday, and it will return it. Perhaps
|
33
|
+
# it should be called by default inside the Date#new*() methods so that
|
34
|
+
# non-integer wday arguments can be used? Perhaps a similar method should
|
35
|
+
# exist for months? But with months, we all know January is 1, who can
|
36
|
+
# remember where Date chooses to start its wday count!
|
37
|
+
#
|
38
|
+
# Examples:
|
39
|
+
# Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in
|
40
|
+
# February
|
41
|
+
# Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice
|
42
|
+
# that a valid wday integer can be passed right through.
|
43
|
+
#
|
44
|
+
def Date.str2wday(wdaystr)
|
45
|
+
return wdaystr unless wdaystr.respond_to? :to_str
|
46
|
+
|
47
|
+
str = wdaystr.to_str.upcase
|
48
|
+
if str.length < 2
|
49
|
+
raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name'
|
50
|
+
end
|
51
|
+
|
52
|
+
wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str)
|
53
|
+
|
54
|
+
return wday if wday
|
55
|
+
|
56
|
+
raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name'
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Create a new Date object for the date specified by year +year+, month
|
61
|
+
# +mon+, and day-of-the-week +wday+.
|
62
|
+
#
|
63
|
+
# The nth, +n+, occurrence of +wday+ within the period will be generated
|
64
|
+
# (+n+ defaults to 1). If +n+ is positive, the nth occurrence from the
|
65
|
+
# beginning of the period will be returned, if negative, the nth occurrence
|
66
|
+
# from the end of the period will be returned.
|
67
|
+
#
|
68
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
69
|
+
# that month.
|
70
|
+
#
|
71
|
+
# Examples:
|
72
|
+
# - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004
|
73
|
+
# - Date.bywday(2004, nil, 1) => the first Sunday of 2004
|
74
|
+
# - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004
|
75
|
+
# - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004
|
76
|
+
# - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004
|
77
|
+
# - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004
|
78
|
+
#
|
79
|
+
# Compare this to Date.new, which allows a Date to be created by
|
80
|
+
# day-of-the-month, mday, to Date.ordinal, which allows a Date to be created by
|
81
|
+
# day-of-the-year, yday, and to Date.commercial, which allows a Date to be created
|
82
|
+
# by day-of-the-week, but within a specific week.
|
83
|
+
def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY)
|
84
|
+
# Normalize mon to 1-12.
|
85
|
+
if mon
|
86
|
+
if mon > 12 || mon == 0 || mon < -12
|
87
|
+
raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12"
|
88
|
+
end
|
89
|
+
if mon < 0
|
90
|
+
mon = 13 + mon
|
91
|
+
end
|
92
|
+
end
|
93
|
+
if wday < 0 || wday > 6
|
94
|
+
raise ArgumentError, 'wday must be in range 0-6, or a weekday name'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Determine direction of indexing.
|
98
|
+
inc = n <=> 0
|
99
|
+
if inc == 0
|
100
|
+
raise ArgumentError, 'n must be greater or less than zero'
|
101
|
+
end
|
102
|
+
|
103
|
+
# if !mon, n is index into year, but direction of search is determined by
|
104
|
+
# sign of n
|
105
|
+
d = Date.new(year, mon ? mon : inc, inc, sg)
|
106
|
+
|
107
|
+
while d.wday != wday
|
108
|
+
d += inc
|
109
|
+
end
|
110
|
+
|
111
|
+
# Now we have found the first/last day with the correct wday, search
|
112
|
+
# for nth occurrence, by jumping by n.abs-1 weeks forward or backward.
|
113
|
+
d += 7 * (n.abs - 1) * inc
|
114
|
+
|
115
|
+
if d.year != year
|
116
|
+
raise ArgumentError, 'n is out of bounds of year'
|
117
|
+
end
|
118
|
+
if mon && d.mon != mon
|
119
|
+
raise ArgumentError, 'n is out of bounds of month'
|
120
|
+
end
|
121
|
+
d
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return the first day of the week for the specified date. Commercial weeks
|
125
|
+
# start on Monday, but the weekstart can be specified (as 0-6, where 0 is
|
126
|
+
# sunday, or in formate of Date.str2day).
|
127
|
+
def Date.weekstart(year, mon, day, weekstart="MO")
|
128
|
+
wkst = Date.str2wday(weekstart)
|
129
|
+
d = Date.new(year, mon, day)
|
130
|
+
until d.wday == wkst
|
131
|
+
d = d - 1
|
132
|
+
end
|
133
|
+
d
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# DateGen generates arrays of dates matching simple criteria.
|
138
|
+
class DateGen
|
139
|
+
|
140
|
+
# Generate an array of a week's dates, where week is specified by year, mon,
|
141
|
+
# day, and the weekstart (the day-of-week that is considered the "first" day
|
142
|
+
# of that week, 0-6, where 0 is sunday).
|
143
|
+
def DateGen.weekofdate(year, mon, day, weekstart)
|
144
|
+
d = Date.weekstart(year, mon, day, weekstart)
|
145
|
+
week = []
|
146
|
+
7.times do
|
147
|
+
week << d
|
148
|
+
d = d + 1
|
149
|
+
end
|
150
|
+
week
|
151
|
+
end
|
152
|
+
|
153
|
+
# Generate an array of dates on +wday+ (the day-of-week,
|
154
|
+
# 0-6, where 0 is Sunday).
|
155
|
+
#
|
156
|
+
# If +n+ is specified, only the nth occurrence of +wday+ within the period
|
157
|
+
# will be generated. If +n+ is positive, the nth occurrence from the
|
158
|
+
# beginning of the period will be returned, if negative, the nth occurrence
|
159
|
+
# from the end of the period will be returned.
|
160
|
+
#
|
161
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
162
|
+
# that month.
|
163
|
+
#
|
164
|
+
# Examples:
|
165
|
+
# - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004
|
166
|
+
# - DateGen.bywday(2004, nil, 1) => all Sundays in 2004
|
167
|
+
# - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004
|
168
|
+
# - DateGen.bywday(2004, 12, 1) => all sundays in December 2004
|
169
|
+
# - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004
|
170
|
+
# - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004
|
171
|
+
#
|
172
|
+
# Compare to Date.bywday(), which allows a single Date to be created with
|
173
|
+
# similar criteria.
|
174
|
+
def DateGen.bywday(year, month, wday, n = nil)
|
175
|
+
seed = Date.bywday(year, month, wday, n ? n : 1)
|
176
|
+
|
177
|
+
dates = [ seed ]
|
178
|
+
|
179
|
+
return dates if n
|
180
|
+
|
181
|
+
succ = seed.clone
|
182
|
+
|
183
|
+
# Collect all matches until we're out of the year (or month, if specified)
|
184
|
+
loop do
|
185
|
+
succ += 7
|
186
|
+
|
187
|
+
break if succ.year != year
|
188
|
+
break if month && succ.month != seed.month
|
189
|
+
|
190
|
+
dates.push succ
|
191
|
+
end
|
192
|
+
dates.sort!
|
193
|
+
dates
|
194
|
+
end
|
195
|
+
|
196
|
+
# Generate an array of dates on +mday+ (the day-of-month, 1-31). For months
|
197
|
+
# in which the +mday+ is not present, no date will be generated.
|
198
|
+
#
|
199
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
200
|
+
# that month.
|
201
|
+
#
|
202
|
+
# Compare to Date.new(), which allows a single Date to be created with
|
203
|
+
# similar criteria.
|
204
|
+
def DateGen.bymonthday(year, month, mday)
|
205
|
+
months = month ? [ month ] : 1..12
|
206
|
+
dates = [ ]
|
207
|
+
|
208
|
+
months.each do |m|
|
209
|
+
begin
|
210
|
+
dates << Date.new(year, m, mday)
|
211
|
+
rescue ArgumentError
|
212
|
+
# Don't generate dates for invalid combinations (Feb 29, when it's not
|
213
|
+
# a leap year, for example).
|
214
|
+
#
|
215
|
+
# TODO - should we raise when month is out of range, or mday can never
|
216
|
+
# be in range (32)?
|
217
|
+
end
|
218
|
+
end
|
219
|
+
dates
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|