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