smart_month 1.0.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.
- data/MIT-LICENSE +20 -0
- data/README +33 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/lib/month.rb +26 -0
- data/lib/smart_month.rb +11 -0
- data/lib/smart_month/calculations.rb +87 -0
- data/lib/smart_month/collection.rb +51 -0
- data/lib/smart_month/extensions/date.rb +19 -0
- data/lib/smart_month/magic.rb +71 -0
- data/lib/smart_month/math.rb +41 -0
- data/lib/smart_month/rulesets.rb +151 -0
- data/lib/smart_month/util.rb +37 -0
- data/pkg/smart_month-1.0.0.gem +0 -0
- data/rdoc/classes/Date.html +209 -0
- data/rdoc/classes/Month.html +189 -0
- data/rdoc/classes/SmartMonth.html +136 -0
- data/rdoc/classes/SmartMonth/Calculations.html +383 -0
- data/rdoc/classes/SmartMonth/Collection.html +306 -0
- data/rdoc/classes/SmartMonth/Magic.html +214 -0
- data/rdoc/classes/SmartMonth/Magic/MonthFactory.html +178 -0
- data/rdoc/classes/SmartMonth/Math.html +311 -0
- data/rdoc/classes/SmartMonth/Rulesets.html +419 -0
- data/rdoc/classes/SmartMonth/Util.html +302 -0
- data/rdoc/classes/Time.html +152 -0
- data/rdoc/created.rid +1 -0
- data/rdoc/files/README.html +170 -0
- data/rdoc/files/lib/month_rb.html +113 -0
- data/rdoc/files/lib/smart_month/calculations_rb.html +101 -0
- data/rdoc/files/lib/smart_month/collection_rb.html +101 -0
- data/rdoc/files/lib/smart_month/extensions/date_rb.html +108 -0
- data/rdoc/files/lib/smart_month/magic_rb.html +101 -0
- data/rdoc/files/lib/smart_month/math_rb.html +101 -0
- data/rdoc/files/lib/smart_month/rulesets_rb.html +108 -0
- data/rdoc/files/lib/smart_month/util_rb.html +101 -0
- data/rdoc/files/lib/smart_month_rb.html +108 -0
- data/rdoc/fr_class_index.html +37 -0
- data/rdoc/fr_file_index.html +36 -0
- data/rdoc/fr_method_index.html +74 -0
- data/rdoc/index.html +24 -0
- data/rdoc/rdoc-style.css +208 -0
- data/smart_month.gemspec +179 -0
- data/test/spec/date/accessor_spec.rb +91 -0
- data/test/spec/date/add_month_spec.rb +24 -0
- data/test/spec/date/add_spec.rb +23 -0
- data/test/spec/date/boat_spec.rb +20 -0
- data/test/spec/date/civil_spec.rb +28 -0
- data/test/spec/date/commercial_spec.rb +29 -0
- data/test/spec/date/constants_spec.rb +41 -0
- data/test/spec/date/conversions_spec.rb +153 -0
- data/test/spec/date/downto_spec.rb +18 -0
- data/test/spec/date/eql_spec.rb +10 -0
- data/test/spec/date/gregorian_spec.rb +28 -0
- data/test/spec/date/hash_spec.rb +14 -0
- data/test/spec/date/infinity_spec.rb +77 -0
- data/test/spec/date/julian_spec.rb +52 -0
- data/test/spec/date/minus_month_spec.rb +23 -0
- data/test/spec/date/minus_spec.rb +30 -0
- data/test/spec/date/new_spec.rb +9 -0
- data/test/spec/date/neww_spec.rb +9 -0
- data/test/spec/date/ordinal_spec.rb +29 -0
- data/test/spec/date/parse_spec.rb +149 -0
- data/test/spec/date/relationship_spec.rb +20 -0
- data/test/spec/date/shared/civil.rb +66 -0
- data/test/spec/date/shared/commercial.rb +39 -0
- data/test/spec/date/shared/parse.rb +54 -0
- data/test/spec/date/shared/parse_eu.rb +30 -0
- data/test/spec/date/shared/parse_us.rb +29 -0
- data/test/spec/date/step_spec.rb +56 -0
- data/test/spec/date/strftime_spec.rb +205 -0
- data/test/spec/date/strptime_spec.rb +143 -0
- data/test/spec/date/upto_spec.rb +18 -0
- data/test/spec/parsedate/parsedate.rb +95 -0
- data/test/spec/time/httpdate_spec.rb +21 -0
- data/test/spec/time/iso8601_spec.rb +7 -0
- data/test/spec/time/rfc2822_spec.rb +7 -0
- data/test/spec/time/rfc822_spec.rb +7 -0
- data/test/spec/time/shared/rfc2822.rb +65 -0
- data/test/spec/time/shared/xmlschema.rb +53 -0
- data/test/spec/time/xmlschema_spec.rb +7 -0
- data/test/spec_helper.rb +56 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/calculations_test.rb +82 -0
- data/test/unit/collection_test.rb +42 -0
- data/test/unit/magic_test.rb +37 -0
- data/test/unit/math_test.rb +30 -0
- data/test/unit/rulesets_test.rb +94 -0
- data/test/unit/samples/test_ruleset.yml +6 -0
- data/test/unit/util_test.rb +29 -0
- metadata +196 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 (Derek Perez | www.derekperez.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
NOTE: Please read the wiki for usage examples and to see how you can help our project!
|
2
|
+
|
3
|
+
SmartMonth is a "bodysnatcher" plugin that takes boring Date month fixnums and replaces them with a rich toolkit of functionality. You can use SmartMonth to:
|
4
|
+
|
5
|
+
- Determine the first tuesday of any month in any year.
|
6
|
+
- Determine all of the fridays of any month in any year.
|
7
|
+
- Iterate through all the days of a month.
|
8
|
+
- Determine how many days of the month there are.
|
9
|
+
- Determine the first and last days of the month.
|
10
|
+
- And other fun date/month related things!
|
11
|
+
|
12
|
+
This is designed to be an extension of not only Time.now.month, but adds a new Month class to the ruby object model.
|
13
|
+
|
14
|
+
Keep in mind, this is 1.1, and may break things that rely on Time.now.month being a Fixnum. I've tried to fix this by making sure #to_i works as expected, but it may be an issue, handle with care.
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
|
18
|
+
Time.now.month.every_tuesday #=> [array of tuesdays for the current month]
|
19
|
+
Month.april.first_wednesday #=> Date object corresponding to the first wednesday of april.
|
20
|
+
Month.may.every_monday_and_friday #=> {:monday=>[...],:friday=>[...]}
|
21
|
+
Month.june(2012).last_monday #=> Date object corresponding to the last monday in june 2012.
|
22
|
+
|
23
|
+
# iterator:
|
24
|
+
Month.june(2004).each do |day|
|
25
|
+
day.to_day #=> name of day (Saturday, Sunday, etc.)
|
26
|
+
day.to_i #=> value between 1 and the last day of the month corresponding.
|
27
|
+
end
|
28
|
+
|
29
|
+
Check the included documentation (rdoc) or the documentation site http://www.derekperez.com/projects/smartmonth
|
30
|
+
for more info on usage!
|
31
|
+
|
32
|
+
Copyright (c) 2009 (Derek Perez | www.derekperez.com), released under the MIT license
|
33
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gemspec|
|
8
|
+
gemspec.name = "smart_month"
|
9
|
+
gemspec.summary = 'SmartMonth is a "bodysnatcher" plugin that takes boring Date month fixnums and replaces them with a rich toolkit of
|
10
|
+
functionality.'
|
11
|
+
gemspec.description = "You can use SmartMonth to: \n" +
|
12
|
+
"- Determine the first tuesday of any month in any year. \n" +
|
13
|
+
"- Determine all of the fridays of any month in any year. \n" +
|
14
|
+
"- Iterate through all the days of a month. \n" +
|
15
|
+
"- Determine how many days of the month there are. \n" +
|
16
|
+
"- Determine the first and last days of the month. \n" +
|
17
|
+
"- And other fun date/month related things!"
|
18
|
+
gemspec.email = "derek@derekperez.com"
|
19
|
+
gemspec.homepage = "http://github.com/perezd/smart_month"
|
20
|
+
gemspec.authors = ["Derek Perez", "Jonathan Silverman"]
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
desc 'Default: run unit tests.'
|
29
|
+
task :default => :test
|
30
|
+
|
31
|
+
desc 'Runs unit test suite.'
|
32
|
+
Rake::TestTask.new do |test|
|
33
|
+
test.libs << './test'
|
34
|
+
test.test_files = FileList['test/unit/*test.rb']
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Runs Ruby Standard Library tests for date and time.'
|
39
|
+
task :test_ruby do
|
40
|
+
puts `mspec test/spec`
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Generate documentation for the smart_month plugin.'
|
44
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = 'SmartMonth'
|
47
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
48
|
+
rdoc.rdoc_files.include('README')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
51
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/month.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__) + '/smart_month'
|
2
|
+
|
3
|
+
require 'calculations'
|
4
|
+
require 'collection'
|
5
|
+
require 'math'
|
6
|
+
require 'magic'
|
7
|
+
require 'util'
|
8
|
+
require 'rulesets'
|
9
|
+
|
10
|
+
class Month
|
11
|
+
# gather the modules
|
12
|
+
include SmartMonth::Calculations
|
13
|
+
include SmartMonth::Collection
|
14
|
+
include SmartMonth::Math
|
15
|
+
include SmartMonth::Util
|
16
|
+
include SmartMonth::Magic
|
17
|
+
# abstraction that returns an array of months.
|
18
|
+
NAMES = Date::MONTHNAMES
|
19
|
+
# abstraction that returns an array of day names.
|
20
|
+
DAYS = Date::DAYNAMES
|
21
|
+
# constructor, takes 2 optional arguments, if you don't provide them, it will default to
|
22
|
+
# the current month and year.
|
23
|
+
def initialize(month = Time.now.mon, year = Time.now.year)
|
24
|
+
@date = Date.new(year,( month.is_a?(Integer) ? month : Month::NAMES.index(month.to_s.capitalize) ) ,1)
|
25
|
+
end
|
26
|
+
end
|
data/lib/smart_month.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'date'
|
2
|
+
require File.expand_path(File.dirname(__FILE__)) + '/month'
|
3
|
+
require File.expand_path(File.dirname(__FILE__)) + '/smart_month/extensions/date'
|
4
|
+
|
5
|
+
# inject smart_month system into the Time class.
|
6
|
+
class ::Time
|
7
|
+
def month
|
8
|
+
return Month.new(self.mon,self.year)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module SmartMonth
|
2
|
+
# Responsible for all core month/week calculations.
|
3
|
+
module Calculations
|
4
|
+
# returns the first date of the day requested.
|
5
|
+
# if no day is defined, it returns the very first day of the month.
|
6
|
+
def first(day = nil)
|
7
|
+
(day.nil?) ? @date : nth_weekday(day,1)
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns the second date of the day requested.
|
11
|
+
def second(day)
|
12
|
+
nth_weekday(day,2)
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns the third date of the day requested.
|
16
|
+
def third(day)
|
17
|
+
nth_weekday(day,3)
|
18
|
+
end
|
19
|
+
|
20
|
+
# returns the fourth (or potentially last), date of the day requested.
|
21
|
+
def fourth(day)
|
22
|
+
nth_weekday(day,4) || self.last(day)
|
23
|
+
end
|
24
|
+
|
25
|
+
# returns last date of the day requested.
|
26
|
+
# if no day is defined, it returns the very last day of the month.
|
27
|
+
def last(day = nil)
|
28
|
+
(day.nil?) ? Date.new(@date.year,@date.month,-1) : every(day).last
|
29
|
+
end
|
30
|
+
|
31
|
+
# returns an array of dates of the day requested.
|
32
|
+
# if an array of dates are passed in, a hash of day names containing
|
33
|
+
# the corresponding date are returned.
|
34
|
+
def every!(*days)
|
35
|
+
days = [days].flatten
|
36
|
+
if days.size == 1
|
37
|
+
dates = []
|
38
|
+
(1..5).each do |week|
|
39
|
+
dates << nth_weekday(days.first,week)
|
40
|
+
end
|
41
|
+
dates.compact!
|
42
|
+
else
|
43
|
+
dates = {}
|
44
|
+
days.each do |day|
|
45
|
+
day = day.to_sym
|
46
|
+
dates[day] = []
|
47
|
+
(1..5).each do |week|
|
48
|
+
dates[day] << nth_weekday(day,week)
|
49
|
+
end
|
50
|
+
dates[day].compact!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
dates
|
54
|
+
end
|
55
|
+
|
56
|
+
# this returns the same data as #every! but removes the hash
|
57
|
+
# organization of the data.
|
58
|
+
def every(*days)
|
59
|
+
days = every!(days)
|
60
|
+
(days.is_a?(Array)) ? days : days.values.flatten
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns the total number of days in month.
|
64
|
+
def size
|
65
|
+
self.last.day.to_i
|
66
|
+
end
|
67
|
+
alias_method :length, :size
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# this is the heart of the calculations module.
|
72
|
+
def nth_weekday(day,limit = 0)
|
73
|
+
raise RuntimeError, "#{day} is not a valid day of the week" unless Month::DAYS.collect{|d| d.downcase}.include?(day.to_s)
|
74
|
+
limit -= 1 # make more sense to humans
|
75
|
+
requested_weekday = Month::DAYS.collect{|d| d.downcase}.index(day.to_s)
|
76
|
+
iter_date = Date.new(@date.year,@date.month,1)
|
77
|
+
# find the first day that works
|
78
|
+
until iter_date.wday == requested_weekday
|
79
|
+
iter_date +=1
|
80
|
+
end
|
81
|
+
# then add a multipler based on the limit
|
82
|
+
iter_date = iter_date + (limit*7)
|
83
|
+
# if the date we've iterated to doesn't exist in this month, send out a nil.
|
84
|
+
return iter_date unless iter_date.month != @date.month
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module SmartMonth
|
2
|
+
# Responsible for all collection-based functionality.
|
3
|
+
module Collection
|
4
|
+
|
5
|
+
# allows for increment by copy
|
6
|
+
def next
|
7
|
+
if @date.month + 1 > 12
|
8
|
+
Month.new(1,@date.year+1)
|
9
|
+
else
|
10
|
+
Month.new(@date.month+1,@date.year)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# allows for in place incrementing by 1
|
15
|
+
def next!
|
16
|
+
if @date.month + 1 > 12
|
17
|
+
@date = Date.new(@date.year+1,@date.month,1)
|
18
|
+
else
|
19
|
+
@date = Date.new(@date.year,@date.month+1,1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# allows for deincrement by copy
|
24
|
+
def previous
|
25
|
+
if @date.month - 1 == 0
|
26
|
+
Month.new(12,@date.year-1)
|
27
|
+
else
|
28
|
+
Month.new(@date.month-1,@date.year)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
alias_method :prev, :previous
|
32
|
+
|
33
|
+
# allows for in place deincrementing by 1
|
34
|
+
def previous!
|
35
|
+
if @date.month - 1 == 0
|
36
|
+
@date = Date.new(@date.year-1,12,1)
|
37
|
+
else
|
38
|
+
@date = Date.new(@date.year,@date.month-1,1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
alias_method :prev!, :previous!
|
42
|
+
|
43
|
+
# allows us to iterate internally on the days of a given month.
|
44
|
+
def each(first = 1, last = self.last.day, &block)
|
45
|
+
(1..self.last.day).each do |e|
|
46
|
+
block.call(Date.new(@date.year,@date.month,e))
|
47
|
+
end
|
48
|
+
return self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# mixin some new methods to the Date class.
|
2
|
+
class Date
|
3
|
+
|
4
|
+
# this is to make date objects more console-friendly
|
5
|
+
def inspect
|
6
|
+
strftime("%a, %d %b %Y")
|
7
|
+
end
|
8
|
+
|
9
|
+
# allows a date to retrun the current day its set to as an integer
|
10
|
+
def to_i
|
11
|
+
self.day.to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
# allows a date to return the day of the week it currently is as a string
|
15
|
+
def to_day
|
16
|
+
Month::DAYS[self.wday]
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module SmartMonth
|
2
|
+
# Magic is responsible for the rails-like dynamic "magic finders".
|
3
|
+
module Magic
|
4
|
+
|
5
|
+
# extends the Month class with the Month factory
|
6
|
+
def self.included(base)
|
7
|
+
base.extend MonthFactory
|
8
|
+
end
|
9
|
+
|
10
|
+
# This module allows your code to ignore the class instantiation, simply do Month.june!
|
11
|
+
module MonthFactory
|
12
|
+
# singleton month factory
|
13
|
+
def method_missing(meth,*args)
|
14
|
+
return new(meth.to_s.capitalize, (args.first || Time.now.year)) if Month::NAMES.include? meth.to_s.capitalize
|
15
|
+
end
|
16
|
+
|
17
|
+
# this allows you to essentially treat the month as a hash or array object to construct new months.
|
18
|
+
def [](month)
|
19
|
+
new(month)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# nice catchall to allow rails-esque APIs if you choose to.
|
24
|
+
def method_missing(meth)
|
25
|
+
raw = meth.to_s.split("_")
|
26
|
+
|
27
|
+
# execute lookup using week as positioning control
|
28
|
+
if week_position?(raw[0]) && raw[1] == "and"
|
29
|
+
results = []
|
30
|
+
# parse the request
|
31
|
+
arg = raw.slice!(raw.index(raw.last))
|
32
|
+
funcs = parse_arguments(raw)
|
33
|
+
# execute
|
34
|
+
funcs.each do |func|
|
35
|
+
results << self.send(func,arg) if week_position?(func)
|
36
|
+
end
|
37
|
+
return results
|
38
|
+
|
39
|
+
# execute an every pattern on n-days
|
40
|
+
elsif raw[0] == "every"
|
41
|
+
# parse the request
|
42
|
+
func = raw.slice!(0)
|
43
|
+
args = parse_arguments(raw)
|
44
|
+
# execute
|
45
|
+
return self.every(args)
|
46
|
+
|
47
|
+
# revert to basic singular lookup or raise an exception
|
48
|
+
elsif self.respond_to?(raw[0])
|
49
|
+
func = raw.slice!(0)
|
50
|
+
return self.send(func,raw) if week_position?(func)
|
51
|
+
else
|
52
|
+
raise NoMethodError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# a quicky helper for breaking up text to args.
|
59
|
+
def parse_arguments(args)
|
60
|
+
args.select { |a| a != 'and' }
|
61
|
+
end
|
62
|
+
|
63
|
+
# this is used a lot for validity and sanitizing.
|
64
|
+
def week_position?(compare)
|
65
|
+
[compare].flatten.each do |comp|
|
66
|
+
%w(first second third fourth last).include?(comp)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SmartMonth
|
2
|
+
# Responsible for all math related functionality.
|
3
|
+
module Math
|
4
|
+
|
5
|
+
# adds a number to the month value and returns a Fixnum.
|
6
|
+
def +(int)
|
7
|
+
self.to_i + int
|
8
|
+
end
|
9
|
+
|
10
|
+
# subtracts a number to the month value and returns a Fixnum.
|
11
|
+
def -(int)
|
12
|
+
self.to_i - int
|
13
|
+
end
|
14
|
+
|
15
|
+
# multiplies a number to the month value and returns a Fixnum.
|
16
|
+
def *(int)
|
17
|
+
self.to_i * int
|
18
|
+
end
|
19
|
+
|
20
|
+
# divides a number to the month value and returns a Fixnum.
|
21
|
+
def /(int)
|
22
|
+
self.to_i / int
|
23
|
+
end
|
24
|
+
|
25
|
+
# exponents a number to the month value and returns a Fixnum.
|
26
|
+
def **(int)
|
27
|
+
self.to_i ** int
|
28
|
+
end
|
29
|
+
|
30
|
+
# modulos a number to the month value and returns a Fixnum.
|
31
|
+
def %(int)
|
32
|
+
self.to_i % int
|
33
|
+
end
|
34
|
+
|
35
|
+
# compares the current month to another month.
|
36
|
+
def ==(month)
|
37
|
+
self.inspect! == month.inspect!
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module SmartMonth
|
4
|
+
class Rulesets
|
5
|
+
|
6
|
+
## LOADING RULES FROM YAML STREAM OR YAML FILE
|
7
|
+
|
8
|
+
# this allows importing of rules into the system as
|
9
|
+
# a raw yaml stream
|
10
|
+
def self.load_yaml(yaml)
|
11
|
+
@@rulesets = YAML::load(yaml)
|
12
|
+
self.new.send(:activate_rules!)
|
13
|
+
end
|
14
|
+
|
15
|
+
# this allows import of rules into the system as
|
16
|
+
# a yaml file on disk
|
17
|
+
def self.load_file(path)
|
18
|
+
File.open(path) { |yaml| self.load_yaml(yaml) }
|
19
|
+
end
|
20
|
+
|
21
|
+
## MANUAL RULE MANAGEMENT
|
22
|
+
|
23
|
+
# manually add a rule to the system. The rule argument should be a string,
|
24
|
+
# an optional year argument can be passed if using a "natural frequency"
|
25
|
+
def self.add_rule(name, rule, year = nil)
|
26
|
+
@@rulesets = {} unless defined? @@rulesets
|
27
|
+
if rule.is_a?(String)
|
28
|
+
# define rule
|
29
|
+
rule_hash = { name => {'when' => rule } }
|
30
|
+
rule_hash[name]['year'] = year unless year.nil?
|
31
|
+
# merge and inject
|
32
|
+
@@rulesets.merge!(rule_hash)
|
33
|
+
self.new.send(:inject_rule,name,@@rulesets[name])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# updating and adding rules should do the same thing
|
38
|
+
class << self
|
39
|
+
alias_method :update_rule, :add_rule
|
40
|
+
end
|
41
|
+
|
42
|
+
# manually remove an existing rule from the system.
|
43
|
+
def self.remove_rule(name)
|
44
|
+
begin
|
45
|
+
name = name.gsub(' ','_').downcase
|
46
|
+
@@rulesets.delete_if { |k,v| k.downcase == name }
|
47
|
+
Date.class_eval { eval("undef is_#{name}?") }
|
48
|
+
Month.class_eval { eval("undef #{name}")}
|
49
|
+
rescue
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# iterates through all the rules currently defined in the class and injects them
|
58
|
+
# if they are not currently implemented.
|
59
|
+
def activate_rules!
|
60
|
+
@@rulesets.each_key do |rule|
|
61
|
+
inject_rule(rule, @@rulesets[rule])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# this method parses and defines the methods in our pre-existing ruby classes
|
66
|
+
def inject_rule(name,rule)
|
67
|
+
meth_name = name.gsub(' ','_').downcase
|
68
|
+
type, data = parse(rule)
|
69
|
+
# don't define the rulset methods if they might override existing methods!
|
70
|
+
define_methods(type,meth_name,data) unless Month.respond_to?(meth_name) || Date.respond_to?(meth_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
## PARSER STRATEGIES
|
74
|
+
|
75
|
+
# factory method for determing which parsing strategy to use based on the rule.
|
76
|
+
def parse(rule)
|
77
|
+
(Month::NAMES.include?(rule['when'].split[0].capitalize)) ? parse_string_as_date(rule) : parse_string_as_frequency(rule)
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# this is the parsing process for basic "date as string" rules
|
82
|
+
# (eg: 'December 12th, 2005' or 'March 15th')
|
83
|
+
def parse_string_as_date(rule)
|
84
|
+
# first attempt to parse the string and load structure
|
85
|
+
month,day,year = rule['when'].split
|
86
|
+
year = Time.now.year.to_i if year.nil?
|
87
|
+
data = {:year => year.to_i, :month => Month.new(month).to_i, :day => day.to_i}
|
88
|
+
return [:date,data]
|
89
|
+
end
|
90
|
+
|
91
|
+
# this is the parsing process for basic "date as frequency" rules
|
92
|
+
# (eg: 'every tuesday and saturday' or 'every monday in march and april')
|
93
|
+
def parse_string_as_frequency(rule)
|
94
|
+
data,context = rule['when'].split('in')
|
95
|
+
data = data.rstrip.gsub(' ','_').downcase unless data.nil?
|
96
|
+
unless context.nil?
|
97
|
+
context = context.gsub(/(\,|and)/,'').split
|
98
|
+
context.collect {|ctx| ctx.downcase }
|
99
|
+
end
|
100
|
+
return [:freq, data, context, rule['year'].to_i]
|
101
|
+
end
|
102
|
+
|
103
|
+
## METHOD DEFINITION
|
104
|
+
|
105
|
+
## The folowing methods are used to alter the existing objects in memory
|
106
|
+
# through the use of meta-programming.
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# do not define the alias unless the root is defined properly
|
111
|
+
def define_methods(type,meth_name,data)
|
112
|
+
define_alias(meth_name,data) if define_root(type,meth_name,data)
|
113
|
+
end
|
114
|
+
|
115
|
+
# define root definition for lookup directly into the Month class
|
116
|
+
def define_root(type,meth_name,data)
|
117
|
+
return false unless [:date,:freq].include?(type)
|
118
|
+
begin
|
119
|
+
# algorithm template
|
120
|
+
context = "([#{data[:month]}].flatten.include?(self.month))"
|
121
|
+
fail = 'nil'
|
122
|
+
# determine lookup based on requested strategy
|
123
|
+
case type
|
124
|
+
when :date: qry = "Date.new(#{data[:year]},#{data[:month]},#{data[:day]})"
|
125
|
+
when :freq: qry = 'true'
|
126
|
+
else qry = 'nil'
|
127
|
+
end
|
128
|
+
# evaluate the custom rule into the function
|
129
|
+
Month.send(:define_method,meth_name) {
|
130
|
+
eval "#{context} ? #{qry} : #{fail}"
|
131
|
+
}
|
132
|
+
rescue
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
return true
|
136
|
+
end
|
137
|
+
|
138
|
+
# alias root definition as boolean directly into the Date class
|
139
|
+
def define_alias(meth_name,data)
|
140
|
+
begin
|
141
|
+
Date.send(:define_method, "is_#{meth_name}?") {
|
142
|
+
[Month[data[:month]].send(meth_name)].flatten.include?(self)
|
143
|
+
}
|
144
|
+
rescue
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
return true
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|