mumboe-vpim 0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGES +510 -0
  2. data/COPYING +58 -0
  3. data/README +185 -0
  4. data/lib/vpim/address.rb +219 -0
  5. data/lib/vpim/agent/atomize.rb +104 -0
  6. data/lib/vpim/agent/base.rb +73 -0
  7. data/lib/vpim/agent/calendars.rb +173 -0
  8. data/lib/vpim/agent/handler.rb +26 -0
  9. data/lib/vpim/agent/ics.rb +161 -0
  10. data/lib/vpim/attachment.rb +102 -0
  11. data/lib/vpim/date.rb +222 -0
  12. data/lib/vpim/dirinfo.rb +277 -0
  13. data/lib/vpim/duration.rb +119 -0
  14. data/lib/vpim/enumerator.rb +32 -0
  15. data/lib/vpim/field.rb +614 -0
  16. data/lib/vpim/icalendar.rb +384 -0
  17. data/lib/vpim/maker/vcard.rb +16 -0
  18. data/lib/vpim/property/base.rb +193 -0
  19. data/lib/vpim/property/common.rb +315 -0
  20. data/lib/vpim/property/location.rb +38 -0
  21. data/lib/vpim/property/priority.rb +43 -0
  22. data/lib/vpim/property/recurrence.rb +69 -0
  23. data/lib/vpim/property/resources.rb +24 -0
  24. data/lib/vpim/repo.rb +261 -0
  25. data/lib/vpim/rfc2425.rb +367 -0
  26. data/lib/vpim/rrule.rb +591 -0
  27. data/lib/vpim/time.rb +40 -0
  28. data/lib/vpim/vcard.rb +1456 -0
  29. data/lib/vpim/version.rb +18 -0
  30. data/lib/vpim/vevent.rb +187 -0
  31. data/lib/vpim/view.rb +90 -0
  32. data/lib/vpim/vjournal.rb +58 -0
  33. data/lib/vpim/vpim.rb +65 -0
  34. data/lib/vpim/vtodo.rb +103 -0
  35. data/lib/vpim.rb +13 -0
  36. data/samples/README.mutt +93 -0
  37. data/samples/ab-query.rb +57 -0
  38. data/samples/agent.ru +10 -0
  39. data/samples/cmd-itip.rb +156 -0
  40. data/samples/ex_cpvcard.rb +55 -0
  41. data/samples/ex_get_vcard_photo.rb +22 -0
  42. data/samples/ex_mkv21vcard.rb +34 -0
  43. data/samples/ex_mkvcard.rb +64 -0
  44. data/samples/ex_mkyourown.rb +29 -0
  45. data/samples/ics-dump.rb +210 -0
  46. data/samples/ics-to-rss.rb +84 -0
  47. data/samples/mutt-aliases-to-vcf.rb +45 -0
  48. data/samples/osx-wrappers.rb +86 -0
  49. data/samples/reminder.rb +209 -0
  50. data/samples/rrule.rb +71 -0
  51. data/samples/tabbed-file-to-vcf.rb +390 -0
  52. data/samples/vcf-dump.rb +86 -0
  53. data/samples/vcf-lines.rb +61 -0
  54. data/samples/vcf-to-ics.rb +22 -0
  55. data/samples/vcf-to-mutt.rb +121 -0
  56. data/test/test_agent_atomize.rb +84 -0
  57. data/test/test_agent_calendars.rb +128 -0
  58. data/test/test_agent_ics.rb +96 -0
  59. data/test/test_all.rb +17 -0
  60. data/test/test_date.rb +120 -0
  61. data/test/test_dur.rb +41 -0
  62. data/test/test_field.rb +156 -0
  63. data/test/test_ical.rb +437 -0
  64. data/test/test_misc.rb +13 -0
  65. data/test/test_repo.rb +129 -0
  66. data/test/test_rrule.rb +1030 -0
  67. data/test/test_vcard.rb +973 -0
  68. data/test/test_view.rb +79 -0
  69. metadata +140 -0
