temporals 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/History.txt +13 -0
- data/Manifest.txt +10 -0
- data/README.txt +98 -0
- data/Rakefile +9 -0
- data/lib/temporals.rb +116 -0
- data/lib/temporals/parser.rb +140 -0
- data/lib/temporals/patterns.rb +167 -0
- data/lib/temporals/ruby_ext.rb +59 -0
- data/lib/temporals/types.rb +125 -0
- data/spec/temporals_spec.rb +103 -0
- metadata +98 -0
- metadata.gz.sig +0 -0
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
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
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
|