texp 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +15 -0
- data/INFO +33 -0
- data/MIT-LICENSE +21 -0
- data/README +2 -0
- data/Rakefile +86 -0
- data/doc/jamis.rb +591 -0
- data/lib/texp.rb +13 -0
- data/lib/texp/base.rb +146 -0
- data/lib/texp/core.rb +41 -0
- data/lib/texp/day_interval.rb +55 -0
- data/lib/texp/day_of_month.rb +32 -0
- data/lib/texp/day_of_week.rb +32 -0
- data/lib/texp/every_day.rb +30 -0
- data/lib/texp/ext.rb +4 -0
- data/lib/texp/hash_builder.rb +44 -0
- data/lib/texp/logic.rb +96 -0
- data/lib/texp/month.rb +32 -0
- data/lib/texp/parse.rb +112 -0
- data/lib/texp/week.rb +61 -0
- data/lib/texp/window.rb +52 -0
- data/lib/texp/year.rb +31 -0
- data/test/texp/day_interval_test.rb +22 -0
- data/test/texp/day_of_month_test.rb +27 -0
- data/test/texp/day_of_week_test.rb +31 -0
- data/test/texp/every_day_test.rb +15 -0
- data/test/texp/ext_test.rb +82 -0
- data/test/texp/hash_test.rb +26 -0
- data/test/texp/inspect_test.rb +65 -0
- data/test/texp/logic_test.rb +51 -0
- data/test/texp/logic_text_test.rb +0 -0
- data/test/texp/month_test.rb +20 -0
- data/test/texp/parse_test.rb +233 -0
- data/test/texp/week_test.rb +58 -0
- data/test/texp/window_test.rb +32 -0
- data/test/texp/year_test.rb +28 -0
- metadata +94 -0
data/lib/texp/month.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module TExp
|
2
|
+
class Month < Base
|
3
|
+
register_parse_callback('m')
|
4
|
+
|
5
|
+
def initialize(months)
|
6
|
+
@months = listize(months)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is +date+ included in the temporal expression.
|
10
|
+
def include?(date)
|
11
|
+
@months.include?(date.month)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Human readable version of the temporal expression.
|
15
|
+
def inspect
|
16
|
+
"the month is " +
|
17
|
+
humanize_list(@months) { |m| Date::MONTHNAMES[m] }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Encode the temporal expression into +codes+.
|
21
|
+
def encode(codes)
|
22
|
+
encode_list(codes, @months)
|
23
|
+
codes << encoding_token
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
build_hash do |b|
|
28
|
+
b.with @months
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/texp/parse.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module TExp
|
2
|
+
|
3
|
+
# Thrown if an error is encountered during the parsing of a temporal
|
4
|
+
# expression.
|
5
|
+
class ParseError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# ------------------------------------------------------------------
|
9
|
+
# Class methods.
|
10
|
+
#
|
11
|
+
class << self
|
12
|
+
PARSE_CALLBACKS = {}
|
13
|
+
|
14
|
+
# Lexical Definitions
|
15
|
+
TOKEN_PATTERNS = [
|
16
|
+
# Dates
|
17
|
+
'\d\d\d\d-\d\d-\d\d',
|
18
|
+
# Numbers
|
19
|
+
'[+-]?\d+',
|
20
|
+
# Extension Tokens
|
21
|
+
'<(?:[a-zA-Z_][a-zA-Z0-9_]*::)?[a-zA-Z_][a-zA-Z0-9_]*>',
|
22
|
+
# Everything else is a single character
|
23
|
+
# (except commas and spaces which are ignored)
|
24
|
+
'[^, ]',
|
25
|
+
].join('|')
|
26
|
+
TOKEN_RE = Regexp.new(TOKEN_PATTERNS)
|
27
|
+
|
28
|
+
# Register a parsing callback. Individual Temporal Expression
|
29
|
+
# classes will register their won callbacks as needed. A handful
|
30
|
+
# of non-class based parser callbacks are registered below.
|
31
|
+
def register_parse_callback(token, callback)
|
32
|
+
PARSE_CALLBACKS[token] = callback
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse a temporal expression string
|
36
|
+
def parse(string)
|
37
|
+
@stack = []
|
38
|
+
string.scan(TOKEN_RE) do |tok|
|
39
|
+
compile(tok)
|
40
|
+
end
|
41
|
+
fail ParseError, "Incomplete expression" if @stack.size > 1
|
42
|
+
@stack.pop
|
43
|
+
end
|
44
|
+
|
45
|
+
def from_params(hash)
|
46
|
+
h = hash['1']
|
47
|
+
tok = h['type']
|
48
|
+
cb = PARSE_CALLBACKS[tok]
|
49
|
+
a1 = h["#{tok}1"]
|
50
|
+
a2 = h["#{tok}2"]
|
51
|
+
if a2
|
52
|
+
cb.new(a1, a2)
|
53
|
+
elsif a1
|
54
|
+
cb.new(a1)
|
55
|
+
else
|
56
|
+
cb.new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Compile the token into the current definition.
|
63
|
+
def compile(tok)
|
64
|
+
handler = PARSE_CALLBACKS[tok]
|
65
|
+
if handler
|
66
|
+
handler.parse_callback(@stack)
|
67
|
+
else
|
68
|
+
case tok
|
69
|
+
when /^\d\d\d\d-\d\d-\d\d/
|
70
|
+
@stack.push Date.parse(tok)
|
71
|
+
when /^[-+]?\d+$/
|
72
|
+
@stack.push tok.to_i
|
73
|
+
else
|
74
|
+
fail ParseError, "Unrecoginized TExp Token '#{tok}'"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end # << TExp
|
80
|
+
|
81
|
+
# Convenience class for creating parse callbacks.
|
82
|
+
class ParseCallback
|
83
|
+
def initialize(&block)
|
84
|
+
@callback = block
|
85
|
+
end
|
86
|
+
def parse_callback(stack)
|
87
|
+
@callback.call(stack)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# List parsing handlers
|
92
|
+
|
93
|
+
MARK = :mark
|
94
|
+
|
95
|
+
# Push a mark on the stack to start a list.
|
96
|
+
register_parse_callback('[',
|
97
|
+
ParseCallback.new do |stack|
|
98
|
+
stack.push MARK
|
99
|
+
end)
|
100
|
+
|
101
|
+
# Pop the stack and build a list until you find a mark.
|
102
|
+
register_parse_callback(']',
|
103
|
+
ParseCallback.new do |stack|
|
104
|
+
list = []
|
105
|
+
while ! stack.empty? && stack.last != MARK
|
106
|
+
list.unshift stack.pop
|
107
|
+
end
|
108
|
+
fail ParseError, "Expression stack exhausted" if stack.empty?
|
109
|
+
stack.pop
|
110
|
+
stack.push list
|
111
|
+
end)
|
112
|
+
end # module TExp
|
data/lib/texp/week.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module TExp
|
4
|
+
class Week < Base
|
5
|
+
register_parse_callback('k')
|
6
|
+
|
7
|
+
def initialize(weeks)
|
8
|
+
@weeks = listize(weeks)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Is +date+ included in the temporal expression.
|
12
|
+
def include?(date)
|
13
|
+
@weeks.include?(week_from_front(date)) ||
|
14
|
+
@weeks.include?(week_from_back(date))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Encode the temporal expression into +codes+.
|
18
|
+
def encode(codes)
|
19
|
+
encode_list(codes, @weeks)
|
20
|
+
codes << encoding_token
|
21
|
+
end
|
22
|
+
|
23
|
+
# Human readable version of the temporal expression.
|
24
|
+
def inspect
|
25
|
+
"it is the " + ordinal_list(@weeks) + " week of the month"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
build_hash do |b|
|
30
|
+
b.with @weeks
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def week_from_front(date)
|
37
|
+
week = ((date.day-1) / 7) + 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def week_from_back(date)
|
41
|
+
last_day = last_day_of_month(date)
|
42
|
+
-(((last_day - date.day) / 7) + 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.days_in_month(month)
|
46
|
+
d = Date.new(2007, month, 1) + 31
|
47
|
+
while d.day < 27
|
48
|
+
d -= 1
|
49
|
+
end
|
50
|
+
d.day
|
51
|
+
end
|
52
|
+
|
53
|
+
DAYS_IN_MONTH = [ 0 ] +
|
54
|
+
(1..12).collect { |m| days_in_month(m) }
|
55
|
+
|
56
|
+
def last_day_of_month(date)
|
57
|
+
DAYS_IN_MONTH[date.month] +
|
58
|
+
(date.month == 2 && Date.leap?(date.year) ? 1 : 0)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/texp/window.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module TExp
|
2
|
+
class Window < Base
|
3
|
+
register_parse_callback('s')
|
4
|
+
|
5
|
+
def initialize(texp, prewindow_days, postwindow_days)
|
6
|
+
@texp = texp
|
7
|
+
@prewindow_days = prewindow_days
|
8
|
+
@postwindow_days = postwindow_days
|
9
|
+
end
|
10
|
+
|
11
|
+
# Is +date+ included in the temporal expression.
|
12
|
+
def include?(date)
|
13
|
+
@texp.include?(date) ||
|
14
|
+
(1..@prewindow_days).any? { |i| @texp.include?(date + i) } ||
|
15
|
+
(1..@postwindow_days).any? { |i| @texp.include?(date - i) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Human readable version of the temporal expression.
|
19
|
+
def inspect
|
20
|
+
@texp.inspect + ", " +
|
21
|
+
"or up to #{days(@prewindow_days)} prior, " +
|
22
|
+
"or up to #{days(@postwindow_days)} after"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Encode the temporal expression into +codes+.
|
26
|
+
def encode(codes)
|
27
|
+
@texp.encode(codes)
|
28
|
+
codes << @prewindow_days << "," << @postwindow_days << "s"
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
fail "TBD"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def days(n)
|
38
|
+
n == 1 ? "#{n} day" : "#{n} days"
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# Parsing callback for window temporal expressions.
|
43
|
+
def parse_callback(stack)
|
44
|
+
postwindow = stack.pop
|
45
|
+
prewindow = stack.pop
|
46
|
+
te = stack.pop
|
47
|
+
stack.push TExp::Window.new(te, prewindow, postwindow)
|
48
|
+
end
|
49
|
+
end # class << self
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/texp/year.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module TExp
|
2
|
+
class Year < Base
|
3
|
+
register_parse_callback('y')
|
4
|
+
|
5
|
+
def initialize(years)
|
6
|
+
@years = listize(years)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is +date+ included in the temporal expression.
|
10
|
+
def include?(date)
|
11
|
+
@years.include?(date.year)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Human readable version of the temporal expression.
|
15
|
+
def inspect
|
16
|
+
"the year is " + humanize_list(@years)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Encode the temporal expression into +codes+.
|
20
|
+
def encode(codes)
|
21
|
+
encode_list(codes, @years)
|
22
|
+
codes << encoding_token
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
build_hash do |b|
|
27
|
+
b.with @years
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'date'
|
5
|
+
require 'texp'
|
6
|
+
|
7
|
+
class DayIntervalTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_day_interval
|
10
|
+
te = TExp::DayInterval.new(Date.parse("Feb 10, 2008"), 3)
|
11
|
+
assert te.include?(Date.parse("Feb 10, 2008"))
|
12
|
+
assert ! te.include?(Date.parse("Feb 11, 2008"))
|
13
|
+
assert ! te.include?(Date.parse("Feb 12, 2008"))
|
14
|
+
assert te.include?(Date.parse("Feb 13, 2008"))
|
15
|
+
assert ! te.include?(Date.parse("Feb 14, 2008"))
|
16
|
+
assert ! te.include?(Date.parse("Feb 15, 2008"))
|
17
|
+
assert te.include?(Date.parse("Feb 16, 2008"))
|
18
|
+
assert ! te.include?(Date.parse("Feb 17, 2008"))
|
19
|
+
assert ! te.include?(Date.parse("Feb 18, 2008"))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'texp'
|
6
|
+
|
7
|
+
class DayOfMonthTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_day_of_month_with_single_arg
|
10
|
+
te = TExp::DayOfMonth.new(14)
|
11
|
+
assert te.include?(Date.parse("Dec 14, 2006"))
|
12
|
+
assert te.include?(Date.parse("Feb 14, 2008"))
|
13
|
+
assert ! te.include?(Date.parse("Feb 15, 2008"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_day_of_include_with_one_day
|
17
|
+
te = TExp::DayOfMonth.new([10, 14, 16])
|
18
|
+
assert te.include?(Date.parse("Feb 10, 2008"))
|
19
|
+
assert ! te.include?(Date.parse("Feb 11, 2008"))
|
20
|
+
assert ! te.include?(Date.parse("Feb 12, 2008"))
|
21
|
+
assert ! te.include?(Date.parse("Feb 13, 2008"))
|
22
|
+
assert te.include?(Date.parse("Feb 14, 2008"))
|
23
|
+
assert ! te.include?(Date.parse("Feb 15, 2008"))
|
24
|
+
assert te.include?(Date.parse("Feb 16, 2008"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'texp'
|
6
|
+
|
7
|
+
class DayOfWeekTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_day_of_week_include_with_one_day
|
10
|
+
te = TExp::DayOfWeek.new([1])
|
11
|
+
assert ! te.include?(Date.parse("Feb 10, 2008"))
|
12
|
+
assert te.include?(Date.parse("Feb 11, 2008"))
|
13
|
+
assert ! te.include?(Date.parse("Feb 12, 2008"))
|
14
|
+
assert ! te.include?(Date.parse("Feb 13, 2008"))
|
15
|
+
assert ! te.include?(Date.parse("Feb 14, 2008"))
|
16
|
+
assert ! te.include?(Date.parse("Feb 15, 2008"))
|
17
|
+
assert ! te.include?(Date.parse("Feb 16, 2008"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_day_of_week_include_with_several_days
|
21
|
+
te = TExp::DayOfWeek.new([1, 3, 5])
|
22
|
+
assert ! te.include?(Date.parse("Feb 10, 2008"))
|
23
|
+
assert te.include?(Date.parse("Feb 11, 2008"))
|
24
|
+
assert ! te.include?(Date.parse("Feb 12, 2008"))
|
25
|
+
assert te.include?(Date.parse("Feb 13, 2008"))
|
26
|
+
assert ! te.include?(Date.parse("Feb 14, 2008"))
|
27
|
+
assert te.include?(Date.parse("Feb 15, 2008"))
|
28
|
+
assert ! te.include?(Date.parse("Feb 16, 2008"))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'date'
|
5
|
+
require 'texp'
|
6
|
+
|
7
|
+
module TExp
|
8
|
+
|
9
|
+
# Extensions is the standard module for TExp extensions.
|
10
|
+
module Extensions
|
11
|
+
|
12
|
+
# You should put your extensions inside your own namespace module.
|
13
|
+
# This will help avoid collisions with with other extensions.
|
14
|
+
module MyExt
|
15
|
+
|
16
|
+
# Create a Temporal Expression class. Inheriting from
|
17
|
+
# TExp::Base allows you to reuse some common functionality
|
18
|
+
# defined there. Other choices are:
|
19
|
+
#
|
20
|
+
# (1) TExp::TermsBase -- Defines a parsing method for handling
|
21
|
+
# lists of temporal expressions as argument lists.
|
22
|
+
#
|
23
|
+
class Never < Base
|
24
|
+
|
25
|
+
# Register your parsing token. Include your namespace as part
|
26
|
+
# of the token to avoid naming collisions with other
|
27
|
+
# extensions.
|
28
|
+
register_parse_callback("<MyExt::never>")
|
29
|
+
|
30
|
+
# Define how your temporal expression class handles dates.
|
31
|
+
# Our example is easy because we always return false.
|
32
|
+
def include?(date)
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
# Define an encoder for your temporal expression. Your
|
37
|
+
# encoder should push tokens onto +codes+ in a manner that
|
38
|
+
# your +parse_handler+ can handle them. Remember that the
|
39
|
+
# temporal expressions mini-language is a stack machine that
|
40
|
+
# pushs arguments onto the stack before your parse handler
|
41
|
+
# sees them.
|
42
|
+
def encode(codes)
|
43
|
+
codes << "<MyExt::never>"
|
44
|
+
end
|
45
|
+
|
46
|
+
class << self
|
47
|
+
# Define a parse handler that will handle your registered
|
48
|
+
# parse token. At the point the handler is called,
|
49
|
+
# arguments will have been pushed onto the stack. All you
|
50
|
+
# have to do is pop them off, construct your temporal
|
51
|
+
# expression and push it back onto the stack.
|
52
|
+
#
|
53
|
+
# See TExp::DayInterval for an example of an expression that
|
54
|
+
# handle arguments.
|
55
|
+
#
|
56
|
+
def parse_callback(stack)
|
57
|
+
stack.push new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end # module MyExt
|
63
|
+
|
64
|
+
end # module Extensions
|
65
|
+
end # module TExp
|
66
|
+
|
67
|
+
class ExtensionsTest < Test::Unit::TestCase
|
68
|
+
def test_never
|
69
|
+
te = TExp::Extensions::MyExt::Never.new
|
70
|
+
assert ! te.include?(Date.today)
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_parse_never
|
74
|
+
te = TExp.parse("<MyExt::never>")
|
75
|
+
assert ! te.include?(Date.today)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_parsing_round_trip
|
79
|
+
assert_equal "<MyExt::never>", TExp.parse("<MyExt::never>").to_s
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|