temporals 2.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/lib/temporals.rb +11 -9
- data/lib/temporals/parser.rb +155 -120
- data/lib/temporals/patterns.rb +3 -1
- data/lib/temporals/ruby_ext.rb +3 -2
- data/lib/temporals/types.rb +16 -0
- data/spec/temporals_spec.rb +29 -1
- metadata +2 -2
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/lib/temporals.rb
CHANGED
@@ -7,7 +7,7 @@ require 'temporals/patterns'
|
|
7
7
|
require 'temporals/parser'
|
8
8
|
|
9
9
|
class Temporal
|
10
|
-
VERSION = '2.0.
|
10
|
+
VERSION = '2.0.1'
|
11
11
|
|
12
12
|
def initialize(options)
|
13
13
|
options.each do |key,value|
|
@@ -44,14 +44,14 @@ class Temporal
|
|
44
44
|
def occurs_on_day?(datetime)
|
45
45
|
puts "#{datetime} IN? #{inspect}" if $DEBUG
|
46
46
|
if @type =~ /month/
|
47
|
-
puts "Month #{Month.
|
48
|
-
return false unless Month.
|
47
|
+
puts "Month #{Month.new(datetime.month-1).inspect} in? #{@month.inspect} >> #{Month.new(datetime.month-1).value_in?(@month)}" if $DEBUG
|
48
|
+
return false unless Month.new(datetime.month-1).value_in?(@month)
|
49
49
|
end
|
50
50
|
if @type =~ /ord_wday/
|
51
|
-
puts "Weekday: #{WDay.
|
52
|
-
return false unless WDay.
|
53
|
-
puts "WeekdayOrd: #{datetime.wday_ord} in? #{@ord.inspect}
|
54
|
-
puts "WeekdayLast: #{datetime.wday_last} in? #{@ord.inspect}
|
51
|
+
puts "Weekday: #{WDay.new(datetime.wday).inspect} in? #{@wday.inspect} >> #{WDay.new(datetime.wday).value_in?(@wday)}" if $DEBUG
|
52
|
+
return false unless WDay.new(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
55
|
return false unless datetime.wday_ord.value_in?(@ord) || datetime.wday_last.value_in?(@ord)
|
56
56
|
end
|
57
57
|
if @type =~ /month_ord/
|
@@ -63,8 +63,8 @@ class Temporal
|
|
63
63
|
return false unless datetime.year.value_in?(@year)
|
64
64
|
end
|
65
65
|
if @type =~ /wday/
|
66
|
-
puts "Weekday: #{WDay.
|
67
|
-
return false unless WDay.
|
66
|
+
puts "Weekday: #{WDay.new(datetime.wday).inspect} in? #{@wday.inspect} == #{WDay.new(datetime.wday).value_in?(@wday)}" if $DEBUG
|
67
|
+
return false unless WDay.new(datetime.wday).value_in?(@wday)
|
68
68
|
end
|
69
69
|
puts "Occurs on #{datetime}!" if $DEBUG
|
70
70
|
return true
|
@@ -114,3 +114,5 @@ class Temporal
|
|
114
114
|
}.join(' ')
|
115
115
|
end
|
116
116
|
end
|
117
|
+
|
118
|
+
Temporals = Temporal
|
data/lib/temporals/parser.rb
CHANGED
@@ -1,140 +1,175 @@
|
|
1
1
|
class Temporal
|
2
|
-
class
|
3
|
-
def
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
2
|
+
class Parser
|
3
|
+
def initialize(expression)
|
4
|
+
# Make a copy of the passed in string, rather than mutate it
|
5
|
+
@expression = expression.to_s.dup
|
6
|
+
end
|
7
|
+
|
8
|
+
def normalized
|
9
|
+
@normalized || begin
|
10
|
+
normalized = @expression.dup
|
11
|
+
# 1. Normalize the expression
|
12
|
+
# TODO: re-create normalize: ' -&| ', 'time-time'
|
13
|
+
normalized.gsub!(/[\s+,]/,' ')
|
14
|
+
# Pad special characters with spaces for now
|
15
|
+
normalized.gsub!(/([\-\&\|])/,' \1 ')
|
16
|
+
# Get rid of spaces between time ranges
|
17
|
+
normalized.gsub!(/(#{TimeRegexp}?) +(?:-+|to) +(#{TimeRegexp})/,'\1-\2')
|
18
|
+
# Normalize to 4-digit years
|
19
|
+
normalized.gsub!(/in ([09]\d|\d{4})/) {|s|
|
20
|
+
y = $1
|
21
|
+
y.length == 2 ? (y =~ /^0/ ? '20'+y : '19'+y) : y
|
22
|
+
}
|
23
|
+
# Normalize expressions of time
|
24
|
+
normalized.gsub!(/(^| )(#{TimeRegexp})( |$)/i) {|s|
|
25
|
+
b = $1
|
26
|
+
time = $2
|
27
|
+
a = $3
|
28
|
+
if s =~ /[:m]/ # If it really looks like a lone piece of time, it'll have either a am/pm or a ':' in it.
|
29
|
+
# Converting a floating time into a timerange that spans the appropriate duration
|
30
|
+
puts "Converting Time to TimeRange: #{time.inspect}" if $DEBUG
|
31
|
+
# Figure out what precision we're at
|
32
|
+
newtime = time + '-'
|
33
|
+
if time =~ /(\d+):(\d+)([ap]m?|$)?/
|
34
|
+
end_hr = $1.to_i
|
35
|
+
end_mn = $2.to_i + 1
|
36
|
+
if end_mn > 59
|
37
|
+
end_mn -= 60
|
38
|
+
end_hr += 1
|
39
|
+
end
|
40
|
+
end_hr -= 12 if end_hr > 12
|
41
|
+
newtime += "#{end_hr}:#{end_mn}#{$3}" # end-time is 1 minute later
|
42
|
+
elsif time =~ /(\d+)([ap]m?|$)?/
|
43
|
+
end_hr = $1.to_i + 1
|
44
|
+
end_hr -= 12 if end_hr > 12
|
45
|
+
newtime += "#{end_hr}#{$2}" # end-time is 1 hour later
|
28
46
|
end
|
29
|
-
|
30
|
-
newtime
|
31
|
-
|
32
|
-
|
33
|
-
end_hr -= 12 if end_hr > 12
|
34
|
-
newtime += "#{end_hr}#{$2}" # end-time is 1 hour later
|
47
|
+
puts "Converted! #{newtime}" if $DEBUG
|
48
|
+
b+newtime+a
|
49
|
+
else
|
50
|
+
s
|
35
51
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
}
|
42
|
-
puts "Normalized expression: #{expression.inspect}" if $DEBUG
|
52
|
+
}
|
53
|
+
puts "Normalized expression: #{normalized.inspect}" if $DEBUG
|
54
|
+
@normalized = normalized
|
55
|
+
end
|
56
|
+
end
|
43
57
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
58
|
+
def tokenized
|
59
|
+
@tokenized || begin
|
60
|
+
# 2. Tokenize distinct pieces (words) in the expression
|
61
|
+
words = normalized.split(/\s+/)
|
62
|
+
puts words.inspect if $DEBUG
|
63
|
+
tokenized = words.inject([]) do |a,word|
|
64
|
+
a << case word
|
65
|
+
when WordTypes[:ord]
|
66
|
+
{:type => 'ord', :ord => $1}
|
67
|
+
when WordTypes[:word_ord]
|
68
|
+
ord = WordOrds.include?(word.downcase) ? WordOrds.index(word.downcase)+1 : 'last'
|
69
|
+
puts "WordOrd: #{ord}" if $DEBUG
|
70
|
+
{:type => 'ord', :ord => ord}
|
71
|
+
when WordTypes[:wday]
|
72
|
+
{:type => 'wday', :wday => WDay.new($1)}
|
73
|
+
when WordTypes[:year]
|
74
|
+
{:type => 'year', :year => word}
|
75
|
+
when WordTypes[:month]
|
76
|
+
{:type => 'month', :month => Month.new(word)}
|
77
|
+
when WordTypes[:union]
|
78
|
+
{:type => 'union'}
|
79
|
+
when WordTypes[:range]
|
80
|
+
{:type => 'range'}
|
81
|
+
when WordTypes[:timerange]
|
82
|
+
# determine and inject am/pm
|
83
|
+
start_at = $1
|
84
|
+
end_at = $2
|
85
|
+
start_at_p = $1 if start_at =~ /([ap])m?$/
|
86
|
+
end_at_p = $1 if end_at =~ /([ap])m?$/
|
87
|
+
start_hr = start_at.split(/:/)[0].to_i
|
88
|
+
start_hr = '0' if start_hr == '12' # this is used only for > & < comparisons, so converting it to 0 makes everything easier.
|
89
|
+
end_hr = end_at.split(/:/)[0].to_i
|
90
|
+
if start_at_p && !end_at_p
|
91
|
+
# 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.
|
92
|
+
end_at = end_at + (start_hr <= end_hr ? start_at_p : (start_at_p=='a' ? 'p' : 'a'))
|
93
|
+
elsif end_at_p && !start_at_p
|
94
|
+
# 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.
|
95
|
+
start_at = start_at + (start_hr <= end_hr ? end_at_p : (end_at_p=='a' ? 'p' : 'a'))
|
96
|
+
elsif !end_at_p && !start_at_p
|
97
|
+
# If neither had am/pm attached, assume am if after 7, pm if 12 or before 7.
|
98
|
+
start_at_p = (start_hr < 8 ? 'p' : 'a')
|
99
|
+
start_at = start_at + start_at_p
|
100
|
+
# 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.
|
101
|
+
end_at = end_at + (start_hr <= end_hr ? start_at_p : (start_at_p=='a' ? 'p' : 'a'))
|
102
|
+
end
|
103
|
+
start_at += 'm' unless start_at =~ /m$/
|
104
|
+
end_at += 'm' unless end_at =~ /m$/
|
105
|
+
{:type => 'timerange', :start_time => start_at, :end_time => end_at}
|
86
106
|
end
|
87
|
-
|
88
|
-
|
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
|
-
}
|
107
|
+
end.compact
|
108
|
+
@tokenized = tokenized
|
97
109
|
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def language_patterns_combined
|
113
|
+
@language_patterns_combined || begin
|
114
|
+
language_patterns_combined = tokenized.dup
|
98
115
|
|
99
|
-
|
100
|
-
|
101
|
-
|
116
|
+
# 3. Combine common language patterns
|
117
|
+
puts language_patterns_combined.inspect if $DEBUG
|
118
|
+
puts language_patterns_combined.collect {|e| e[:type] }.inspect if $DEBUG
|
102
119
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
120
|
+
something_was_modified = true
|
121
|
+
while something_was_modified
|
122
|
+
something_was_modified = false
|
123
|
+
before_length = language_patterns_combined.length
|
124
|
+
CommonPatterns.each do |pattern|
|
125
|
+
while i = language_patterns_combined.collect {|e| e[:type] }.includes_sequence?(pattern.split(/ /))
|
126
|
+
CommonPatternActions[pattern].call(language_patterns_combined,i)
|
127
|
+
end
|
110
128
|
end
|
129
|
+
after_length = language_patterns_combined.length
|
130
|
+
something_was_modified = true if before_length != after_length
|
111
131
|
end
|
112
|
-
after_length = analyzed_expression.length
|
113
|
-
something_was_modified = true if before_length != after_length
|
114
|
-
end
|
115
132
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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')
|
133
|
+
puts language_patterns_combined.inspect if $DEBUG
|
134
|
+
puts language_patterns_combined.collect {|e| e[:type] }.inspect if $DEBUG
|
135
|
+
|
136
|
+
@language_patterns_combined = language_patterns_combined
|
123
137
|
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def yielded
|
141
|
+
# Binds it all together into a Set or a Union object
|
142
|
+
@yielded || begin
|
143
|
+
|
144
|
+
yielded = language_patterns_combined.dup
|
124
145
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
146
|
+
# What remains should be simply sections of Set logic
|
147
|
+
# 4. Parse Set logic
|
148
|
+
yielded.each_index do |i|
|
149
|
+
yielded[i] = Temporal.new(yielded[i]) unless yielded[i][:type].in?('union', 'range')
|
150
|
+
end
|
151
|
+
|
152
|
+
BooleanPatterns.each do |pattern|
|
153
|
+
while i = yielded.collect {|e| e[:type] }.includes_sequence?(pattern.split(/ /))
|
154
|
+
BooleanPatternActions[pattern].call(yielded,i)
|
155
|
+
break if yielded.length == 1
|
156
|
+
end
|
129
157
|
end
|
130
|
-
end
|
131
158
|
|
132
|
-
|
133
|
-
|
134
|
-
|
159
|
+
# 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
|
160
|
+
if yielded.length > 1
|
161
|
+
raise RuntimeError, "Could not parse Temporal Expression: check to make sure it is clear and has only one possible meaning to an English-speaking person."
|
162
|
+
end
|
163
|
+
|
164
|
+
@yielded = yielded[0]
|
135
165
|
end
|
166
|
+
end
|
167
|
+
end
|
136
168
|
|
137
|
-
|
169
|
+
class << self
|
170
|
+
def parse(expression)
|
171
|
+
puts "Parsing expression: #{expression.inspect}" if $DEBUG
|
172
|
+
Temporal::Parser.new(expression).yielded
|
138
173
|
end
|
139
174
|
end
|
140
175
|
end
|
data/lib/temporals/patterns.rb
CHANGED
@@ -96,7 +96,9 @@ class Temporal
|
|
96
96
|
words.slice!(i+1,2)
|
97
97
|
},
|
98
98
|
'month range month' => lambda {|words,i|
|
99
|
-
raise "Not Implemented Yet!"
|
99
|
+
# raise "Not Implemented Yet!"
|
100
|
+
words[i][:month] = (words[i][:month]..words[i+2][:month])
|
101
|
+
words.slice!(i+1,2)
|
100
102
|
},
|
101
103
|
'ord_wday month' => lambda {|words,i|
|
102
104
|
words[i][:type] = 'ord_wday_month'
|
data/lib/temporals/ruby_ext.rb
CHANGED
@@ -13,12 +13,13 @@ class Array
|
|
13
13
|
any? do |iv|
|
14
14
|
case iv
|
15
15
|
when Range || Array
|
16
|
-
v.
|
16
|
+
v.in?(iv)
|
17
17
|
else
|
18
18
|
if iv.to_s =~ /^\d+$/ && v.to_s =~ /^\d+$/
|
19
19
|
iv.to_i == v.to_i
|
20
20
|
else
|
21
|
-
iv
|
21
|
+
puts "Comparing #{iv} with #{v}" if $DEBUG
|
22
|
+
iv == v
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
data/lib/temporals/types.rb
CHANGED
@@ -23,6 +23,22 @@ class Temporal
|
|
23
23
|
order.include?(word) ? word : (translations.has_key?(word) ? translations[word] : nil)
|
24
24
|
end
|
25
25
|
end
|
26
|
+
|
27
|
+
attr_reader :name, :ord
|
28
|
+
alias :to_s :name
|
29
|
+
alias :inspect :to_s
|
30
|
+
|
31
|
+
def initialize(word)
|
32
|
+
@name = word.is_a?(String) ? self.class.normalize(word) : self.class.order[word]
|
33
|
+
@ord = self.class.order.index(@name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def <=>(other)
|
37
|
+
ord <=> other.ord
|
38
|
+
end
|
39
|
+
def ==(other)
|
40
|
+
ord == other.ord && name == other.name
|
41
|
+
end
|
26
42
|
end
|
27
43
|
|
28
44
|
class WDay < Classification
|
data/spec/temporals_spec.rb
CHANGED
@@ -2,7 +2,21 @@ require 'rubygems'
|
|
2
2
|
require 'spec'
|
3
3
|
require File.dirname(__FILE__) + '/../lib/temporals'
|
4
4
|
|
5
|
+
describe Temporal::Parser do
|
6
|
+
it "should not modify the string passed in" do
|
7
|
+
s = "2pm Tuesdays"
|
8
|
+
Temporal.parse(s)
|
9
|
+
s.should == "2pm Tuesdays"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
5
13
|
describe Temporal do
|
14
|
+
it "Thursday" do
|
15
|
+
t = Temporal.parse('Thursday')
|
16
|
+
t.should_not be_nil
|
17
|
+
t.to_natural.should eql('Thursday')
|
18
|
+
end
|
19
|
+
|
6
20
|
it "1st-2nd and last Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm" do
|
7
21
|
t = Temporal.parse("1st-2nd and last Thursdays of March and April 5-6:30pm and March 16th - 24th at 2-2:30pm")
|
8
22
|
t.include?(Time.parse('2009-03-05 17:54')).should eql(true)
|
@@ -48,7 +62,7 @@ describe Temporal do
|
|
48
62
|
# (1st Thursdays at 4-5pm) and ((First - Fourth of March and April) at 2-3:30pm)
|
49
63
|
# (1st Thursdays at 4-5pm) and (First - Fourth of March) and (April at 2-3:30pm)
|
50
64
|
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.")
|
65
|
+
}.should raise_error(RuntimeError, "Could not parse Temporal Expression: check to make sure it is clear and has only one possible meaning to an English-speaking person.")
|
52
66
|
end
|
53
67
|
|
54
68
|
it "2pm Tuesdays" do
|
@@ -100,4 +114,18 @@ describe Temporal do
|
|
100
114
|
t.include?(Time.parse('2009-01-09 2:14pm')).should eql(true)
|
101
115
|
t.include?(Time.parse('2009-01-09 3:14pm')).should eql(false)
|
102
116
|
end
|
117
|
+
|
118
|
+
it "should parse '2009'" do
|
119
|
+
t = Temporal.parse("2009")
|
120
|
+
t.occurs_on_day?(Date.parse("January 15, 2009")).should eql(true)
|
121
|
+
t.include?(Time.parse('2009-01-09 2:14pm')).should eql(true)
|
122
|
+
t.include?(Time.parse('2009-01-09 3:14pm')).should eql(true)
|
123
|
+
t.occurs_on_day?(Date.parse("August 20, 2010")).should eql(false)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should parse '2-4pm Tuesdays and Thursdays, March through June'" do
|
127
|
+
t = Temporal.parse("2-4pm Tuesdays and Thursdays, March through June")
|
128
|
+
t.include?(Time.parse('2010-03-02 2:10pm')).should eql(true)
|
129
|
+
t.include?(Time.parse('2010-02-02 2:10pm')).should eql(false)
|
130
|
+
end
|
103
131
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: temporals
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Parker
|
@@ -30,7 +30,7 @@ cert_chain:
|
|
30
30
|
teST6sOe8lUhZQ==
|
31
31
|
-----END CERTIFICATE-----
|
32
32
|
|
33
|
-
date:
|
33
|
+
date: 2010-02-12 00:00:00 -05:00
|
34
34
|
default_executable:
|
35
35
|
dependencies:
|
36
36
|
- !ruby/object:Gem::Dependency
|
metadata.gz.sig
CHANGED
Binary file
|