temporals 2.0.0 → 2.0.1
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/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
|