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.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'texp/core'
|
2
|
+
require 'texp/base'
|
3
|
+
require 'texp/parse'
|
4
|
+
require 'texp/day_of_week'
|
5
|
+
require 'texp/day_of_month'
|
6
|
+
require 'texp/week'
|
7
|
+
require 'texp/month'
|
8
|
+
require 'texp/year'
|
9
|
+
require 'texp/day_interval'
|
10
|
+
require 'texp/every_day'
|
11
|
+
require 'texp/window'
|
12
|
+
require 'texp/logic'
|
13
|
+
require 'texp/ext'
|
data/lib/texp/base.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'texp/hash_builder'
|
2
|
+
|
3
|
+
module TExp
|
4
|
+
# Abstract Base class for all Texp Temporal Expressions.
|
5
|
+
class Base
|
6
|
+
|
7
|
+
# Convert the temporal expression into an encoded string (that can
|
8
|
+
# be parsed by TExp.parse).
|
9
|
+
def to_s
|
10
|
+
codes = []
|
11
|
+
encode(codes)
|
12
|
+
codes.join("")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Coerce +arg+ into a list (i.e. Array) if it is not one already.
|
18
|
+
def listize(arg)
|
19
|
+
case arg
|
20
|
+
when Array
|
21
|
+
arg
|
22
|
+
else
|
23
|
+
[arg]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Encode the date into the codes receiver.
|
28
|
+
def encode_date(codes, date)
|
29
|
+
codes << date.strftime("%Y-%m-%d")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Encode the list into the codes receiver. All
|
33
|
+
def encode_list(codes, list)
|
34
|
+
if list.empty?
|
35
|
+
codes << "[]"
|
36
|
+
elsif list.size == 1
|
37
|
+
codes << list.first
|
38
|
+
else
|
39
|
+
codes << "["
|
40
|
+
prev = nil
|
41
|
+
list.each do |item|
|
42
|
+
codes << "," if prev && ! prev.kind_of?(TExp::Base)
|
43
|
+
codes << item.to_s
|
44
|
+
prev = item
|
45
|
+
end
|
46
|
+
codes << "]"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# For the list of integers as a list of ordinal numbers. By
|
51
|
+
# default, use 'or' as a connectingin word. (e.g. [1,2,3] => "1st,
|
52
|
+
# 2nd, or 3rd")
|
53
|
+
def ordinal_list(list, connector='or')
|
54
|
+
humanize_list(list, connector) { |item| ordinal(item) }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Format the list in a human readable format. By default, use
|
58
|
+
# "or" as a connecting word. (e.g. ['a', 'b', 'c'] => "a, b, or
|
59
|
+
# c")
|
60
|
+
def humanize_list(list, connector='or', &block)
|
61
|
+
block ||= lambda { |item| item.to_s }
|
62
|
+
list = list.sort if list.all? { |item| item.kind_of?(Integer) && item >= 0 }
|
63
|
+
case list.size
|
64
|
+
when 1
|
65
|
+
block.call(list.first)
|
66
|
+
else
|
67
|
+
list[0...-1].collect { |d|
|
68
|
+
block.call(d)
|
69
|
+
}.join(", ") +
|
70
|
+
" #{connector} " + block.call(list.last)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Format +date+ in a standard, human-readable format.
|
75
|
+
def humanize_date(date)
|
76
|
+
date.strftime("%B %d, %Y")
|
77
|
+
end
|
78
|
+
|
79
|
+
# Hash of cardinal integers to ordinal suffixes.
|
80
|
+
SUFFIX = {
|
81
|
+
1 => "st",
|
82
|
+
2 => "nd",
|
83
|
+
3 => "rd",
|
84
|
+
4 => "th",
|
85
|
+
5 => "th",
|
86
|
+
6 => "th",
|
87
|
+
7 => "th",
|
88
|
+
8 => "th",
|
89
|
+
9 => "th",
|
90
|
+
0 => "th",
|
91
|
+
11 => "th",
|
92
|
+
12 => "th",
|
93
|
+
13 => "th",
|
94
|
+
}
|
95
|
+
|
96
|
+
# Return the ordinal abbreviation for the integer +n+. (e.g. 1 =>
|
97
|
+
# "1st", 3 => "3rd")
|
98
|
+
def ordinal(n)
|
99
|
+
if n == -1
|
100
|
+
"last"
|
101
|
+
elsif n == -2
|
102
|
+
"next to the last"
|
103
|
+
elsif n < 0
|
104
|
+
ordinal(-n) + " from the last"
|
105
|
+
else
|
106
|
+
n.to_s + suffix(n)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# The ordinal suffex appropriate for the given number. (e.g. 1 =>
|
111
|
+
# "st", 2 => "nd")
|
112
|
+
def suffix(n)
|
113
|
+
SUFFIX[n % 100] || SUFFIX[n % 10]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Convenience method for accessing the class encoding token.
|
117
|
+
def encoding_token
|
118
|
+
self.class.encoding_token
|
119
|
+
end
|
120
|
+
|
121
|
+
# Build a params hash for this temporal expression.
|
122
|
+
def build_hash
|
123
|
+
builder = HashBuilder.new(encoding_token)
|
124
|
+
yield builder if block_given?
|
125
|
+
builder.hash
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
# The token to be used for encoding this temporal expression.
|
130
|
+
attr_reader :encoding_token
|
131
|
+
|
132
|
+
# Register a parse callack for the encoding token for this
|
133
|
+
# class.
|
134
|
+
def register_parse_callback(token, callback=self)
|
135
|
+
@encoding_token = token if callback == self
|
136
|
+
TExp.register_parse_callback(token, callback)
|
137
|
+
end
|
138
|
+
|
139
|
+
# The default parsing callback for single argument time
|
140
|
+
# expressions. Override if you need anything more complicated.
|
141
|
+
def parse_callback(stack)
|
142
|
+
stack.push new(stack.pop)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/texp/core.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module TExp
|
2
|
+
class << self
|
3
|
+
def from_db(string)
|
4
|
+
stack = []
|
5
|
+
string.split(":").each do |code|
|
6
|
+
case code
|
7
|
+
when /^\d+$/
|
8
|
+
stack.push(code.to_i)
|
9
|
+
when /^\d\d\d\d-\d\d-\d\d$/
|
10
|
+
stack.push(Date.parse(code))
|
11
|
+
when 'x'
|
12
|
+
n = stack.pop
|
13
|
+
args = []
|
14
|
+
n.times do args.unshift(stack.pop) end
|
15
|
+
stack.push args
|
16
|
+
when 'e'
|
17
|
+
stack.push TExp::EveryDay.new
|
18
|
+
when 'm'
|
19
|
+
stack.push TExp::DayOfMonth.new(stack.pop)
|
20
|
+
when 'w'
|
21
|
+
stack.push TExp::DayOfWeek.new(stack.pop)
|
22
|
+
when 'i'
|
23
|
+
interval = stack.pop
|
24
|
+
date = stack.pop
|
25
|
+
stack.push TExp::DayInterval.new(date, interval)
|
26
|
+
when 'a'
|
27
|
+
right = stack.pop
|
28
|
+
left = stack.pop
|
29
|
+
stack.push(TExp::And.new(left, right))
|
30
|
+
when 'o'
|
31
|
+
right = stack.pop
|
32
|
+
left = stack.pop
|
33
|
+
stack.push(TExp::Or.new(left, right))
|
34
|
+
else
|
35
|
+
fail "Unknown TExp DB Code"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
stack.pop
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module TExp
|
2
|
+
class DayInterval < Base
|
3
|
+
register_parse_callback('i')
|
4
|
+
|
5
|
+
def initialize(base_date, interval)
|
6
|
+
@base_date = base_date
|
7
|
+
@interval = interval
|
8
|
+
end
|
9
|
+
|
10
|
+
# Is +date+ included in the temporal expression.
|
11
|
+
def include?(date)
|
12
|
+
((date.mjd - base_mjd) % @interval) == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# Human readable version of the temporal expression.
|
16
|
+
def inspect
|
17
|
+
if @interval == 1
|
18
|
+
"every day starting on #{humanize_date(@base_date)}"
|
19
|
+
else
|
20
|
+
"every #{ordinal(@interval)} day starting on #{humanize_date(@base_date)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Encode the temporal expression into +codes+.
|
25
|
+
def encode(codes)
|
26
|
+
encode_date(codes, @base_date)
|
27
|
+
codes << ',' << @interval << encoding_token
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_hash
|
31
|
+
build_hash do |b|
|
32
|
+
b.with(@base_date)
|
33
|
+
b.with(@interval)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def base_mjd
|
40
|
+
@base_date.mjd
|
41
|
+
end
|
42
|
+
|
43
|
+
def formatted_date
|
44
|
+
@base_date.strftime("%Y-%m-%d")
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def parse_callback(stack)
|
49
|
+
interval = stack.pop
|
50
|
+
date = stack.pop
|
51
|
+
stack.push TExp::DayInterval.new(date, interval)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TExp
|
2
|
+
class DayOfMonth < Base
|
3
|
+
register_parse_callback('d')
|
4
|
+
|
5
|
+
def initialize(days)
|
6
|
+
@days = listize(days)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is +date+ included in the temporal expression.
|
10
|
+
def include?(date)
|
11
|
+
@days.include?(date.day)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Human readable version of the temporal expression.
|
15
|
+
def inspect
|
16
|
+
"the day of the month is the " +
|
17
|
+
ordinal_list(@days)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Encode the temporal expression into +codes+.
|
21
|
+
def encode(codes)
|
22
|
+
encode_list(codes, @days)
|
23
|
+
codes << encoding_token
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
build_hash do |b|
|
28
|
+
b.with(@days.map { |d| d.to_s })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TExp
|
2
|
+
class DayOfWeek < Base
|
3
|
+
register_parse_callback('w')
|
4
|
+
|
5
|
+
def initialize(days)
|
6
|
+
@days = listize(days)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is +date+ included in the temporal expression.
|
10
|
+
def include?(date)
|
11
|
+
@days.include?(date.wday)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Human readable version of the temporal expression.
|
15
|
+
def inspect
|
16
|
+
"the day of the week is " +
|
17
|
+
humanize_list(@days) { |d| Date::DAYNAMES[d] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
build_hash do |b|
|
22
|
+
b.with(@days)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Encode the temporal expression into +codes+.
|
27
|
+
def encode(codes)
|
28
|
+
encode_list(codes, @days)
|
29
|
+
codes << encoding_token
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module TExp
|
2
|
+
class EveryDay < Base
|
3
|
+
register_parse_callback('e')
|
4
|
+
|
5
|
+
# Is +date+ included in the temporal expression.
|
6
|
+
def include?(date)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
# Human readable version of the temporal expression.
|
11
|
+
def inspect
|
12
|
+
"every day"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Encode the temporal expression into +codes+.
|
16
|
+
def encode(codes)
|
17
|
+
codes << encoding_token
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
build_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def parse_callback(stack)
|
26
|
+
stack.push TExp::EveryDay.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/texp/ext.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module TExp
|
2
|
+
|
3
|
+
# Build a temporal expression hash representation
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# builder = HashBuilder.new(encoding_token)
|
8
|
+
# builder.with(the_first_paramter)
|
9
|
+
# builder.with(the_next_parameter)
|
10
|
+
# hash = builder.hash
|
11
|
+
#
|
12
|
+
class HashBuilder
|
13
|
+
# Convert the builder to a hash.
|
14
|
+
attr_reader :hash
|
15
|
+
|
16
|
+
# Create a temporal expression params hash builder.
|
17
|
+
def initialize(type)
|
18
|
+
@type = type
|
19
|
+
@hash = { 'type' => type }
|
20
|
+
@args = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add an argument to the hash.
|
24
|
+
def with(arg)
|
25
|
+
@args += 1
|
26
|
+
@hash["#{@type}#{@args}"] = simplify(arg)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Simplify the value to be placed in the hash. Essentially we
|
31
|
+
# want only strings and lists.
|
32
|
+
def simplify(value)
|
33
|
+
case value
|
34
|
+
when String
|
35
|
+
value
|
36
|
+
when Array
|
37
|
+
value.map { |item| simplify(item) }
|
38
|
+
else
|
39
|
+
value.to_s
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/texp/logic.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module TExp
|
2
|
+
|
3
|
+
# Base class for temporal expressions with multiple sub-expressions
|
4
|
+
# (i.e. terms).
|
5
|
+
class TermsBase < Base
|
6
|
+
class << self
|
7
|
+
# Parsing callback for terms based temporal expressions. The
|
8
|
+
# top of the stack is assumed to be a list that is *-expanded to
|
9
|
+
# the temporal expression's constructor.
|
10
|
+
def parse_callback(stack)
|
11
|
+
stack.push self.new(*stack.pop)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Logically AND a list of temporal expressions. A date is included
|
17
|
+
# only if it is included in all of the sub-expressions.
|
18
|
+
class And < TermsBase
|
19
|
+
register_parse_callback('a')
|
20
|
+
|
21
|
+
# Create an AND temporal expression.
|
22
|
+
def initialize(*terms)
|
23
|
+
@terms = terms
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is +date+ included in the temporal expression.
|
27
|
+
def include?(date)
|
28
|
+
@terms.all? { |te| te.include?(date) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Human readable version of the temporal expression.
|
32
|
+
def inspect
|
33
|
+
humanize_list(@terms, "and") { |item| item.inspect }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Encode the temporal expression into +codes+.
|
37
|
+
def encode(codes)
|
38
|
+
encode_list(codes, @terms)
|
39
|
+
codes << encoding_token
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Logically OR a list of temporal expressions. A date is included
|
44
|
+
# if it is included in any of the sub-expressions.
|
45
|
+
class Or < TermsBase
|
46
|
+
register_parse_callback('o')
|
47
|
+
|
48
|
+
# Create an OR temporal expression.
|
49
|
+
def initialize(*terms)
|
50
|
+
@terms = terms
|
51
|
+
end
|
52
|
+
|
53
|
+
# Is +date+ included in the temporal expression.
|
54
|
+
def include?(date)
|
55
|
+
@terms.any? { |te| te.include?(date) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Human readable version of the temporal expression.
|
59
|
+
def inspect
|
60
|
+
humanize_list(@terms) { |item| item.inspect }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Encode the temporal expression into +codes+.
|
64
|
+
def encode(codes)
|
65
|
+
encode_list(codes, @terms)
|
66
|
+
codes << encoding_token
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Logically NEGATE a temporal expression. A date is included if it
|
71
|
+
# is not included in the sub-expression.
|
72
|
+
class Not < Base
|
73
|
+
register_parse_callback('n')
|
74
|
+
|
75
|
+
# Create a NOT temporal expression.
|
76
|
+
def initialize(term)
|
77
|
+
@term = term
|
78
|
+
end
|
79
|
+
|
80
|
+
# Is date included in the temporal expression.
|
81
|
+
def include?(date)
|
82
|
+
! @term.include?(date)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Human readable version of the temporal expression.
|
86
|
+
def inspect
|
87
|
+
"it is not the case that " + @term.inspect
|
88
|
+
end
|
89
|
+
|
90
|
+
# Encode the temporal expression into +codes+.
|
91
|
+
def encode(codes)
|
92
|
+
@term.encode(codes)
|
93
|
+
codes << encoding_token
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|