markos_validates_timeliness 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/CHANGELOG +121 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +402 -0
  4. data/Rakefile +52 -0
  5. data/TODO +8 -0
  6. data/lib/validates_timeliness/action_view/instance_tag.rb +52 -0
  7. data/lib/validates_timeliness/active_record/attribute_methods.rb +77 -0
  8. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +69 -0
  9. data/lib/validates_timeliness/formats.rb +368 -0
  10. data/lib/validates_timeliness/locale/en.new.yml +18 -0
  11. data/lib/validates_timeliness/locale/en.old.yml +18 -0
  12. data/lib/validates_timeliness/matcher.rb +1 -0
  13. data/lib/validates_timeliness/parser.rb +44 -0
  14. data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +162 -0
  15. data/lib/validates_timeliness/validation_methods.rb +46 -0
  16. data/lib/validates_timeliness/validator.rb +230 -0
  17. data/lib/validates_timeliness/version.rb +3 -0
  18. data/lib/validates_timeliness.rb +59 -0
  19. data/spec/action_view/instance_tag_spec.rb +194 -0
  20. data/spec/active_record/attribute_methods_spec.rb +157 -0
  21. data/spec/active_record/multiparameter_attributes_spec.rb +118 -0
  22. data/spec/formats_spec.rb +313 -0
  23. data/spec/ginger_scenarios.rb +19 -0
  24. data/spec/parser_spec.rb +65 -0
  25. data/spec/resources/application.rb +2 -0
  26. data/spec/resources/person.rb +3 -0
  27. data/spec/resources/schema.rb +10 -0
  28. data/spec/resources/sqlite_patch.rb +19 -0
  29. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +245 -0
  30. data/spec/spec_helper.rb +58 -0
  31. data/spec/time_travel/MIT-LICENSE +20 -0
  32. data/spec/time_travel/time_extensions.rb +33 -0
  33. data/spec/time_travel/time_travel.rb +12 -0
  34. data/spec/validator_spec.rb +723 -0
  35. metadata +104 -0
@@ -0,0 +1,77 @@
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
@@ -0,0 +1,69 @@
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
@@ -0,0 +1,368 @@
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
@@ -0,0 +1,18 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ invalid_date: "is not a valid date"
6
+ invalid_time: "is not a valid time"
7
+ invalid_datetime: "is not a valid datetime"
8
+ is_at: "must be at %{restriction}"
9
+ before: "must be before %{restriction}"
10
+ on_or_before: "must be on or before %{restriction}"
11
+ after: "must be after %{restriction}"
12
+ on_or_after: "must be on or after %{restriction}"
13
+ between: "must be between %{earliest} and %{latest}"
14
+ validates_timeliness:
15
+ error_value_formats:
16
+ date: '%Y-%m-%d'
17
+ time: '%H:%M:%S'
18
+ datetime: '%Y-%m-%d %H:%M:%S'
@@ -0,0 +1,18 @@
1
+ en:
2
+ activerecord:
3
+ errors:
4
+ messages:
5
+ invalid_date: "is not a valid date"
6
+ invalid_time: "is not a valid time"
7
+ invalid_datetime: "is not a valid datetime"
8
+ is_at: "must be at {{restriction}}"
9
+ before: "must be before {{restriction}}"
10
+ on_or_before: "must be on or before {{restriction}}"
11
+ after: "must be after {{restriction}}"
12
+ on_or_after: "must be on or after {{restriction}}"
13
+ between: "must be between {{earliest}} and {{latest}}"
14
+ validates_timeliness:
15
+ error_value_formats:
16
+ date: '%Y-%m-%d'
17
+ time: '%H:%M:%S'
18
+ datetime: '%Y-%m-%d %H:%M:%S'
@@ -0,0 +1 @@
1
+ require 'validates_timeliness/spec/rails/matchers/validate_timeliness'
@@ -0,0 +1,44 @@
1
+ module ValidatesTimeliness
2
+ module Parser
3
+
4
+ class << self
5
+
6
+ def parse(raw_value, type, options={})
7
+ return nil if raw_value.blank?
8
+ return raw_value if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
9
+
10
+ time_array = ValidatesTimeliness::Formats.parse(raw_value, type, options.reverse_merge(:strict => true))
11
+ return nil if time_array.nil?
12
+
13
+ if type == :date
14
+ Date.new(*time_array[0..2]) rescue nil
15
+ else
16
+ make_time(time_array[0..6])
17
+ end
18
+ end
19
+
20
+ def make_time(time_array)
21
+ # Enforce date part validity which Time class does not
22
+ return nil unless Date.valid_civil?(*time_array[0..2])
23
+
24
+ if Time.respond_to?(:zone) && ValidatesTimeliness.use_time_zones
25
+ Time.zone.local(*time_array)
26
+ else
27
+ # Older AR way of handling times with datetime fallback
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
36
+ end
37
+ rescue ArgumentError, TypeError
38
+ nil
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end