validates_timeliness 2.3.2 → 3.0.0.beta
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 +12 -4
- data/LICENSE +1 -1
- data/README.rdoc +138 -280
- data/Rakefile +30 -16
- data/lib/generators/validates_timeliness/install_generator.rb +17 -0
- data/lib/generators/validates_timeliness/templates/en.yml +16 -0
- data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +22 -0
- data/lib/validates_timeliness.rb +46 -52
- data/lib/validates_timeliness/attribute_methods.rb +51 -0
- data/lib/validates_timeliness/conversion.rb +69 -0
- data/lib/validates_timeliness/extensions.rb +14 -0
- data/lib/validates_timeliness/extensions/date_time_select.rb +45 -0
- data/lib/validates_timeliness/extensions/multiparameter_handler.rb +31 -0
- data/lib/validates_timeliness/helper_methods.rb +41 -0
- data/lib/validates_timeliness/orms/active_record.rb +14 -0
- data/lib/validates_timeliness/parser.rb +389 -17
- data/lib/validates_timeliness/validator.rb +37 -200
- data/lib/validates_timeliness/version.rb +1 -1
- data/spec/model_helpers.rb +27 -0
- data/spec/spec_helper.rb +74 -43
- data/spec/test_model.rb +56 -0
- data/spec/validates_timeliness/attribute_methods_spec.rb +36 -0
- data/spec/validates_timeliness/conversion_spec.rb +204 -0
- data/spec/validates_timeliness/extensions/date_time_select_spec.rb +178 -0
- data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +21 -0
- data/spec/validates_timeliness/helper_methods_spec.rb +36 -0
- data/spec/{formats_spec.rb → validates_timeliness/parser_spec.rb} +105 -71
- data/spec/validates_timeliness/validator/after_spec.rb +59 -0
- data/spec/validates_timeliness/validator/before_spec.rb +59 -0
- data/spec/validates_timeliness/validator/is_at_spec.rb +63 -0
- data/spec/validates_timeliness/validator/on_or_after_spec.rb +59 -0
- data/spec/validates_timeliness/validator/on_or_before_spec.rb +59 -0
- data/spec/validates_timeliness/validator_spec.rb +172 -0
- data/validates_timeliness.gemspec +30 -0
- metadata +42 -40
- data/TODO +0 -8
- data/lib/validates_timeliness/action_view/instance_tag.rb +0 -52
- data/lib/validates_timeliness/active_record/attribute_methods.rb +0 -77
- data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +0 -69
- data/lib/validates_timeliness/formats.rb +0 -368
- data/lib/validates_timeliness/locale/en.new.yml +0 -18
- data/lib/validates_timeliness/locale/en.old.yml +0 -18
- data/lib/validates_timeliness/matcher.rb +0 -1
- data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +0 -162
- data/lib/validates_timeliness/validation_methods.rb +0 -46
- data/spec/action_view/instance_tag_spec.rb +0 -194
- data/spec/active_record/attribute_methods_spec.rb +0 -157
- data/spec/active_record/multiparameter_attributes_spec.rb +0 -118
- data/spec/ginger_scenarios.rb +0 -19
- data/spec/parser_spec.rb +0 -65
- data/spec/resources/application.rb +0 -2
- data/spec/resources/person.rb +0 -3
- data/spec/resources/schema.rb +0 -10
- data/spec/resources/sqlite_patch.rb +0 -19
- data/spec/spec/rails/matchers/validate_timeliness_spec.rb +0 -245
- data/spec/time_travel/MIT-LICENSE +0 -20
- data/spec/time_travel/time_extensions.rb +0 -33
- data/spec/time_travel/time_travel.rb +0 -12
- data/spec/validator_spec.rb +0 -723
@@ -0,0 +1,41 @@
|
|
1
|
+
module ValidatesTimeliness
|
2
|
+
module HelperMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ValidationMethods
|
7
|
+
extend ValidationMethods
|
8
|
+
class_inheritable_hash :timeliness_validated_attributes
|
9
|
+
self.timeliness_validated_attributes = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
module ValidationMethods
|
13
|
+
def validates_timeliness_of(*attr_names)
|
14
|
+
options = _merge_attributes(attr_names)
|
15
|
+
attributes = options[:attributes].inject({}) {|validated, attr_name|
|
16
|
+
attr_name = attr_name.to_s
|
17
|
+
validated[attr_name] = options[:type]
|
18
|
+
validated
|
19
|
+
}
|
20
|
+
self.timeliness_validated_attributes = attributes
|
21
|
+
validates_with Validator, options
|
22
|
+
end
|
23
|
+
|
24
|
+
def validates_date(*attr_names)
|
25
|
+
options = attr_names.extract_options!
|
26
|
+
validates_timeliness_of *(attr_names << options.merge(:type => :date))
|
27
|
+
end
|
28
|
+
|
29
|
+
def validates_time(*attr_names)
|
30
|
+
options = attr_names.extract_options!
|
31
|
+
validates_timeliness_of *(attr_names << options.merge(:type => :time))
|
32
|
+
end
|
33
|
+
|
34
|
+
def validates_datetime(*attr_names)
|
35
|
+
options = attr_names.extract_options!
|
36
|
+
validates_timeliness_of *(attr_names << options.merge(:type => :datetime))
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
include ValidatesTimeliness::HelperMethods
|
3
|
+
include ValidatesTimeliness::AttributeMethods
|
4
|
+
|
5
|
+
def self.define_attribute_methods
|
6
|
+
super
|
7
|
+
# Define write method and before_type_cast method
|
8
|
+
define_timeliness_methods(true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.timeliness_attribute_timezone_aware?(attr_name)
|
12
|
+
create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
13
|
+
end
|
14
|
+
end
|
@@ -1,44 +1,416 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module ValidatesTimeliness
|
2
|
-
|
4
|
+
|
5
|
+
# A date and time parsing library which allows you to add custom formats using
|
6
|
+
# simple predefined tokens. This makes it much easier to catalogue and customize
|
7
|
+
# the formats rather than dealing directly with regular expressions.
|
8
|
+
#
|
9
|
+
# Formats can be added or removed to customize the set of valid date or time
|
10
|
+
# string values.
|
11
|
+
#
|
12
|
+
class Parser
|
13
|
+
cattr_accessor :time_formats,
|
14
|
+
:date_formats,
|
15
|
+
:datetime_formats,
|
16
|
+
:time_expressions,
|
17
|
+
:date_expressions,
|
18
|
+
:datetime_expressions,
|
19
|
+
:format_tokens,
|
20
|
+
:format_proc_args
|
21
|
+
|
22
|
+
|
23
|
+
# Set the threshold value for a two digit year to be considered last century
|
24
|
+
#
|
25
|
+
# Default: 30
|
26
|
+
#
|
27
|
+
# Example:
|
28
|
+
# year = '29' is considered 2029
|
29
|
+
# year = '30' is considered 1930
|
30
|
+
#
|
31
|
+
cattr_accessor :ambiguous_year_threshold
|
32
|
+
self.ambiguous_year_threshold = 30
|
33
|
+
|
34
|
+
# Set the dummy date part for a time type value. Should be an array of 3 values
|
35
|
+
# being year, month and day in that order.
|
36
|
+
#
|
37
|
+
# Default: [ 2000, 1, 1 ] same as ActiveRecord
|
38
|
+
#
|
39
|
+
cattr_accessor :dummy_date_for_time_type
|
40
|
+
self.dummy_date_for_time_type = [ 2000, 1, 1 ]
|
41
|
+
|
42
|
+
# Format tokens:
|
43
|
+
# y = year
|
44
|
+
# m = month
|
45
|
+
# d = day
|
46
|
+
# h = hour
|
47
|
+
# n = minute
|
48
|
+
# s = second
|
49
|
+
# u = micro-seconds
|
50
|
+
# ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.)
|
51
|
+
# _ = optional space
|
52
|
+
# tz = Timezone abbreviation (e.g. UTC, GMT, PST, EST)
|
53
|
+
# zo = Timezone offset (e.g. +10:00, -08:00, +1000)
|
54
|
+
#
|
55
|
+
# All other characters are considered literal. You can embed regexp in the
|
56
|
+
# format but no gurantees that it will remain intact. If you avoid the use
|
57
|
+
# of any token characters and regexp dots or backslashes as special characters
|
58
|
+
# in the regexp, it may well work as expected. For special characters use
|
59
|
+
# POSIX character clsses for safety.
|
60
|
+
#
|
61
|
+
# Repeating tokens:
|
62
|
+
# x = 1 or 2 digits for unit (e.g. 'h' means an hour can be '9' or '09')
|
63
|
+
# xx = 2 digits exactly for unit (e.g. 'hh' means an hour can only be '09')
|
64
|
+
#
|
65
|
+
# Special Cases:
|
66
|
+
# yy = 2 or 4 digit year
|
67
|
+
# yyyy = exactly 4 digit year
|
68
|
+
# mmm = month long name (e.g. 'Jul' or 'July')
|
69
|
+
# ddd = Day name of 3 to 9 letters (e.g. Wed or Wednesday)
|
70
|
+
# u = microseconds matches 1 to 6 digits
|
71
|
+
#
|
72
|
+
# Any other invalid combination of repeating tokens will be swallowed up
|
73
|
+
# by the next lowest length valid repeating token (e.g. yyy will be
|
74
|
+
# replaced with yy)
|
75
|
+
|
76
|
+
@@time_formats = [
|
77
|
+
'hh:nn:ss',
|
78
|
+
'hh-nn-ss',
|
79
|
+
'h:nn',
|
80
|
+
'h.nn',
|
81
|
+
'h nn',
|
82
|
+
'h-nn',
|
83
|
+
'h:nn_ampm',
|
84
|
+
'h.nn_ampm',
|
85
|
+
'h nn_ampm',
|
86
|
+
'h-nn_ampm',
|
87
|
+
'h_ampm'
|
88
|
+
]
|
89
|
+
|
90
|
+
@@date_formats = [
|
91
|
+
'yyyy-mm-dd',
|
92
|
+
'yyyy/mm/dd',
|
93
|
+
'yyyy.mm.dd',
|
94
|
+
'm/d/yy',
|
95
|
+
'd/m/yy',
|
96
|
+
'm\d\yy',
|
97
|
+
'd\m\yy',
|
98
|
+
'd-m-yy',
|
99
|
+
'dd-mm-yyyy',
|
100
|
+
'd.m.yy',
|
101
|
+
'd mmm yy'
|
102
|
+
]
|
103
|
+
|
104
|
+
@@datetime_formats = [
|
105
|
+
'yyyy-mm-dd hh:nn:ss',
|
106
|
+
'yyyy-mm-dd h:nn',
|
107
|
+
'yyyy-mm-dd h:nn_ampm',
|
108
|
+
'yyyy-mm-dd hh:nn:ss.u',
|
109
|
+
'm/d/yy h:nn:ss',
|
110
|
+
'm/d/yy h:nn_ampm',
|
111
|
+
'm/d/yy h:nn',
|
112
|
+
'd/m/yy hh:nn:ss',
|
113
|
+
'd/m/yy h:nn_ampm',
|
114
|
+
'd/m/yy h:nn',
|
115
|
+
'dd-mm-yyyy hh:nn:ss',
|
116
|
+
'dd-mm-yyyy h:nn_ampm',
|
117
|
+
'dd-mm-yyyy h:nn',
|
118
|
+
'ddd, dd mmm yyyy hh:nn:ss (zo|tz)', # RFC 822
|
119
|
+
'ddd mmm d hh:nn:ss zo yyyy', # Ruby time string
|
120
|
+
'yyyy-mm-ddThh:nn:ssZ', # iso 8601 without zone offset
|
121
|
+
'yyyy-mm-ddThh:nn:sszo' # iso 8601 with zone offset
|
122
|
+
]
|
123
|
+
|
124
|
+
|
125
|
+
# All tokens available for format construction. The token array is made of
|
126
|
+
# validation regexp and key for format proc mapping if any.
|
127
|
+
# If the token needs no format proc arg then the validation regexp should
|
128
|
+
# not have a capturing group, as all captured groups are passed to the
|
129
|
+
# format proc.
|
130
|
+
#
|
131
|
+
# The token regexp should only use a capture group if 'look-behind' anchor
|
132
|
+
# is required. The first capture group will be considered a literal and put
|
133
|
+
# into the validation regexp string as-is. This is a hack.
|
134
|
+
#
|
135
|
+
@@format_tokens = {
|
136
|
+
'ddd' => [ '\w{3,9}' ],
|
137
|
+
'dd' => [ '\d{2}', :day ],
|
138
|
+
'd' => [ '\d{1,2}', :day ],
|
139
|
+
'ampm' => [ '[aApP]\.?[mM]\.?', :meridian ],
|
140
|
+
'mmm' => [ '\w{3,9}', :month ],
|
141
|
+
'mm' => [ '\d{2}', :month ],
|
142
|
+
'm' => [ '\d{1,2}', :month ],
|
143
|
+
'yyyy' => [ '\d{4}', :year ],
|
144
|
+
'yy' => [ '\d{4}|\d{2}', :year ],
|
145
|
+
'hh' => [ '\d{2}', :hour ],
|
146
|
+
'h' => [ '\d{1,2}', :hour ],
|
147
|
+
'nn' => [ '\d{2}', :min ],
|
148
|
+
'n' => [ '\d{1,2}', :min ],
|
149
|
+
'ss' => [ '\d{2}', :sec ],
|
150
|
+
's' => [ '\d{1,2}', :sec ],
|
151
|
+
'u' => [ '\d{1,6}', :usec ],
|
152
|
+
'zo' => [ '[+-]\d{2}:?\d{2}', :offset ],
|
153
|
+
'tz' => [ '[A-Z]{1,4}' ],
|
154
|
+
'_' => [ '\s?' ]
|
155
|
+
}
|
156
|
+
|
157
|
+
# Arguments which will be passed to the format proc if matched in the
|
158
|
+
# time string. The key must be the key from the format tokens. The array
|
159
|
+
# consists of the arry position of the arg, the arg name, and the code to
|
160
|
+
# place in the time array slot. The position can be nil which means the arg
|
161
|
+
# won't be placed in the array.
|
162
|
+
#
|
163
|
+
# The code can be used to manipulate the arg value if required, otherwise
|
164
|
+
# should just be the arg name.
|
165
|
+
#
|
166
|
+
@@format_proc_args = {
|
167
|
+
:year => [0, 'y', 'unambiguous_year(y)'],
|
168
|
+
:month => [1, 'm', 'month_index(m)'],
|
169
|
+
:day => [2, 'd', 'd'],
|
170
|
+
:hour => [3, 'h', 'full_hour(h, md ||= nil)'],
|
171
|
+
:min => [4, 'n', 'n'],
|
172
|
+
:sec => [5, 's', 's'],
|
173
|
+
:usec => [6, 'u', 'microseconds(u)'],
|
174
|
+
:offset => [7, 'z', 'offset_in_seconds(z)'],
|
175
|
+
:meridian => [nil, 'md', nil]
|
176
|
+
}
|
177
|
+
|
178
|
+
|
179
|
+
@@type_wrapper = {
|
180
|
+
:date => [/\A/, nil],
|
181
|
+
:time => [nil , /\Z/],
|
182
|
+
:datetime => [/\A/, /\Z/]
|
183
|
+
}
|
3
184
|
|
4
185
|
class << self
|
5
186
|
|
187
|
+
def compile_format_expressions
|
188
|
+
@@time_expressions = compile_formats(@@time_formats)
|
189
|
+
@@date_expressions = compile_formats(@@date_formats)
|
190
|
+
@@datetime_expressions = compile_formats(@@datetime_formats)
|
191
|
+
end
|
192
|
+
|
6
193
|
def parse(raw_value, type, options={})
|
7
194
|
return nil if raw_value.blank?
|
8
|
-
return raw_value if raw_value.acts_like?(:time) || raw_value.
|
195
|
+
return raw_value if raw_value.acts_like?(:time) || raw_value.acts_like?(:date)
|
9
196
|
|
10
|
-
time_array =
|
197
|
+
time_array = _parse(raw_value, type, options.reverse_merge(:strict => true))
|
11
198
|
return nil if time_array.nil?
|
12
199
|
|
13
200
|
if type == :date
|
14
201
|
Date.new(*time_array[0..2]) rescue nil
|
15
202
|
else
|
16
|
-
make_time(time_array[0..
|
203
|
+
make_time(time_array[0..7], options[:timezone_aware])
|
17
204
|
end
|
18
205
|
end
|
19
206
|
|
20
|
-
def make_time(time_array)
|
21
|
-
# Enforce date part validity which Time class does not
|
207
|
+
def make_time(time_array, timezone_aware=false)
|
208
|
+
# Enforce strict date part validity which Time class does not
|
22
209
|
return nil unless Date.valid_civil?(*time_array[0..2])
|
23
210
|
|
24
|
-
if
|
211
|
+
if timezone_aware
|
25
212
|
Time.zone.local(*time_array)
|
26
213
|
else
|
27
|
-
|
28
|
-
begin
|
29
|
-
time_zone = ValidatesTimeliness.default_timezone
|
30
|
-
Time.send(time_zone, *time_array)
|
31
|
-
rescue ArgumentError, TypeError
|
32
|
-
zone_offset = time_zone == :local ? DateTime.local_offset : 0
|
33
|
-
time_array.pop # remove microseconds
|
34
|
-
DateTime.civil(*(time_array << zone_offset))
|
35
|
-
end
|
214
|
+
Time.time_with_datetime_fallback(ValidatesTimeliness.default_timezone, *time_array)
|
36
215
|
end
|
37
216
|
rescue ArgumentError, TypeError
|
38
217
|
nil
|
39
218
|
end
|
40
219
|
|
41
|
-
|
220
|
+
# Loop through format expressions for type and call the format method on a match.
|
221
|
+
# Allow pre or post match strings to exist if strict is false. Otherwise wrap
|
222
|
+
# regexp in start and end anchors.
|
223
|
+
#
|
224
|
+
# Returns time array if matches a format, nil otherwise.
|
225
|
+
#
|
226
|
+
def _parse(string, type, options={})
|
227
|
+
options.reverse_merge!(:strict => true)
|
42
228
|
|
229
|
+
sets = if options[:format]
|
230
|
+
options[:strict] = true
|
231
|
+
[ send("#{type}_expressions").assoc(options[:format]) ]
|
232
|
+
else
|
233
|
+
expression_set(type, string)
|
234
|
+
end
|
235
|
+
|
236
|
+
set = sets.find do |format, regexp|
|
237
|
+
string =~ wrap_regexp(regexp, type, options[:strict])
|
238
|
+
end
|
239
|
+
|
240
|
+
if set
|
241
|
+
last = options[:include_offset] ? 8 : 7
|
242
|
+
values = send(:"format_#{set[0]}", *$~[1..last])
|
243
|
+
values[0..2] = ValidatesTimeliness.dummy_date_for_time_type if type == :time
|
244
|
+
return values
|
245
|
+
end
|
246
|
+
rescue
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
|
250
|
+
# Delete formats of specified type. Error raised if format not found.
|
251
|
+
def remove_formats(type, *remove_formats)
|
252
|
+
remove_formats.each do |format|
|
253
|
+
unless self.send("#{type}_formats").delete(format)
|
254
|
+
raise "Format #{format} not found in #{type} formats"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
compile_format_expressions
|
258
|
+
end
|
259
|
+
|
260
|
+
# Adds new formats. Must specify format type and can specify a :before
|
261
|
+
# option to nominate which format the new formats should be inserted in
|
262
|
+
# front on to take higher precedence.
|
263
|
+
# Error is raised if format already exists or if :before format is not found.
|
264
|
+
def add_formats(type, *add_formats)
|
265
|
+
formats = self.send("#{type}_formats")
|
266
|
+
options = {}
|
267
|
+
options = add_formats.pop if add_formats.last.is_a?(Hash)
|
268
|
+
before = options[:before]
|
269
|
+
raise "Format for :before option #{format} was not found." if before && !formats.include?(before)
|
270
|
+
|
271
|
+
add_formats.each do |format|
|
272
|
+
raise "Format #{format} is already included in #{type} formats" if formats.include?(format)
|
273
|
+
|
274
|
+
index = before ? formats.index(before) : -1
|
275
|
+
formats.insert(index, format)
|
276
|
+
end
|
277
|
+
compile_format_expressions
|
278
|
+
end
|
279
|
+
|
280
|
+
# Removes formats where the 1 or 2 digit month comes first, to eliminate
|
281
|
+
# formats which are ambiguous with the European style of day then month.
|
282
|
+
# The mmm token is ignored as its not ambigous.
|
283
|
+
def remove_us_formats
|
284
|
+
us_format_regexp = /\Am{1,2}[^m]/
|
285
|
+
date_formats.reject! { |format| us_format_regexp =~ format }
|
286
|
+
datetime_formats.reject! { |format| us_format_regexp =~ format }
|
287
|
+
compile_format_expressions
|
288
|
+
end
|
289
|
+
|
290
|
+
def full_hour(hour, meridian)
|
291
|
+
hour = hour.to_i
|
292
|
+
return hour if meridian.nil?
|
293
|
+
if meridian.delete('.').downcase == 'am'
|
294
|
+
raise if hour == 0 || hour > 12
|
295
|
+
hour == 12 ? 0 : hour
|
296
|
+
else
|
297
|
+
hour == 12 ? hour : hour + 12
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def unambiguous_year(year)
|
302
|
+
if year.length <= 2
|
303
|
+
century = Time.now.year.to_s[0..1].to_i
|
304
|
+
century -= 1 if year.to_i >= ambiguous_year_threshold
|
305
|
+
year = "#{century}#{year.rjust(2,'0')}"
|
306
|
+
end
|
307
|
+
year.to_i
|
308
|
+
end
|
309
|
+
|
310
|
+
def month_index(month)
|
311
|
+
return month.to_i if month.to_i.nonzero?
|
312
|
+
abbr_month_names.index(month.capitalize) || month_names.index(month.capitalize)
|
313
|
+
end
|
314
|
+
|
315
|
+
def month_names
|
316
|
+
I18n.t('date.month_names')
|
317
|
+
end
|
318
|
+
|
319
|
+
def abbr_month_names
|
320
|
+
I18n.t('date.abbr_month_names')
|
321
|
+
end
|
322
|
+
|
323
|
+
def microseconds(usec)
|
324
|
+
(".#{usec}".to_f * 1_000_000).to_i
|
325
|
+
end
|
326
|
+
|
327
|
+
def offset_in_seconds(offset)
|
328
|
+
sign = offset =~ /^-/ ? -1 : 1
|
329
|
+
parts = offset.scan(/\d\d/).map {|p| p.to_f }
|
330
|
+
parts[1] = parts[1].to_f / 60
|
331
|
+
(parts[0] + parts[1]) * sign * 3600
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
# Generate regular expression from format string
|
337
|
+
def generate_format_expression(string_format)
|
338
|
+
format = string_format.dup
|
339
|
+
format.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
|
340
|
+
found_tokens, token_order = [], []
|
341
|
+
|
342
|
+
tokens = format_tokens.keys.sort {|a,b| a.size <=> b.size }.reverse
|
343
|
+
tokens.each do |token|
|
344
|
+
regexp_str, arg_key = *format_tokens[token]
|
345
|
+
if format.gsub!(/#{token}/, "%<#{found_tokens.size}>")
|
346
|
+
regexp_str = "(#{regexp_str})" if arg_key
|
347
|
+
found_tokens << [regexp_str, arg_key]
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
format.scan(/%<(\d)>/).each {|token_index|
|
352
|
+
token_index = token_index.first
|
353
|
+
token = found_tokens[token_index.to_i]
|
354
|
+
format.gsub!("%<#{token_index}>", token[0])
|
355
|
+
token_order << token[1]
|
356
|
+
}
|
357
|
+
|
358
|
+
compile_format_method(token_order.compact, string_format)
|
359
|
+
Regexp.new(format)
|
360
|
+
rescue
|
361
|
+
raise "The following format regular expression failed to compile: #{format}\n from format #{string_format}."
|
362
|
+
end
|
363
|
+
|
364
|
+
# Compiles a format method which maps the regexp capture groups to method
|
365
|
+
# arguments based on order captured. A time array is built using the values
|
366
|
+
# in the position indicated by the first element of the proc arg array.
|
367
|
+
#
|
368
|
+
def compile_format_method(order, name)
|
369
|
+
values = [nil] * 7
|
370
|
+
args = []
|
371
|
+
order.each do |part|
|
372
|
+
proc_arg = format_proc_args[part]
|
373
|
+
args << proc_arg[1]
|
374
|
+
values[proc_arg[0]] = proc_arg[2] if proc_arg[0]
|
375
|
+
end
|
376
|
+
class_eval <<-DEF
|
377
|
+
class << self
|
378
|
+
define_method(:"format_#{name}") do |#{args.join(',')}|
|
379
|
+
[#{values.map {|i| i || 'nil' }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i }
|
380
|
+
end
|
381
|
+
end
|
382
|
+
DEF
|
383
|
+
end
|
384
|
+
|
385
|
+
def compile_formats(formats)
|
386
|
+
formats.map { |format| [ format, generate_format_expression(format) ] }
|
387
|
+
end
|
388
|
+
|
389
|
+
# Pick expression set and combine date and datetimes for
|
390
|
+
# datetime attributes to allow date string as datetime
|
391
|
+
def expression_set(type, string)
|
392
|
+
case type
|
393
|
+
when :date
|
394
|
+
date_expressions
|
395
|
+
when :time
|
396
|
+
time_expressions
|
397
|
+
when :datetime
|
398
|
+
# gives a speed-up for date string as datetime attributes
|
399
|
+
if string.length < 11
|
400
|
+
date_expressions + datetime_expressions
|
401
|
+
else
|
402
|
+
datetime_expressions + date_expressions
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def wrap_regexp(regexp, type, strict=false)
|
408
|
+
type = strict ? :datetime : type
|
409
|
+
/#{@@type_wrapper[type][0]}#{regexp}#{@@type_wrapper[type][1]}/
|
410
|
+
end
|
411
|
+
|
412
|
+
end
|
43
413
|
end
|
44
414
|
end
|
415
|
+
|
416
|
+
ValidatesTimeliness::Parser.compile_format_expressions
|