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 +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
|