temporals 2.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.tar.gz.sig ADDED
Binary file
data/History.txt ADDED
@@ -0,0 +1,13 @@
1
+ === 2.0.0 / 2009-11-21
2
+
3
+ * Moved gem to gemcutter.org
4
+ * Added several examples and specs
5
+ * Made them all work properly
6
+ * The parsing engine is a bit smarter and more flexible
7
+
8
+ === 1.0.0 / 2009-08-21
9
+
10
+ * 1 major enhancement
11
+
12
+ * Initial release
13
+
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.txt
2
+ lib/temporals.rb
3
+ lib/temporals/parser.rb
4
+ lib/temporals/patterns.rb
5
+ lib/temporals/ruby_ext.rb
6
+ lib/temporals/types.rb
7
+ Manifest.txt
8
+ Rakefile
9
+ README.txt
10
+ spec/temporals_spec.rb
data/README.txt ADDED
@@ -0,0 +1,98 @@
1
+ = temporals
2
+
3
+ * Homepage: http://dcparker.github.com/temporals
4
+ * Code: http://github.com/dcparker/temporals
5
+
6
+ == DESCRIPTION:
7
+
8
+ "We could develop some interpreter that would be able to parse and process a range of expressions that we might want to deal with. This would be quite flexible, but also pretty hard" (Martin Fowler, http://martinfowler.com/apsupp/recurring.pdf). Temporals is a Ruby parser for just that.
9
+
10
+ == FEATURES:
11
+
12
+ Temporals can parse the following expressions:
13
+
14
+ * "2-4pm every thursday"
15
+ * "2:30-3p every mon and wed and 3-3:30 on friday"
16
+ * "Thursdays in 2009 and 9 January 2009"
17
+ * "Third Wednesday and Thursday Aug at 3pm"
18
+ * "2pm Fridays in January 2009 and Thursdays in 2009"
19
+ * "1st Thursdays at 4-5pm and First - Fourth of March at 2-3:30pm"
20
+ * "1st-2nd and last Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm"
21
+ * ...and probably more that you are going to waste your time thinking of.
22
+
23
+ Temporals is NOT GUARANTEED to parse anything you give it. It has a limited, though rather large and flexible, vocabulary.
24
+ If you come across something that doesn't parse properly, please write a spec test for it and send it to me: gems@behindlogic.com.
25
+
26
+ == PROBLEMS:
27
+
28
+ * Should add support for "4th of the month" or "4th month"
29
+
30
+ == SYNOPSIS:
31
+
32
+ Temporals works to follow the true spirit of the TimePoint pattern -- the idea that every expression of time in fact has a certain level of intended precision. For example, if I say "March 5, 2000", I simply mean any time within that day -- all day. However, if I say "2:05pm March 5, 2000", I mean any second within that specific minute. But if I say "2:00pm Fridays" I really mean every Friday, and that expression is precise to the day-of-week and to the hour and minute, but the second, week, month, or year don't matter.
33
+ The main usage of Temporals, as it currently has been built for, is Temporals.parse and Temporals#include?. Here are several examples:
34
+
35
+ Example Expressions:
36
+ # All day Tuesday, every week
37
+ t1 = Temporals.parse('Tuesday')
38
+
39
+ # Every Feb 24, all day long
40
+ t2 = Temporals.parse("February 24th")
41
+
42
+ # Feb 24 in 2001, all day long
43
+ t3 = Temporals.parse("24 February 2001")
44
+
45
+ # every Thursday in 2009, and the 9th of January 2009 too
46
+ t4 = Temporals.parse("9 January 2009 and Thursday 2009")
47
+
48
+ # default duration of ONE of the most specific piece mentioned: 2-3pm every
49
+ # friday in January of '09, and also all day every thursday all year in 2009
50
+ t5 = Temporals.parse("2pm Fridays in January 2009 and Thursdays in 2009")
51
+
52
+ # first thursday of every month, forever, from 4 to 5 pm;
53
+ # also 2 to 3:30 pm on the 1st, 2nd, 3rd, and 4th of March (every year!)
54
+ t6 = Temporals.parse("1st Thursdays at 4-5pm and 1st - 4th of March at 2pm")
55
+
56
+ # you can figure this one out for yourself... Then figure out how the parsing knows exactly what this means! :P
57
+ t7 = Temporals.parse("1st-2nd and 4th Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm")
58
+
59
+ More methods available (referencing some of the above examples):
60
+ t5.include?(Time.parse('2009-02-05 19:00:00')) => true
61
+ t5.include?(Time.parse('2009-01-09 2:31pm')) => true
62
+ t5.include?(Time.parse('February 5, 2010')) => false
63
+ t5.include?(Time.parse('January 16, 2007')) => false
64
+ t6.occurrances_on_day(Time.parse('2009-04-02')) => [{:start_time=>Thu Apr 02 16:00:00 2009, :end_time=>Thu Apr 02 17:00:00 2009}, {:start_time=>Thu Apr 02 14:00:00 2009, :end_time=>Thu Apr 02 15:30:00 2009}]
65
+ t7.occurs_on_day?(Time.parse('2009-03-05')) => true
66
+
67
+ == REQUIREMENTS:
68
+
69
+ * Just Ruby!
70
+
71
+ == INSTALL
72
+
73
+ * gem install temporals -s http://gemcutter.org
74
+
75
+ ## License ##
76
+
77
+ (The MIT License)
78
+
79
+ Copyright (c) 2009 BehindLogic <gems@behindlogic.com>
80
+
81
+ Permission is hereby granted, free of charge, to any person obtaining
82
+ a copy of this software and associated documentation files (the
83
+ 'Software'), to deal in the Software without restriction, including
84
+ without limitation the rights to use, copy, modify, merge, publish,
85
+ distribute, sublicense, and/or sell copies of the Software, and to
86
+ permit persons to whom the Software is furnished to do so, subject to
87
+ the following conditions:
88
+
89
+ The above copyright notice and this permission notice shall be
90
+ included in all copies or substantial portions of the Software.
91
+
92
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
93
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
94
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
95
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
96
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
97
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
98
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'temporals' do
7
+ developer 'Daniel Parker', 'gems@behindlogic.com'
8
+ self.url = "http://dcparker.github.com/temporals"
9
+ end
data/lib/temporals.rb ADDED
@@ -0,0 +1,116 @@
1
+
2
+ require 'date'
3
+ require 'time'
4
+ require 'temporals/ruby_ext'
5
+ require 'temporals/types'
6
+ require 'temporals/patterns'
7
+ require 'temporals/parser'
8
+
9
+ class Temporal
10
+ VERSION = '2.0.0'
11
+
12
+ def initialize(options)
13
+ options.each do |key,value|
14
+ instance_variable_set(:"@#{key}", value)
15
+ end
16
+ end
17
+
18
+ def [](key)
19
+ instance_variable_get(:"@#{key}")
20
+ end
21
+
22
+ def start_pm?
23
+ if @start_time =~ /([ap])m$/ || @end_time =~ /([ap])m$/
24
+ $1 == 'p'
25
+ else
26
+ nil
27
+ end
28
+ end
29
+
30
+ def include?(datetime)
31
+ return false unless occurs_on_day?(datetime)
32
+ if @type =~ /timerange/
33
+ test_date = datetime.strftime("%Y-%m-%d")
34
+ test_start_time = Time.parse("#{test_date} #{@start_time.gsub(/([ap]m)$/,'')}#{start_pm? ? 'pm' : 'am'}")
35
+ test_end_time = Time.parse("#{test_date} #{@end_time}")
36
+ test_end_time = test_end_time+59 if test_end_time == test_start_time # If they're equal, they are assumed to be to the minute precision
37
+ puts "TimeRange: date:#{test_date} test_start:#{test_start_time} test_end:#{test_end_time} <=> #{datetime}" if $DEBUG
38
+ return false unless datetime.between?(test_start_time, test_end_time)
39
+ end
40
+ return true
41
+ puts "#{datetime} Included!" if $DEBUG
42
+ end
43
+
44
+ def occurs_on_day?(datetime)
45
+ puts "#{datetime} IN? #{inspect}" if $DEBUG
46
+ if @type =~ /month/
47
+ puts "Month #{Month.order[datetime.month-1].inspect} == #{@month.inspect} >> #{Month.order[datetime.month-1].value_in?(@month)}" if $DEBUG
48
+ return false unless Month.order[datetime.month-1].value_in?(@month)
49
+ end
50
+ if @type =~ /ord_wday/
51
+ puts "Weekday: #{WDay.order[datetime.wday].inspect} in? #{@wday.inspect} == #{WDay.order[datetime.wday].value_in?(@wday)}" if $DEBUG
52
+ return false unless WDay.order[datetime.wday].value_in?(@wday)
53
+ puts "WeekdayOrd: #{datetime.wday_ord} in? #{@ord.inspect} == #{datetime.wday_ord.value_in?(@ord)}" if $DEBUG
54
+ puts "WeekdayLast: #{datetime.wday_last} in? #{@ord.inspect} == #{datetime.wday_last.value_in?(@ord)}" if $DEBUG
55
+ return false unless datetime.wday_ord.value_in?(@ord) || datetime.wday_last.value_in?(@ord)
56
+ end
57
+ if @type =~ /month_ord/
58
+ puts "Day #{datetime.day} == #{@ord.inspect} >> #{datetime.day.value_in?(@ord)}" if $DEBUG
59
+ return false unless datetime.day.value_in?(@ord)
60
+ end
61
+ if @type =~ /year/
62
+ puts "Year #{datetime.year} == #{@year.inspect} >> #{datetime.year.value_in?(@year)}" if $DEBUG
63
+ return false unless datetime.year.value_in?(@year)
64
+ end
65
+ if @type =~ /wday/
66
+ puts "Weekday: #{WDay.order[datetime.wday].inspect} in? #{@wday.inspect} == #{WDay.order[datetime.wday].value_in?(@wday)}" if $DEBUG
67
+ return false unless WDay.order[datetime.wday].value_in?(@wday)
68
+ end
69
+ puts "Occurs on #{datetime}!" if $DEBUG
70
+ return true
71
+ end
72
+
73
+ def occurrances_on_day(date)
74
+ occurs_on_day?(date) ? [{:start_time => start_time(date), :end_time => end_time(date)}] : []
75
+ end
76
+
77
+ def start_time(date=nil)
78
+ if date
79
+ @start_time.sub!(/^(\d+)/,'\1:00') if @start_time =~ /^(\d+)[^:]/
80
+ puts "#{date.strftime("%Y-%m-%d")} #{@start_time}" if $DEBUG
81
+ Time.parse("#{date.strftime("%Y-%m-%d")} #{@start_time}")
82
+ else
83
+ @start_time
84
+ end
85
+ end
86
+ def end_time(date=nil)
87
+ if date
88
+ @end_time.sub!(/^(\d+)/,'\1:00') if @end_time =~ /^(\d+)[^:]/
89
+ puts "#{date.strftime("%Y-%m-%d")} #{@end_time}" if $DEBUG
90
+ Time.parse("#{date.strftime("%Y-%m-%d")} #{@end_time}")
91
+ else
92
+ @end_time
93
+ end
94
+ end
95
+
96
+ def to_natural
97
+ @type.split(/_/).collect {|w|
98
+ case w
99
+ # when 'ord'
100
+ # if @ord.respond_to?(:to_natural)
101
+ # @ord.to_natural('ord')
102
+ # end
103
+ # if @ord.is_a?(Range)
104
+ # @ord.begin + 'th-' + @ord.end
105
+ # elsif @ord.is_a?(Array)
106
+ #
107
+ # else
108
+ # @ord + 'th'
109
+ # end
110
+ when 'dummy'
111
+ else
112
+ instance_variable_get('@'+w)
113
+ end
114
+ }.join(' ')
115
+ end
116
+ end
@@ -0,0 +1,140 @@
1
+ class Temporal
2
+ class << self
3
+ def parse(expression)
4
+ puts "Parsing expression: #{expression.inspect}" if $DEBUG
5
+ # 1. Normalize the expression
6
+ # TODO: re-create normalize: ' -&| ', 'time-time'
7
+ expression.gsub!(/\s+/,' ').gsub!(/([\-\&\|])/,' \1 ')
8
+ expression.gsub!(/(#{TimeRegexp}?) +- +(#{TimeRegexp})/,'\1-\2')
9
+ expression.gsub!(/in ([09]\d|\d{4})/) {|s|
10
+ y = $1
11
+ y.length == 2 ? (y =~ /^0/ ? '20'+y : '19'+y) : y
12
+ }
13
+ expression.gsub!(/(^| )(#{TimeRegexp})( |$)/i) {|s|
14
+ b = $1
15
+ time = $2
16
+ a = $3
17
+ if s =~ /[:m]/ # If it really looks like a lone piece of time, it'll have either a am/pm or a ':' in it.
18
+ # Converting a floating time into a timerange that spans the appropriate duration
19
+ puts "Converting Time to TimeRange: #{time.inspect}" if $DEBUG
20
+ # Figure out what precision we're at
21
+ newtime = time + '-'
22
+ if time =~ /(\d+):(\d+)([ap]m?|$)?/
23
+ end_hr = $1.to_i
24
+ end_mn = $2.to_i + 1
25
+ if end_mn > 59
26
+ end_mn -= 60
27
+ end_hr += 1
28
+ end
29
+ end_hr -= 12 if end_hr > 12
30
+ newtime += "#{end_hr}:#{end_mn}#{$3}" # end-time is 1 minute later
31
+ elsif time =~ /(\d+)([ap]m?|$)?/
32
+ end_hr = $1.to_i + 1
33
+ end_hr -= 12 if end_hr > 12
34
+ newtime += "#{end_hr}#{$2}" # end-time is 1 hour later
35
+ end
36
+ puts "Converted! #{newtime}" if $DEBUG
37
+ b+newtime+a
38
+ else
39
+ s
40
+ end
41
+ }
42
+ puts "Normalized expression: #{expression.inspect}" if $DEBUG
43
+
44
+ # 2. Analyze the expression
45
+ words = expression.split(/\s+/)
46
+ puts words.inspect if $DEBUG
47
+ analyzed_expression = words.inject([]) do |a,word|
48
+ a << case word
49
+ when WordTypes[:ord]
50
+ {:type => 'ord', :ord => $1}
51
+ when WordTypes[:word_ord]
52
+ ord = WordOrds.include?(word.downcase) ? WordOrds.index(word.downcase)+1 : 'last'
53
+ puts "WordOrd: #{ord}" if $DEBUG
54
+ {:type => 'ord', :ord => ord}
55
+ when WordTypes[:wday]
56
+ {:type => 'wday', :wday => WDay.normalize($1)}
57
+ when WordTypes[:year]
58
+ {:type => 'year', :year => word}
59
+ when WordTypes[:month]
60
+ {:type => 'month', :month => Month.normalize(word)}
61
+ when WordTypes[:union]
62
+ {:type => 'union'}
63
+ when WordTypes[:range]
64
+ {:type => 'range'}
65
+ when WordTypes[:timerange]
66
+ # determine and inject am/pm
67
+ start_at = $1
68
+ end_at = $2
69
+ start_at_p = $1 if start_at =~ /([ap])m?$/
70
+ end_at_p = $1 if end_at =~ /([ap])m?$/
71
+ start_hr = start_at.split(/:/)[0].to_i
72
+ start_hr = '0' if start_hr == '12' # this is used only for > & < comparisons, so converting it to 0 makes everything easier.
73
+ end_hr = end_at.split(/:/)[0].to_i
74
+ if start_at_p && !end_at_p
75
+ # If end-time is a lower hour number than start-time, then we've crossed noon or midnight, and the end-time a/pm should be opposite.
76
+ end_at = end_at + (start_hr <= end_hr ? start_at_p : (start_at_p=='a' ? 'p' : 'a'))
77
+ elsif end_at_p && !start_at_p
78
+ # If end-time is a lower hour number than start-time, then we've crossed noon or midnight, and the start-time a/pm should be opposite.
79
+ start_at = start_at + (start_hr <= end_hr ? end_at_p : (end_at_p=='a' ? 'p' : 'a'))
80
+ elsif !end_at_p && !start_at_p
81
+ # If neither had am/pm attached, assume am if after 7, pm if 12 or before 7.
82
+ start_at_p = (start_hr < 8 ? 'p' : 'a')
83
+ start_at = start_at + start_at_p
84
+ # If end-time is a lower hour number than start-time, then we've crossed noon or midnight, and the end-time a/pm should be opposite.
85
+ end_at = end_at + (start_hr <= end_hr ? start_at_p : (start_at_p=='a' ? 'p' : 'a'))
86
+ end
87
+ start_at += 'm' unless start_at =~ /m$/
88
+ end_at += 'm' unless end_at =~ /m$/
89
+ {:type => 'timerange', :start_time => start_at, :end_time => end_at}
90
+ end
91
+ end.compact
92
+ def analyzed_expression.collect_types
93
+ collect {|e|
94
+ puts "E: #{e.inspect}" if $DEBUG
95
+ e[:type]
96
+ }
97
+ end
98
+
99
+ # 3. Combine common patterns
100
+ puts analyzed_expression.inspect if $DEBUG
101
+ puts analyzed_expression.collect_types.inspect if $DEBUG
102
+
103
+ something_was_modified = true
104
+ while something_was_modified
105
+ something_was_modified = false
106
+ before_length = analyzed_expression.length
107
+ CommonPatterns.each do |pattern|
108
+ while i = analyzed_expression.collect_types.includes_sequence?(pattern.split(/ /))
109
+ CommonPatternActions[pattern].call(analyzed_expression,i)
110
+ end
111
+ end
112
+ after_length = analyzed_expression.length
113
+ something_was_modified = true if before_length != after_length
114
+ end
115
+
116
+ puts analyzed_expression.inspect if $DEBUG
117
+ puts analyzed_expression.collect_types.inspect if $DEBUG
118
+
119
+ # What remains should be simply sections of boolean logic
120
+ # 4. Parse boolean logic
121
+ analyzed_expression.each_index do |i|
122
+ analyzed_expression[i] = Temporal.new(analyzed_expression[i]) unless analyzed_expression[i][:type].in?('union', 'range')
123
+ end
124
+
125
+ BooleanPatterns.each do |pattern|
126
+ while i = analyzed_expression.collect_types.includes_sequence?(pattern.split(/ /))
127
+ BooleanPatternActions[pattern].call(analyzed_expression,i)
128
+ break if analyzed_expression.length == 1
129
+ end
130
+ end
131
+
132
+ # This is how we know if the expression couldn't quite be figured out. It should have been condensed down to a single Temporal or Temporal::Set
133
+ if analyzed_expression.length > 1
134
+ raise RuntimeError, "Could not parse Temporal Expression: check to make sure it is clear and has only one possible meaning."
135
+ end
136
+
137
+ return analyzed_expression[0]
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,167 @@
1
+ class Temporal
2
+ TimeRegexp = '\d{1,2}(?::\d\d)?(?::\d{1,2})?(?:[ap]m?)?'
3
+ WordOrds = %w(first second third fourth fifth)
4
+ WordTypes = {
5
+ :ord => /^([1-3]?\d)(?:st|nd|rd|th)?$/i, # Should be able to distinguish
6
+ :word_ord => /^(first|second|third|fourth|fifth|last)$/i,
7
+ :wday => /^(#{(WDay.order + WDay.translations.keys).join('|')})s?$/i,
8
+ :month => /^#{(Month.order + Month.translations.keys).join('|')}$/i,
9
+ :year => /^([09]\d|\d{4})$/, # A year will be either 2 digits starting with a 9 or a 0, or 4 digits.
10
+ :union => /^(?:and)$/i,
11
+ :range => /^(?:-|to|through)$/i,
12
+ :timerange => /^(#{TimeRegexp}?)-(#{TimeRegexp})$/i,
13
+ # :from => /^from$/i,
14
+ # :to => /^to$/i,
15
+ # :between => /^between$/i
16
+ }
17
+
18
+ # These are in a specific order
19
+ CommonPatterns = [
20
+ 'ord range ord',
21
+ 'ord union ord',
22
+ 'year range year',
23
+ 'year union year',
24
+ 'wday range wday',
25
+ 'wday union wday',
26
+ # 'timerange union timerange', # Not quite figured out yet, I'd need to implement new code into the question methods.
27
+ 'month ord',
28
+ 'ord month',
29
+ 'ord wday',
30
+ 'month_ord timerange',
31
+ 'month union month',
32
+ 'month range month',
33
+ 'ord_wday month',
34
+ 'ord_wday timerange',
35
+ 'ord_wday_month timerange',
36
+ 'timerange wday',
37
+ 'month_ord year',
38
+ 'month year',
39
+ 'wday year',
40
+ "wday_timerange month",
41
+ "wday_timerange year",
42
+ "wday_timerange month_year"
43
+ ]
44
+ CommonPatternActions = {
45
+ 'ord range ord' => lambda {|words,i|
46
+ words[i][:ord] = (words[i][:ord].to_i..words[i+2][:ord].to_i)
47
+ words.slice!(i+1,2)
48
+ },
49
+ 'ord union ord' => lambda {|words,i|
50
+ words[i][:ord] = ArrayOfRanges.new(words[i][:ord], words[i+2][:ord])
51
+ words.slice!(i+1,2)
52
+ },
53
+ 'year range year' => lambda {|words,i|
54
+ words[i][:year] = (words[i][:year].to_i..words[i+2][:year].to_i)
55
+ words.slice!(i+1,2)
56
+ },
57
+ 'year union year' => lambda {|words,i|
58
+ words[i][:year] = ArrayOfRanges.new(words[i][:year], words[i+2][:year])
59
+ words.slice!(i+1,2)
60
+ },
61
+ 'wday range wday' => lambda {|words,i|
62
+ words[i][:wday] = (words[i][:wday].to_i..words[i+2][:wday].to_i)
63
+ words.slice!(i+1,2)
64
+ },
65
+ 'wday union wday' => lambda {|words,i|
66
+ words[i][:wday] = ArrayOfRanges.new(words[i][:wday], words[i+2][:wday])
67
+ words.slice!(i+1,2)
68
+ },
69
+ # 'timerange union timerange' => lambda {|words,i|
70
+ # words[i][:wday] = ArrayOfRanges.new(words[i][:wday], words[i+2][:wday])
71
+ # words.slice!(i+1,2)
72
+ # },
73
+ 'month ord' => lambda {|words,i|
74
+ words[i][:type] = 'month_ord'
75
+ words[i][:ord] = words[i+1][:ord]
76
+ words.slice!(i+1,1)
77
+ },
78
+ 'ord month' => lambda {|words,i|
79
+ words[i][:type] = 'month_ord'
80
+ words[i][:month] = words[i+1][:month]
81
+ words.slice!(i+1,1)
82
+ },
83
+ 'ord wday' => lambda {|words,i|
84
+ words[i][:type] = 'ord_wday'
85
+ words[i][:wday] = words[i+1][:wday]
86
+ words.slice!(i+1,1)
87
+ },
88
+ 'month_ord timerange' => lambda {|words,i|
89
+ words[i][:type] = 'month_ord_timerange'
90
+ words[i][:start_time] = words[i+1][:start_time]
91
+ words[i][:end_time] = words[i+1][:end_time]
92
+ words.slice!(i+1,1)
93
+ },
94
+ 'month union month' => lambda {|words,i|
95
+ words[i][:month] = ArrayOfRanges.new(words[i][:month], words[i+2][:month])
96
+ words.slice!(i+1,2)
97
+ },
98
+ 'month range month' => lambda {|words,i|
99
+ raise "Not Implemented Yet!"
100
+ },
101
+ 'ord_wday month' => lambda {|words,i|
102
+ words[i][:type] = 'ord_wday_month'
103
+ words[i][:month] = words[i+1][:month]
104
+ words.slice!(i+1,1)
105
+ },
106
+ 'ord_wday timerange' => lambda {|words,i|
107
+ words[i][:type] = 'ord_wday_timerange'
108
+ words[i][:start_time] = words[i+1][:start_time]
109
+ words[i][:end_time] = words[i+1][:end_time]
110
+ words.slice!(i+1,1)
111
+ },
112
+ 'timerange wday' => lambda {|words,i|
113
+ words[i][:type] = 'wday_timerange'
114
+ words[i][:wday] = words[i+1][:wday]
115
+ words.slice!(i+1,1)
116
+ },
117
+ 'ord_wday_month timerange' => lambda {|words,i|
118
+ words[i][:type] = 'ord_wday_month_timerange'
119
+ words[i][:start_time] = words[i+1][:start_time]
120
+ words[i][:end_time] = words[i+1][:end_time]
121
+ words.slice!(i+1,1)
122
+ },
123
+ 'month_ord year' => lambda {|words,i|
124
+ words[i][:type] = 'ord_month_year'
125
+ words[i][:year] = words[i+1][:year]
126
+ words.slice!(i+1,1)
127
+ },
128
+ 'wday year' => lambda {|words,i|
129
+ words[i][:type] = 'wday_year'
130
+ words[i][:year] = words[i+1][:year]
131
+ words.slice!(i+1,1)
132
+ },
133
+ 'month year' => lambda {|words,i|
134
+ words[i][:type] = 'month_year'
135
+ words[i][:year] = words[i+1][:year]
136
+ words.slice!(i+1,1)
137
+ },
138
+ 'wday_timerange month' => lambda {|words,i|
139
+ words[i][:type] = 'wday_timerange_month'
140
+ words[i][:month] = words[i+1][:month]
141
+ words.slice!(i+1,1)
142
+ },
143
+ 'wday_timerange year' => lambda {|words,i|
144
+ words[i][:type] = 'wday_timerange_year'
145
+ words[i][:year] = words[i+1][:year]
146
+ words.slice!(i+1,1)
147
+ },
148
+ 'wday_timerange month_year' => lambda {|words,i|
149
+ words[i][:type] = 'wday_timerange_month_year'
150
+ words[i][:month] = words[i+1][:month]
151
+ words[i][:year] = words[i+1][:year]
152
+ words.slice!(i+1,1)
153
+ }
154
+ }
155
+
156
+ BooleanPatterns = [
157
+ 'union'
158
+ ]
159
+
160
+ BooleanPatternActions = {
161
+ 'union' => lambda {|words,i|
162
+ words[i-1] = Temporal::Union.new(words[i-1], words[i+1])
163
+ words.slice!(i,2)
164
+ puts "Boolean-connected: " + words.inspect if $DEBUG
165
+ }
166
+ }
167
+ end
@@ -0,0 +1,59 @@
1
+ class Array
2
+ def includes_sequence?(sequence)
3
+ first_exists = []
4
+ each_index do |i|
5
+ first_exists << i if self[i] == sequence[0]
6
+ end
7
+ first_exists.any? do |i|
8
+ return i if self[i...(i+sequence.length)] == sequence
9
+ end
10
+ end
11
+
12
+ def include_value?(v)
13
+ any? do |iv|
14
+ case iv
15
+ when Range || Array
16
+ v.to_i.in?(iv)
17
+ else
18
+ if iv.to_s =~ /^\d+$/ && v.to_s =~ /^\d+$/
19
+ iv.to_i == v.to_i
20
+ else
21
+ iv.to_s == v.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class Range
29
+ alias :include_value? :include?
30
+ end
31
+
32
+ class Object
33
+ def value_in?(*arg)
34
+ arg = arg[0] if arg.length == 1 && arg[0].respond_to?(:include_value?)
35
+ arg.include_value?(self)
36
+ end
37
+ def in?(*arg)
38
+ arg = arg[0] if arg.length == 1 && arg[0].respond_to?(:include?)
39
+ arg.include?(self)
40
+ end
41
+ def my_methods
42
+ methods.sort - Object.methods
43
+ end
44
+ end
45
+
46
+ module WdayOrd
47
+ def wday_ord
48
+ (day.to_f / 7).ceil
49
+ end
50
+ def wday_last
51
+ 'last' if day.to_i + 7 > Date.new(year, month, -1).day
52
+ end
53
+ end
54
+ class DateTime
55
+ include WdayOrd
56
+ end
57
+ class Time
58
+ include WdayOrd
59
+ end
@@ -0,0 +1,125 @@
1
+ class Temporal
2
+ class ArrayOfRanges < Array
3
+ def self.new(*values)
4
+ n = allocate
5
+ n.push(*values)
6
+ n
7
+ end
8
+ end
9
+
10
+ class Classification
11
+ class << self
12
+ attr_reader :order, :translations
13
+
14
+ def abbreviations
15
+ @abbreviations ||= translations.inject({}) do |h,(k,v)|
16
+ h[v] = k unless h.has_key?(v) && h[v].length < k.length
17
+ h
18
+ end
19
+ end
20
+
21
+ def normalize(word)
22
+ word = word.capitalize
23
+ order.include?(word) ? word : (translations.has_key?(word) ? translations[word] : nil)
24
+ end
25
+ end
26
+ end
27
+
28
+ class WDay < Classification
29
+ @order = %w(Sunday Monday Tuesday Wednesday Thursday Friday)
30
+ @translations = {
31
+ 'S' => 'Sunday',
32
+ 'M' => 'Monday',
33
+ 'T' => 'Tuesday',
34
+ 'W' => 'Wednesday',
35
+ 'Th' => 'Thursday',
36
+ 'F' => 'Friday',
37
+ 'Sa' => 'Saturday',
38
+ 'Sun' => 'Sunday',
39
+ 'Mo' => 'Monday',
40
+ 'Mon' => 'Monday',
41
+ 'Tu' => 'Tuesday',
42
+ 'Tue' => 'Tuesday',
43
+ 'Tues' => 'Tuesday',
44
+ 'Wed' => 'Wednesday',
45
+ 'Thu' => 'Thursday',
46
+ 'Thur' => 'Thursday',
47
+ 'Thurs' => 'Thursday',
48
+ 'Fri' => 'Friday',
49
+ 'Sat' => 'Saturday'
50
+ }
51
+ end
52
+ class Month < Classification
53
+ @order = %w(January February March April May June July August September October November December)
54
+ @translations = {
55
+ 'Jan' => 'January',
56
+ 'Feb' => 'February',
57
+ 'Mar' => 'March',
58
+ 'Apr' => 'April',
59
+ 'Jun' => 'June',
60
+ 'Jul' => 'July',
61
+ 'Aug' => 'August',
62
+ 'Sep' => 'September',
63
+ 'Oct' => 'October',
64
+ 'Nov' => 'November',
65
+ 'Dec' => 'December'
66
+ }
67
+ end
68
+
69
+ class Set
70
+ def set
71
+ @set ||= []
72
+ end
73
+
74
+ def [](key)
75
+ instance_variable_get(:"@#{key}")
76
+ end
77
+
78
+ def initialize(*args)
79
+ # @set = args.select {|e| e.is_a?(Temporal) || e.is_a?(Range)}
80
+ @set = args.select {|e| e.is_a?(Temporal)}
81
+ end
82
+ end
83
+ class Union < Set
84
+ def initialize(*args)
85
+ @type = 'UNION'
86
+ super
87
+ end
88
+
89
+ def include?(other)
90
+ set.any? {|tp| tp.include?(other)}
91
+ end
92
+
93
+ def occurs_on_day?(other)
94
+ set.any? {|tp| tp.occurs_on_day?(other)}
95
+ end
96
+
97
+ def occurrances_on_day(other)
98
+ set.inject([]) {|a,tp| a.concat(tp.occurrances_on_day(other)); a}
99
+ end
100
+
101
+ def eql?(other)
102
+ if other.is_a?(Temporal::Union)
103
+ set.length == other.length && set.length.times { |i| return false unless set[i].eql? other[i] }
104
+ else
105
+ # what else can we compare to?
106
+ raise "Comparison of Temporal::Union with something different (#{other.class.name})."
107
+ end
108
+ end
109
+
110
+ def to_natural
111
+ set.inject([]) {|a,tp| a << tp.to_natural}.join(' and ')
112
+ end
113
+
114
+ private
115
+ # Sends all other methods to the set array.
116
+ def method_missing(name, *args, &block)
117
+ if set.respond_to?(name)
118
+ args << block if block_given?
119
+ set.send(name, *args)
120
+ else
121
+ super
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,103 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.dirname(__FILE__) + '/../lib/temporals'
4
+
5
+ describe Temporal do
6
+ it "1st-2nd and last Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm" do
7
+ t = Temporal.parse("1st-2nd and last Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm")
8
+ t.include?(Time.parse('2009-03-05 17:54')).should eql(true)
9
+ t.include?(Time.parse('2009-03-05 18:24')).should eql(true)
10
+ t.include?(Time.parse('2009-03-05 18:30')).should eql(true)
11
+ t.include?(Time.parse('2009-03-05 18:31')).should eql(false)
12
+ t.include?(Time.parse('2009-04-26 18:31')).should eql(false)
13
+ t.occurs_on_day?(Time.parse('2009-03-04')).should eql(false)
14
+ t.occurs_on_day?(Time.parse('2009-03-11')).should eql(false)
15
+ t.occurs_on_day?(Time.parse('2009-03-25')).should eql(false)
16
+ t.occurs_on_day?(Time.parse('2009-04-01')).should eql(false)
17
+ t.occurs_on_day?(Time.parse('2009-04-08')).should eql(false)
18
+ t.occurs_on_day?(Time.parse('2009-04-22')).should eql(false)
19
+ t.occurs_on_day?(Time.parse('2009-03-05')).should eql(true)
20
+ t.occurs_on_day?(Time.parse('2009-03-12')).should eql(true)
21
+ t.occurs_on_day?(Time.parse('2009-03-26')).should eql(true)
22
+ t.occurs_on_day?(Time.parse('2009-04-02')).should eql(true)
23
+ t.occurs_on_day?(Time.parse('2009-04-09')).should eql(true)
24
+ t.occurs_on_day?(Time.parse('2009-04-30')).should eql(true)
25
+ t.occurs_on_day?(Time.parse('2009-03-06')).should eql(false)
26
+ t.occurs_on_day?(Time.parse('2009-03-13')).should eql(false)
27
+ t.occurs_on_day?(Time.parse('2009-03-27')).should eql(false)
28
+ t.occurs_on_day?(Time.parse('2009-04-03')).should eql(false)
29
+ t.occurs_on_day?(Time.parse('2009-04-10')).should eql(false)
30
+ t.occurs_on_day?(Time.parse('2009-04-24')).should eql(false)
31
+ t.occurrances_on_day(Time.parse('2009-04-23')).length.should eql(0)
32
+ t.occurrances_on_day(Time.parse('2009-04-30')).length.should eql(1)
33
+ end
34
+
35
+ it "1st Thursdays at 4-5pm and First - Fourth of March at 2-3:30pm" do
36
+ t = Temporal.parse("1st Tuesdays at 4-5pm and First - Fourth of March at 2-3:30pm")
37
+ t.occurs_on_day?(Time.parse('2009-04-07')).should eql(true)
38
+ t.occurrances_on_day(Time.parse('2009-03-01')).length.should eql(1)
39
+ t.occurrances_on_day(Time.parse('2009-03-03')).length.should eql(2)
40
+ t.occurrances_on_day(Time.parse('2009-05-05')).length.should eql(1)
41
+ t.occurrances_on_day(Time.parse('2009-05-12')).length.should eql(0)
42
+ end
43
+
44
+ it "should FAIL on 1st Thursdays at 4-5pm and First - Fourth of March and April at 2-3:30pm" do
45
+ lambda {
46
+ # This could be grouped in two different ways. The first is more likely what is meant, but
47
+ # the second is possible so the patterns are not set up to match the first option for sure.
48
+ # (1st Thursdays at 4-5pm) and ((First - Fourth of March and April) at 2-3:30pm)
49
+ # (1st Thursdays at 4-5pm) and (First - Fourth of March) and (April at 2-3:30pm)
50
+ Temporal.parse("1st Thursdays at 4-5pm and First - Fourth of March and April at 2-3:30pm")
51
+ }.should raise_error(RuntimeError, "Could not parse Temporal Expression: check to make sure it is clear and has only one possible meaning.")
52
+ end
53
+
54
+ it "2pm Tuesdays" do
55
+ t = Temporal.parse("2pm Tuesdays")
56
+ t.occurs_on_day?(Time.parse('2009-04-28')).should eql(true)
57
+ t.occurrances_on_day(Time.parse('2009-04-28')).length.should eql(1)
58
+ t.occurrances_on_day(Time.parse('2009-04-28'))[0][:start_time].should eql(Time.parse('2009-04-28 2:00pm'))
59
+ t.occurrances_on_day(Time.parse('2009-04-28'))[0][:end_time].should eql(Time.parse('2009-04-28 3pm'))
60
+ t.include?(Time.parse('2009-04-21 14:52')).should eql(true)
61
+ t.include?(Time.parse('2009-04-21 14:59:59')).should eql(true)
62
+ end
63
+
64
+ it "2:30pm Tuesdays" do
65
+ t = Temporal.parse("2:30pm Tuesdays")
66
+ t.occurs_on_day?(Time.parse('2009-04-28')).should eql(true)
67
+ t.occurrances_on_day(Time.parse('2009-04-28')).length.should eql(1)
68
+ t.occurrances_on_day(Time.parse('2009-04-28'))[0][:start_time].should eql(Time.parse('2009-04-28 2:30pm'))
69
+ t.occurrances_on_day(Time.parse('2009-04-28'))[0][:end_time].should eql(Time.parse('2009-04-28 2:31pm'))
70
+ t.include?(Time.parse('2009-04-21 14:30')).should eql(true)
71
+ t.include?(Time.parse('2009-04-21 14:30:59')).should eql(true)
72
+ t.include?(Time.parse('2009-04-21 14:31')).should eql(true)
73
+ t.include?(Time.parse('2009-04-21 14:31:01')).should eql(false)
74
+ end
75
+
76
+ it "2:30-3p every mon and wed and 3-3:30 on friday" do
77
+ t = Temporal.parse("2:30-3p every mon and wed and 3-3:30 on friday")
78
+ t.occurs_on_day?(Time.parse('2009-11-20')).should eql(true)
79
+ t.occurs_on_day?(Time.parse('2009-11-19')).should eql(false)
80
+ t.occurs_on_day?(Time.parse('2009-11-18')).should eql(true)
81
+ t.occurrances_on_day(Time.parse('2009-11-20'))[0][:start_time].should eql(Time.parse('2009-11-20 3pm'))
82
+ t.occurrances_on_day(Time.parse('2009-11-18'))[0][:end_time].should eql(Time.parse('2009-11-18 3pm'))
83
+ t.include?(Time.parse('2009-11-20 3:14pm')).should eql(true)
84
+ end
85
+
86
+ it "Thursdays in 2009 and 9 January 2009" do
87
+ t = Temporal.parse("Thursdays in 2009 and 9 January 2009")
88
+ t.occurs_on_day?(Time.parse('January 9, 2009')).should eql(true)
89
+ t.occurs_on_day?(Time.parse('November 19, 2009')).should eql(true)
90
+ t.occurs_on_day?(Time.parse('October 19, 2009')).should eql(false)
91
+ t.include?(Time.parse('2009-01-08 3:14pm')).should eql(true)
92
+ end
93
+
94
+ it "2pm Fridays in January 2009 and Thursdays in 2009" do
95
+ t = Temporal.parse("2pm Fridays in January 2009 and Thursdays in 2009")
96
+ t.occurs_on_day?(Time.parse('November 19, 2009')).should eql(true)
97
+ t.occurs_on_day?(Time.parse('January 9, 2009')).should eql(true)
98
+ t.occurs_on_day?(Time.parse('October 19, 2009')).should eql(false)
99
+ t.include?(Time.parse('2009-01-08 3:14pm')).should eql(true)
100
+ t.include?(Time.parse('2009-01-09 2:14pm')).should eql(true)
101
+ t.include?(Time.parse('2009-01-09 3:14pm')).should eql(false)
102
+ end
103
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: temporals
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Parker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMQ0wCwYDVQQDDARnZW1z
14
+ MRswGQYKCZImiZPyLGQBGRYLYmVoaW5kbG9naWMxEzARBgoJkiaJk/IsZAEZFgNj
15
+ b20wHhcNMDkxMTE5MDQ0NzIzWhcNMTAxMTE5MDQ0NzIzWjBBMQ0wCwYDVQQDDARn
16
+ ZW1zMRswGQYKCZImiZPyLGQBGRYLYmVoaW5kbG9naWMxEzARBgoJkiaJk/IsZAEZ
17
+ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDs+2PvSoBC5mCz
18
+ Nm95RXKOb/u+CmPA70DoG9cF0zipCie003Vm0DYL3Rtobcr32eMvHxkWoJ6xoz0I
19
+ h74yNKtHjTTCxj86HWBPsE6xVMxVCftClndjCyKsiMiqvvp1wDNO0FFK+6LijmL3
20
+ 2Xkp4brWq1JO92y9vYct34R7o2X//+nwZs+sss+EYhNdvdUJfWy7tA5dghGdLvRn
21
+ UhJJSAtTefkBCwO7bufLEt+n7wIRbiJJ5dDCwE3NIX4wUSrNeYwXGXA/Ybki+BUl
22
+ 3KJF9IC0XR9fY9DGF0FXBKrkfDlZRrnQOem2aIxeuln0KQLJXXJuDTQPHO+mK3EG
23
+ UPhR7IAHAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
24
+ BBQn32ZStKmqwFqs2vuglYzkvDzBZjANBgkqhkiG9w0BAQUFAAOCAQEAe3Z+iMmy
25
+ IX9ChQW4hNNb1HOpgCMc2RL+vUwku9WE95dZ+BE4A6mOYTj5JXdYf4R4Z2vavr+d
26
+ nwJWtXPeIBWxireb8gUU0DwqodYTpsmkj5LD1zIaZ59rXlwDA9O0V4fwE1iRG5MD
27
+ mB7m8fT8WNOeg4AfjH4aSiHI1+HX1RQkc7KFdLotKnCevzYU6Jza5VUbXyJ+yCEH
28
+ DFARN3mkfGI+18MRDEi39nK2O/bBd6Wf0cYPEKsGQjNNAIBtv9belepSMd1KKfQ2
29
+ L7j8CnNDCrsHDe7/251D85wSvTH4Q/41NE5ahdCkkHwzDJeyhXpmNuUSswdn7woz
30
+ teST6sOe8lUhZQ==
31
+ -----END CERTIFICATE-----
32
+
33
+ date: 2009-11-21 00:00:00 -05:00
34
+ default_executable:
35
+ dependencies:
36
+ - !ruby/object:Gem::Dependency
37
+ name: hoe
38
+ type: :development
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 2.3.3
45
+ version:
46
+ description: "\"We could develop some interpreter that would be able to parse and process a range of expressions that we might want to deal with. This would be quite flexible, but also pretty hard\" (Martin Fowler, http://martinfowler.com/apsupp/recurring.pdf). Temporals is a Ruby parser for just that."
47
+ email:
48
+ - gems@behindlogic.com
49
+ executables: []
50
+
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - History.txt
55
+ - Manifest.txt
56
+ - README.txt
57
+ files:
58
+ - History.txt
59
+ - lib/temporals.rb
60
+ - lib/temporals/parser.rb
61
+ - lib/temporals/patterns.rb
62
+ - lib/temporals/ruby_ext.rb
63
+ - lib/temporals/types.rb
64
+ - Manifest.txt
65
+ - Rakefile
66
+ - README.txt
67
+ - spec/temporals_spec.rb
68
+ has_rdoc: true
69
+ homepage: http://dcparker.github.com/temporals
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --main
75
+ - README.txt
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ requirements: []
91
+
92
+ rubyforge_project: temporals
93
+ rubygems_version: 1.3.5
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: "\"We could develop some interpreter that would be able to parse and process a range of expressions that we might want to deal with"
97
+ test_files: []
98
+
metadata.gz.sig ADDED
Binary file