texp 0.0.3
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/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
|
+
|