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.
Files changed (59) hide show
  1. data/CHANGELOG +12 -4
  2. data/LICENSE +1 -1
  3. data/README.rdoc +138 -280
  4. data/Rakefile +30 -16
  5. data/lib/generators/validates_timeliness/install_generator.rb +17 -0
  6. data/lib/generators/validates_timeliness/templates/en.yml +16 -0
  7. data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +22 -0
  8. data/lib/validates_timeliness.rb +46 -52
  9. data/lib/validates_timeliness/attribute_methods.rb +51 -0
  10. data/lib/validates_timeliness/conversion.rb +69 -0
  11. data/lib/validates_timeliness/extensions.rb +14 -0
  12. data/lib/validates_timeliness/extensions/date_time_select.rb +45 -0
  13. data/lib/validates_timeliness/extensions/multiparameter_handler.rb +31 -0
  14. data/lib/validates_timeliness/helper_methods.rb +41 -0
  15. data/lib/validates_timeliness/orms/active_record.rb +14 -0
  16. data/lib/validates_timeliness/parser.rb +389 -17
  17. data/lib/validates_timeliness/validator.rb +37 -200
  18. data/lib/validates_timeliness/version.rb +1 -1
  19. data/spec/model_helpers.rb +27 -0
  20. data/spec/spec_helper.rb +74 -43
  21. data/spec/test_model.rb +56 -0
  22. data/spec/validates_timeliness/attribute_methods_spec.rb +36 -0
  23. data/spec/validates_timeliness/conversion_spec.rb +204 -0
  24. data/spec/validates_timeliness/extensions/date_time_select_spec.rb +178 -0
  25. data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +21 -0
  26. data/spec/validates_timeliness/helper_methods_spec.rb +36 -0
  27. data/spec/{formats_spec.rb → validates_timeliness/parser_spec.rb} +105 -71
  28. data/spec/validates_timeliness/validator/after_spec.rb +59 -0
  29. data/spec/validates_timeliness/validator/before_spec.rb +59 -0
  30. data/spec/validates_timeliness/validator/is_at_spec.rb +63 -0
  31. data/spec/validates_timeliness/validator/on_or_after_spec.rb +59 -0
  32. data/spec/validates_timeliness/validator/on_or_before_spec.rb +59 -0
  33. data/spec/validates_timeliness/validator_spec.rb +172 -0
  34. data/validates_timeliness.gemspec +30 -0
  35. metadata +42 -40
  36. data/TODO +0 -8
  37. data/lib/validates_timeliness/action_view/instance_tag.rb +0 -52
  38. data/lib/validates_timeliness/active_record/attribute_methods.rb +0 -77
  39. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +0 -69
  40. data/lib/validates_timeliness/formats.rb +0 -368
  41. data/lib/validates_timeliness/locale/en.new.yml +0 -18
  42. data/lib/validates_timeliness/locale/en.old.yml +0 -18
  43. data/lib/validates_timeliness/matcher.rb +0 -1
  44. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +0 -162
  45. data/lib/validates_timeliness/validation_methods.rb +0 -46
  46. data/spec/action_view/instance_tag_spec.rb +0 -194
  47. data/spec/active_record/attribute_methods_spec.rb +0 -157
  48. data/spec/active_record/multiparameter_attributes_spec.rb +0 -118
  49. data/spec/ginger_scenarios.rb +0 -19
  50. data/spec/parser_spec.rb +0 -65
  51. data/spec/resources/application.rb +0 -2
  52. data/spec/resources/person.rb +0 -3
  53. data/spec/resources/schema.rb +0 -10
  54. data/spec/resources/sqlite_patch.rb +0 -19
  55. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +0 -245
  56. data/spec/time_travel/MIT-LICENSE +0 -20
  57. data/spec/time_travel/time_extensions.rb +0 -33
  58. data/spec/time_travel/time_travel.rb +0 -12
  59. data/spec/validator_spec.rb +0 -723