@@ -0,0 +1,173 @@
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 "cgi"
10
+ require "uri"
11
+
12
+ require "vpim/repo"
13
+ require "vpim/agent/atomize"
14
+
15
+ module Vpim
16
+ module Agent
17
+ # On failure, raise this with an error message. text/plain for now,
18
+ # text/html later. Will convert to a 404 and a message.
19
+ class NotFound < Exception
20
+ def initialize(name, path)
21
+ super %{Resource "#{name}" under "#{path.prefix}" was not found!}
22
+ end
23
+ end
24
+
25
+ class Path
26
+ def self.split_path(path)
27
+ begin
28
+ path = path.to_ary
29
+ rescue NameError
30
+ path = path.split("/")
31
+ end
32
+ path.map{|w| CGI.unescape(w)}
33
+ end
34
+
35
+ # URI is the uri being queried, base is where this path is mounted under?
36
+ def initialize(uri, base = "")
37
+ @uri = URI.parse(uri.to_s)
38
+ #pp [uri, base, @uri]
39
+ if @uri.path.size == 0
40
+ @uri.path = "/"
41
+ end
42
+ @path = Path.split_path(@uri.path)
43
+ @base = base.to_str
44
+ @mark = 0
45
+
46
+ @base.split.size.times{ shift }
47
+ end
48
+
49
+ def uri
50
+ @uri.to_s
51
+ end
52
+
53
+ def to_path
54
+ self
55
+ end
56
+
57
+ # TODO - call this #next
58
+ def shift
59
+ if @path[@mark]
60
+ @path[@mark += 1]
61
+ end
62
+ end
63
+
64
+ def append(name, scheme = nil)
65
+ uri = @uri.dup
66
+ uri.path += "/" + CGI.escape(name)
67
+ if scheme
68
+ uri.scheme = scheme
69
+ end
70
+ uri
71
+ end
72
+
73
+ def prefix(len = nil)
74
+ len ||= @mark
75
+ @path[0, len].map{|p| CGI.escape(p)}.join("/") + "/"
76
+ end
77
+
78
+ end
79
+
80
+ module Form
81
+ ATOM = Atomize::MIME
82
+ HTML = "text/html"
83
+ ICS = "text/calendar"
84
+ PLAIN = "text/plain"
85
+ VCF = "text/directory"
86
+ end
87
+
88
+ # Return an HTML description of a list of resources accessible under this
89
+ # path.
90
+ class ResourceList
91
+ def initialize(description, items)
92
+ @description = description
93
+ @items = items
94
+ end
95
+
96
+ def get(path)
97
+ return <<__, Form::HTML
98
+ <html><body>
99
+ #{@description}
100
+ <ul>
101
+ #{
102
+ @items.map do |name,description,scheme|
103
+ "<li><a href=\"#{path.append(name,scheme)}\">#{description || name}</a></li>\n"
104
+ end
105
+ }
106
+ </ul>
107
+ </body></html>
108
+ __
109
+ end
110
+ end
111
+
112
+ # Return calendar information based on RESTful (lovein' the jargon...)
113
+ # paths. Input is a Vpim::Repo.
114
+ #
115
+ # .../coding/month/atom
116
+ # .../coding/events/month/ics <- next month?
117
+ # .../coding/events/month/2008-04/ics <- a specified month?
118
+ # .../coding/week/atom
119
+ # .../year/atom
120
+ class Calendars
121
+ def initialize(repo)
122
+ @repo = repo
123
+ end
124
+
125
+ class Calendar
126
+ def initialize(cal)
127
+ @cal = cal
128
+ @list = ResourceList.new(
129
+ "Calendar #{@cal.name.inspect}:",
130
+ [
131
+ ["calendar", "download"],
132
+ ["calendar", "subscription", "webcal"],
133
+ ["atom", "syndication"],
134
+ ]
135
+ )
136
+ end
137
+
138
+ def get(path)
139
+ form = path.shift
140
+
141
+ # TODO should redirect to an object, so that extra paths can be
142
+ # handled more gracefully.
143
+ case form
144
+ when nil
145
+ return @list.get(path)
146
+ when "calendar"
147
+ return @cal.encode, Form::ICS
148
+ when "atom"
149
+ return Atomize.calendar(@cal, path.uri, nil, @cal.name).to_xml, Form::ATOM
150
+ else
151
+ raise NotFound.new(form, path)
152
+ end
153
+ end
154
+ end
155
+
156
+ # Get object at this path. Return value is a tuple of data and mime content type.
157
+ def get(path)
158
+ case name = path.to_path.shift
159
+ when nil
160
+ list = ResourceList.new("Calendars:", @repo.map{|c| c.name})
161
+ return list.get(path)
162
+ else
163
+ if cal = @repo.find{|c| c.name == name}
164
+ return Calendar.new(cal).get(path)
165
+ else
166
+ raise NotFound.new(name, path)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
@@ -0,0 +1,26 @@
1
+ =begin
2
+ Copyright (C) 2009 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 'sinatra/base'
10
+
11
+ # Auto-choose our handler based on the environment.
12
+ # TODO Code should be in Sinatra, and should handle Thin, Mongrel, etc.
13
+ Sinatra::Base.configure do
14
+ server = Sinatra::Base.server
15
+ Sinatra::Base.set :server, Proc.new {
16
+ if ENV.include?("PHP_FCGI_CHILDREN")
17
+ break "fastcgi" # Must NOT be the correct class name!
18
+ elsif ENV.include?("REQUEST_METHOD")
19
+ break "cgi" # Must NOT be the correct class name!
20
+ else
21
+ # Fall back on whatever it was going to be.
22
+ server
23
+ end
24
+ }
25
+ end
26
+
@@ -0,0 +1,161 @@
1
+ =begin
2
+ Copyright (C) 2009 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 'cgi'
10
+
11
+ require 'vpim/agent/base'
12
+ require 'vpim/agent/atomize'
13
+ require 'vpim/repo'
14
+ require 'vpim/view'
15
+
16
+ require 'sinatra/base'
17
+
18
+ module Vpim
19
+ module Agent
20
+
21
+ class Ics < Base
22
+ use_in_file_templates!
23
+
24
+ def atomize(caluri, feeduri)
25
+ repo = Vpim::Repo::Uri.new(caluri)
26
+ cal = repo.find{true}
27
+ cal = View.week(cal)
28
+ feed = Agent::Atomize.calendar(cal, feeduri, caluri, cal.name)
29
+ return feed.to_xml, Agent::Atomize::MIME
30
+ end
31
+
32
+ ## Route handlers:
33
+ def get_base(from)
34
+ @url_base = script_url # agent mount point
35
+ @url_ics = from # ics from here
36
+ @url_atom = nil # atom feed from here, if ics is accessible
37
+ @url_error= nil # error message, if is is not accessible
38
+
39
+ if not from.empty?
40
+ begin
41
+ atomize(from, "http://example.com")
42
+ @url_atom = @url_base + "/atom" + "?" + from
43
+ rescue
44
+ @url_error = CGI.escapeHTML($!.to_s)
45
+ end
46
+ end
47
+
48
+ haml :"vpim/agent/ics/view"
49
+ end
50
+
51
+ # When we support other forms..
52
+ #get '/ics/:form' do
53
+ # form = params[:form]
54
+ def get_atom(caluri)
55
+ if caluri.empty?
56
+ redirect script_url
57
+ end
58
+
59
+ feeduri = script_url + "/atom?" + caluri
60
+
61
+ begin
62
+ xml, xmltype = atomize(caluri, feeduri)
63
+ content_type xmltype
64
+ body xml
65
+ rescue
66
+ redirect script_url + "?" + caluri
67
+ end
68
+ end
69
+
70
+ def get_style
71
+ content_type 'text/css'
72
+ css :"vpim/agent/ics/style"
73
+ end
74
+
75
+ ## Sinatra routing:
76
+ get '/?' do
77
+ get_base(env['QUERY_STRING'])
78
+ end
79
+
80
+ post "/?" do
81
+ redirect script_url + "?" + (params[:url] || "")
82
+ end
83
+
84
+ get "/atom" do
85
+ get_atom(env['QUERY_STRING'])
86
+ end
87
+
88
+ get '/style.css' do
89
+ get_style
90
+ end
91
+
92
+ end # Ics
93
+
94
+ end # Agent
95
+ end # Vpim
96
+
97
+ __END__
98
+ @@vpim/agent/ics/style
99
+ body {
100
+ background-color: gray;
101
+ }
102
+ h1 {
103
+ border-bottom: 3px solid #8B0000;
104
+ font-size: large;
105
+ }
106
+ form {
107
+ margin-left: 10%;
108
+ }
109
+ input.text {
110
+ width: 80%;
111
+ }
112
+ a {
113
+ color: black;
114
+ }
115
+ a:hover {
116
+ color: #8B0000;
117
+ }
118
+ tt {
119
+ margin-left: 10%;
120
+ }
121
+ .footer {
122
+ border-top: 3px solid #8B0000;
123
+ }
124
+ @@vpim/agent/ics/view
125
+ !!! strict
126
+ %html
127
+ %head
128
+ %title Subscribe to calendar feeds as atom feeds
129
+ %link{:href => script_url + "/style.css", :media => "screen", :type => "text/css"}
130
+ %meta{:"http-equiv" => "Content-Type", :content => "text/html;charset=utf-8"}
131
+ %body
132
+ %h1 Subscribe to calendar feeds as atom feeds
133
+ %p
134
+ Calendar feeds are great, but when you want a reminder of what's coming up
135
+ in the next week, you might want those events as an atom feed.
136
+ %p
137
+ Paste the URL of the calendar below, submit it, and subscribe.
138
+ %form{:method => 'POST', :action => script_url}
139
+ %p
140
+ %input.text{:type => 'text', :name => 'url', :value => @url_ics}
141
+ %input{:type => 'submit', :value => 'Submit'}
142
+ - if @url_atom
143
+ %p
144
+ Subscribe to
145
+ %a{:href => @url_ics}= @url_ics
146
+ as:
147
+ %ul.feed
148
+ %li
149
+ %a{:href => @url_atom}= @url_atom
150
+ (atom feed)
151
+ - if @url_error
152
+ %p
153
+ Sorry, trying to access
154
+ %tt=@url_ics
155
+ resulted in:
156
+ %p
157
+ %tt= @url_error
158
+ .footer
159
+ :textile
160
+ Brought from the "Octet Cloud":http://octetcloud.com/ using "vPim":http://vpim.rubyforge.org/, by cloud monkey "Sam Roberts":mailto:vieuxtech@gmail.com.
161
+
@@ -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
+