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 CHANGED
Binary file
@@ -7,7 +7,7 @@ require 'temporals/patterns'
7
7
  require 'temporals/parser'
8
8
 
9
9
  class Temporal
10
- VERSION = '2.0.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.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)
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.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
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.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)
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
@@ -1,140 +1,175 @@
1
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
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
- 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
47
+ puts "Converted! #{newtime}" if $DEBUG
48
+ b+newtime+a
49
+ else
50
+ s
35
51
  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
52
+ }
53
+ puts "Normalized expression: #{normalized.inspect}" if $DEBUG
54
+ @normalized = normalized
55
+ end
56
+ end
43
57
 
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'))
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
- 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
- }
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
- # 3. Combine common patterns
100
- puts analyzed_expression.inspect if $DEBUG
101
- puts analyzed_expression.collect_types.inspect if $DEBUG
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
- 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)
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
- 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')
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
- 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
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
- # 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."
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
- return analyzed_expression[0]
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
@@ -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'
@@ -13,12 +13,13 @@ class Array
13
13
  any? do |iv|
14
14
  case iv
15
15
  when Range || Array
16
- v.to_i.in?(iv)
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.to_s == v.to_s
21
+ puts "Comparing #{iv} with #{v}" if $DEBUG
22
+ iv == v
22
23
  end
23
24
  end
24
25
  end
@@ -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
@@ -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.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: 2009-11-21 00:00:00 -05:00
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