data/TODO DELETED
@@ -1,8 +0,0 @@
1
- - valid formats could come from locale file
2
- - add replace_formats instead add_formats :before
3
- - array of values for all temporal options
4
- - use tz and zo value from time string?
5
- - filter valid formats rather than remove for hot swapping without recompilation
6
- - config generator
7
- - move all config into top namespace
8
- - remove action_view stuff
@@ -1,52 +0,0 @@
1
- # TODO remove this from the plugin for v3.
2
- module ValidatesTimeliness
3
-
4
- def self.enable_datetime_select_invalid_value_extension!
5
- ::ActionView::Helpers::InstanceTag.send(:include, ValidatesTimeliness::ActionView::InstanceTag)
6
- end
7
-
8
- module ActionView
9
-
10
- # Intercepts the date and time select helpers to reuse the values from the
11
- # the params rather than the parsed value. This allows invalid date/time
12
- # values to be redisplayed instead of blanks to aid correction by the user.
13
- # Its a minor usability improvement which is rarely an issue for the user.
14
- #
15
- module InstanceTag
16
-
17
- def self.included(base)
18
- selector_method = Rails::VERSION::STRING.to_f < 2.2 ? :date_or_time_select : :datetime_selector
19
- base.class_eval do
20
- alias_method :datetime_selector_without_timeliness, selector_method
21
- alias_method selector_method, :datetime_selector_with_timeliness
22
- end
23
- base.alias_method_chain :value, :timeliness
24
- end
25
-
26
- TimelinessDateTime = Struct.new(:year, :month, :day, :hour, :min, :sec)
27
-
28
- def datetime_selector_with_timeliness(*args)
29
- @timeliness_date_or_time_tag = true
30
- datetime_selector_without_timeliness(*args)
31
- end
32
-
33
- def value_with_timeliness(object)
34
- unless @timeliness_date_or_time_tag && @template_object.params[@object_name]
35
- return value_without_timeliness(object)
36
- end
37
-
38
- pairs = @template_object.params[@object_name].select {|k,v| k =~ /^#{@method_name}\(/ }
39
- return value_without_timeliness(object) if pairs.empty?
40
-
41
- values = pairs.map do |(param, value)|
42
- position = param.scan(/\(([0-9]*).*\)/).first.first
43
- [position, value]
44
- end.sort {|a,b| a[0] <=> b[0] }.map {|v| v[1] }
45
-
46
- TimelinessDateTime.new(*values)
47
- end
48
-
49
- end
50
-
51
- end
52
- end
@@ -1,77 +0,0 @@
1
- module ValidatesTimeliness
2
-
3
- def self.enable_active_record_datetime_parser!
4
- ::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::AttributeMethods)
5
- end
6
-
7
- module ActiveRecord
8
-
9
- # Overrides write method for date, time and datetime columns
10
- # to use plugin parser. Also adds mechanism to store value
11
- # before type cast.
12
- #
13
- module AttributeMethods
14
-
15
- def self.included(base)
16
- base.extend ClassMethods
17
- base.class_eval do
18
- alias_method_chain :read_attribute_before_type_cast, :timeliness
19
- class << self
20
- alias_method_chain :define_attribute_methods, :timeliness
21
- end
22
- end
23
- end
24
-
25
- def write_date_time_attribute(attr_name, value, type, time_zone_aware)
26
- @attributes_cache["_#{attr_name}_before_type_cast"] = value
27
- value = ValidatesTimeliness::Parser.parse(value, type)
28
-
29
- if value && type != :date
30
- value = value.to_time
31
- value = value.in_time_zone if time_zone_aware
32
- end
33
-
34
- write_attribute(attr_name.to_sym, value)
35
- end
36
-
37
- def read_attribute_before_type_cast_with_timeliness(attr_name)
38
- cached_attr = "_#{attr_name}_before_type_cast"
39
- return @attributes_cache[cached_attr] if @attributes_cache.has_key?(cached_attr)
40
- read_attribute_before_type_cast_without_timeliness(attr_name)
41
- end
42
-
43
- module ClassMethods
44
-
45
- def define_attribute_methods_with_timeliness
46
- return if generated_methods?
47
- timeliness_methods = []
48
-
49
- columns_hash.each do |name, column|
50
- if [:date, :time, :datetime].include?(column.type)
51
- method_name = "#{name}="
52
- next if instance_method_already_implemented?(method_name)
53
-
54
- time_zone_aware = create_time_zone_conversion_attribute?(name, column) rescue false
55
- define_method(method_name) do |value|
56
- write_date_time_attribute(name, value, column.type, time_zone_aware)
57
- end
58
- timeliness_methods << method_name
59
- end
60
- end
61
-
62
- # Hack to get around instance_method_already_implemented? caching the
63
- # methods in the ivar. It then appears to subsequent calls that the
64
- # methods defined here, have not been and get defined again.
65
- @_defined_class_methods = nil
66
-
67
- define_attribute_methods_without_timeliness
68
- # add generated methods which is a Set object hence no += method
69
- timeliness_methods.each {|attr| generated_methods << attr }
70
- end
71
-
72
- end
73
-
74
- end
75
-
76
- end
77
- end
@@ -1,69 +0,0 @@
1
- module ValidatesTimeliness
2
-
3
- def self.enable_multiparameter_attributes_extension!
4
- ::ActiveRecord::Base.send(:include, ValidatesTimeliness::ActiveRecord::MultiparameterAttributes)
5
- end
6
-
7
- module ActiveRecord
8
-
9
- class << self
10
-
11
- def time_array_to_string(values, type)
12
- values.collect! {|v| v.to_s }
13
-
14
- case type
15
- when :date
16
- extract_date_from_multiparameter_attributes(values)
17
- when :time
18
- extract_time_from_multiparameter_attributes(values)
19
- when :datetime
20
- extract_date_from_multiparameter_attributes(values) + " " + extract_time_from_multiparameter_attributes(values)
21
- end
22
- end
23
-
24
- def extract_date_from_multiparameter_attributes(values)
25
- year = values[0].blank? ? nil : ValidatesTimeliness::Formats.unambiguous_year(values[0].rjust(2, "0"))
26
- [year, *values.slice(1, 2).map { |s| s.blank? ? nil : s.rjust(2, "0") }].join("-")
27
- end
28
-
29
- def extract_time_from_multiparameter_attributes(values)
30
- values[3..5].map { |s| s.blank? ? nil : s.rjust(2, "0") }.join(":")
31
- end
32
-
33
- end
34
-
35
- module MultiparameterAttributes
36
-
37
- def self.included(base)
38
- base.alias_method_chain :execute_callstack_for_multiparameter_attributes, :timeliness
39
- end
40
-
41
- # Assign dates and times as formatted strings to force the use of the plugin parser
42
- def execute_callstack_for_multiparameter_attributes_with_timeliness(callstack)
43
- errors = []
44
- callstack.each do |name, values|
45
- column = column_for_attribute(name)
46
- if column && [:date, :time, :datetime].include?(column.type)
47
- begin
48
- callstack.delete(name)
49
- if values.empty? || values.all?(&:nil?)
50
- send("#{name}=", nil)
51
- else
52
- value = ValidatesTimeliness::ActiveRecord.time_array_to_string(values, column.type)
53
- send("#{name}=", value)
54
- end
55
- rescue => ex
56
- errors << ::ActiveRecord::AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
57
- end
58
- end
59
- end
60
- unless errors.empty?
61
- raise ::ActiveRecord::MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
62
- end
63
- execute_callstack_for_multiparameter_attributes_without_timeliness(callstack)
64
- end
65
-
66
- end
67
-
68
- end
69
- end
@@ -1,368 +0,0 @@
1
- require 'date'
2
-
3
- module ValidatesTimeliness
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 Formats
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
- 'd.m.yy',
100
- 'd mmm yy'
101
- ]
102
-
103
- @@datetime_formats = [
104
- 'yyyy-mm-dd hh:nn:ss',
105
- 'yyyy-mm-dd h:nn',
106
- 'yyyy-mm-dd h:nn_ampm',
107
- 'yyyy-mm-dd hh:nn:ss.u',
108
- 'm/d/yy h:nn:ss',
109
- 'm/d/yy h:nn_ampm',
110
- 'm/d/yy h:nn',
111
- 'd/m/yy hh:nn:ss',
112
- 'd/m/yy h:nn_ampm',
113
- 'd/m/yy h:nn',
114
- 'ddd, dd mmm yyyy hh:nn:ss (zo|tz)', # RFC 822
115
- 'ddd mmm d hh:nn:ss zo yyyy', # Ruby time string
116
- 'yyyy-mm-ddThh:nn:ssZ', # iso 8601 without zone offset
117
- 'yyyy-mm-ddThh:nn:sszo' # iso 8601 with zone offset
118
- ]
119
-
120
-
121
- # All tokens available for format construction. The token array is made of
122
- # token regexp, validation regexp and key for format proc mapping if any.
123
- # If the token needs no format proc arg then the validation regexp should
124
- # not have a capturing group, as all captured groups are passed to the
125
- # format proc.
126
- #
127
- # The token regexp should only use a capture group if 'look-behind' anchor
128
- # is required. The first capture group will be considered a literal and put
129
- # into the validation regexp string as-is. This is a hack.
130
- @@format_tokens = [
131
- { 'd' => [ /(\A|[^d])d{1}(?=[^d])/, '(\d{1,2})', :day ] }, #/
132
- { 'ddd' => [ /d{3,}/, '(\w{3,9})' ] },
133
- { 'dd' => [ /d{2,}/, '(\d{2})', :day ] },
134
- { 'mmm' => [ /m{3,}/, '(\w{3,9})', :month ] },
135
- { 'mm' => [ /m{2}/, '(\d{2})', :month ] },
136
- { 'm' => [ /(\A|[^ap])m{1}/, '(\d{1,2})', :month ] },
137
- { 'yyyy' => [ /y{4,}/, '(\d{4})', :year ] },
138
- { 'yy' => [ /y{2,}/, '(\d{4}|\d{2})', :year ] },
139
- { 'hh' => [ /h{2,}/, '(\d{2})', :hour ] },
140
- { 'h' => [ /h{1}/, '(\d{1,2})', :hour ] },
141
- { 'nn' => [ /n{2,}/, '(\d{2})', :min ] },
142
- { 'n' => [ /n{1}/, '(\d{1,2})', :min ] },
143
- { 'ss' => [ /s{2,}/, '(\d{2})', :sec ] },
144
- { 's' => [ /s{1}/, '(\d{1,2})', :sec ] },
145
- { 'u' => [ /u{1,}/, '(\d{1,6})', :usec ] },
146
- { 'ampm' => [ /ampm/, '((?:[aApP])\.?[mM]\.?)', :meridian ] },
147
- { 'zo' => [ /zo/, '([+-]\d{2}:?\d{2})', :offset ] },
148
- { 'tz' => [ /tz/, '(?:[A-Z]{1,4})' ] },
149
- { '_' => [ /_/, '\s?' ] }
150
- ]
151
-
152
- # Arguments which will be passed to the format proc if matched in the
153
- # time string. The key must be the key from the format tokens. The array
154
- # consists of the arry position of the arg, the arg name, and the code to
155
- # place in the time array slot. The position can be nil which means the arg
156
- # won't be placed in the array.
157
- #
158
- # The code can be used to manipulate the arg value if required, otherwise
159
- # should just be the arg name.
160
- #
161
- @@format_proc_args = {
162
- :year => [0, 'y', 'unambiguous_year(y)'],
163
- :month => [1, 'm', 'month_index(m)'],
164
- :day => [2, 'd', 'd'],
165
- :hour => [3, 'h', 'full_hour(h,md)'],
166
- :min => [4, 'n', 'n'],
167
- :sec => [5, 's', 's'],
168
- :usec => [6, 'u', 'microseconds(u)'],
169
- :offset => [7, 'z', 'offset_in_seconds(z)'],
170
- :meridian => [nil, 'md', nil]
171
- }
172
-
173
- class << self
174
-
175
- def compile_format_expressions
176
- @@time_expressions = compile_formats(@@time_formats)
177
- @@date_expressions = compile_formats(@@date_formats)
178
- @@datetime_expressions = compile_formats(@@datetime_formats)
179
- end
180
-
181
- # Loop through format expressions for type and call proc on matches. Allow
182
- # pre or post match strings to exist if strict is false. Otherwise wrap
183
- # regexp in start and end anchors.
184
- # Returns time array if matches a format, nil otherwise.
185
- def parse(string, type, options={})
186
- return string unless string.is_a?(String)
187
- options.reverse_merge!(:strict => true)
188
-
189
- sets = if options[:format]
190
- options[:strict] = true
191
- [ send("#{type}_expressions").assoc(options[:format]) ]
192
- else
193
- expression_set(type, string)
194
- end
195
-
196
- matches = nil
197
- processor = sets.each do |format, regexp, proc|
198
- full = /\A#{regexp}\Z/ if options[:strict]
199
- full ||= case type
200
- when :date then /\A#{regexp}/
201
- when :time then /#{regexp}\Z/
202
- when :datetime then /\A#{regexp}\Z/
203
- end
204
- break(proc) if matches = full.match(string.strip)
205
- end
206
- last = options[:include_offset] ? 8 : 7
207
- if matches
208
- values = processor.call(*matches[1..last])
209
- values[0..2] = dummy_date_for_time_type if type == :time
210
- return values
211
- end
212
- rescue
213
- nil
214
- end
215
-
216
- # Delete formats of specified type. Error raised if format not found.
217
- def remove_formats(type, *remove_formats)
218
- remove_formats.each do |format|
219
- unless self.send("#{type}_formats").delete(format)
220
- raise "Format #{format} not found in #{type} formats"
221
- end
222
- end
223
- compile_format_expressions
224
- end
225
-
226
- # Adds new formats. Must specify format type and can specify a :before
227
- # option to nominate which format the new formats should be inserted in
228
- # front on to take higher precedence.
229
- # Error is raised if format already exists or if :before format is not found.
230
- def add_formats(type, *add_formats)
231
- formats = self.send("#{type}_formats")
232
- options = {}
233
- options = add_formats.pop if add_formats.last.is_a?(Hash)
234
- before = options[:before]
235
- raise "Format for :before option #{format} was not found." if before && !formats.include?(before)
236
-
237
- add_formats.each do |format|
238
- raise "Format #{format} is already included in #{type} formats" if formats.include?(format)
239
-
240
- index = before ? formats.index(before) : -1
241
- formats.insert(index, format)
242
- end
243
- compile_format_expressions
244
- end
245
-
246
- # Removes formats where the 1 or 2 digit month comes first, to eliminate
247
- # formats which are ambiguous with the European style of day then month.
248
- # The mmm token is ignored as its not ambigous.
249
- def remove_us_formats
250
- us_format_regexp = /\Am{1,2}[^m]/
251
- date_formats.reject! { |format| us_format_regexp =~ format }
252
- datetime_formats.reject! { |format| us_format_regexp =~ format }
253
- compile_format_expressions
254
- end
255
-
256
- def full_hour(hour, meridian)
257
- hour = hour.to_i
258
- return hour if meridian.nil?
259
- if meridian.delete('.').downcase == 'am'
260
- raise if hour == 0 || hour > 12
261
- hour == 12 ? 0 : hour
262
- else
263
- hour == 12 ? hour : hour + 12
264
- end
265
- end
266
-
267
- def unambiguous_year(year)
268
- if year.length <= 2
269
- century = Time.now.year.to_s[0..1].to_i
270
- century -= 1 if year.to_i >= ambiguous_year_threshold
271
- year = "#{century}#{year.rjust(2,'0')}"
272
- end
273
- year.to_i
274
- end
275
-
276
- def month_index(month)
277
- return month.to_i if month.to_i.nonzero?
278
- abbr_month_names.index(month.capitalize) || month_names.index(month.capitalize)
279
- end
280
-
281
- def month_names
282
- defined?(I18n) ? I18n.t('date.month_names') : Date::MONTHNAMES
283
- end
284
-
285
- def abbr_month_names
286
- defined?(I18n) ? I18n.t('date.abbr_month_names') : Date::ABBR_MONTHNAMES
287
- end
288
-
289
- def microseconds(usec)
290
- (".#{usec}".to_f * 1_000_000).to_i
291
- end
292
-
293
- def offset_in_seconds(offset)
294
- sign = offset =~ /^-/ ? -1 : 1
295
- parts = offset.scan(/\d\d/).map {|p| p.to_f }
296
- parts[1] = parts[1].to_f / 60
297
- (parts[0] + parts[1]) * sign * 3600
298
- end
299
-
300
- private
301
-
302
- # Generate regular expression and processor from format string
303
- def generate_format_expression(string_format)
304
- regexp = string_format.dup
305
- order = {}
306
- regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
307
-
308
- format_tokens.each do |token|
309
- token_name = token.keys.first
310
- token_regexp, regexp_str, arg_key = *token.values.first
311
-
312
- # hack for lack of look-behinds. If has a capture group then is
313
- # considered an anchor to put straight back in the regexp string.
314
- regexp.gsub!(token_regexp) {|m| "#{$1}" + regexp_str }
315
- order[arg_key] = $~.begin(0) if $~ && !arg_key.nil?
316
- end
317
-
318
- return Regexp.new(regexp), format_proc(order)
319
- rescue
320
- raise "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
321
- end
322
-
323
- # Generates a proc which when executed maps the regexp capture groups to a
324
- # proc argument based on order captured. A time array is built using the proc
325
- # argument in the position indicated by the first element of the proc arg
326
- # array.
327
- #
328
- def format_proc(order)
329
- arg_map = format_proc_args
330
- args = order.invert.sort.map {|p| arg_map[p[1]][1] }
331
- arr = [nil] * 7
332
- order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? }
333
- proc_string = <<-EOL
334
- lambda {|#{args.join(',')}|
335
- md ||= nil
336
- [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i }
337
- }
338
- EOL
339
- eval proc_string
340
- end
341
-
342
- def compile_formats(formats)
343
- formats.map { |format| [ format, *generate_format_expression(format) ] }
344
- end
345
-
346
- # Pick expression set and combine date and datetimes for
347
- # datetime attributes to allow date string as datetime
348
- def expression_set(type, string)
349
- case type
350
- when :date
351
- date_expressions
352
- when :time
353
- time_expressions
354
- when :datetime
355
- # gives a speed-up for date string as datetime attributes
356
- if string.length < 11
357
- date_expressions + datetime_expressions
358
- else
359
- datetime_expressions + date_expressions
360
- end
361
- end
362
- end
363
-
364
- end
365
- end
366
- end
367
-
368
- ValidatesTimeliness::Formats.compile_format_expressions