szilm-validates_timeliness 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/CHANGELOG +116 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +403 -0
  4. data/Rakefile +52 -0
  5. data/TODO +8 -0
  6. data/lib/validates_timeliness.rb +49 -0
  7. data/lib/validates_timeliness/action_view/instance_tag.rb +52 -0
  8. data/lib/validates_timeliness/active_record/attribute_methods.rb +77 -0
  9. data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +69 -0
  10. data/lib/validates_timeliness/formats.rb +365 -0
  11. data/lib/validates_timeliness/locale/en.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/spec/action_view/instance_tag_spec.rb +194 -0
  19. data/spec/active_record/attribute_methods_spec.rb +157 -0
  20. data/spec/active_record/multiparameter_attributes_spec.rb +118 -0
  21. data/spec/formats_spec.rb +306 -0
  22. data/spec/ginger_scenarios.rb +19 -0
  23. data/spec/parser_spec.rb +61 -0
  24. data/spec/resources/application.rb +2 -0
  25. data/spec/resources/person.rb +3 -0
  26. data/spec/resources/schema.rb +10 -0
  27. data/spec/resources/sqlite_patch.rb +19 -0
  28. data/spec/spec/rails/matchers/validate_timeliness_spec.rb +245 -0
  29. data/spec/spec_helper.rb +58 -0
  30. data/spec/time_travel/MIT-LICENSE +20 -0
  31. data/spec/time_travel/time_extensions.rb +33 -0
  32. data/spec/time_travel/time_travel.rb +12 -0
  33. data/spec/validator_spec.rb +713 -0
  34. metadata +102 -0
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+ require 'lib/validates_timeliness/version'
7
+
8
+ GEM = "validates_timeliness"
9
+ GEM_VERSION = ValidatesTimeliness::VERSION
10
+
11
+ spec = Gem::Specification.new do |s|
12
+ s.name = GEM
13
+ s.version = GEM_VERSION
14
+ s.platform = Gem::Platform::RUBY
15
+ s.rubyforge_project = "validatestime"
16
+ s.has_rdoc = true
17
+ s.extra_rdoc_files = ["README.rdoc", "LICENSE", "TODO", "CHANGELOG"]
18
+ s.summary = "Date and time validation plugin for Rails 2.x which allows custom formats"
19
+ s.description = s.summary
20
+ s.author = "Adam Meehan"
21
+ s.email = "adam.meehan@gmail.com"
22
+ s.homepage = "http://github.com/adzap/validates_timeliness"
23
+
24
+ s.require_path = 'lib'
25
+ s.autorequire = GEM
26
+ s.files = %w(LICENSE README.rdoc Rakefile TODO CHANGELOG) + Dir.glob("{lib,spec}/**/*")
27
+ end
28
+
29
+ task :default => :spec
30
+
31
+ desc "Run specs"
32
+ Spec::Rake::SpecTask.new do |t|
33
+ t.spec_files = FileList['spec/**/*_spec.rb']
34
+ t.spec_opts = %w(--color)
35
+ end
36
+
37
+
38
+ Rake::GemPackageTask.new(spec) do |pkg|
39
+ pkg.gem_spec = spec
40
+ end
41
+
42
+ desc "install the gem locally"
43
+ task :install => [:package] do
44
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
45
+ end
46
+
47
+ desc "create a gemspec file"
48
+ task :make_spec do
49
+ File.open("#{GEM}.gemspec", "w") do |file|
50
+ file.puts spec.to_ruby
51
+ end
52
+ end
data/TODO ADDED
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,49 @@
1
+ require 'validates_timeliness/formats'
2
+ require 'validates_timeliness/parser'
3
+ require 'validates_timeliness/validator'
4
+ require 'validates_timeliness/validation_methods'
5
+
6
+ require 'validates_timeliness/active_record/attribute_methods'
7
+ require 'validates_timeliness/active_record/multiparameter_attributes'
8
+ require 'validates_timeliness/action_view/instance_tag'
9
+
10
+ module ValidatesTimeliness
11
+
12
+ mattr_accessor :default_timezone
13
+ self.default_timezone = :utc
14
+
15
+ mattr_accessor :use_time_zones
16
+ self.use_time_zones = false
17
+
18
+ LOCALE_PATH = File.expand_path(File.dirname(__FILE__) + '/validates_timeliness/locale/en.yml')
19
+
20
+ class << self
21
+
22
+ def enable_datetime_select_extension!
23
+ enable_datetime_select_invalid_value_extension!
24
+ enable_multiparameter_attributes_extension!
25
+ end
26
+
27
+ def load_error_messages
28
+ defaults = YAML::load(IO.read(LOCALE_PATH))['en']
29
+ ValidatesTimeliness::Validator.error_value_formats = defaults['validates_timeliness']['error_value_formats'].symbolize_keys
30
+
31
+ if defined?(I18n)
32
+ I18n.load_path.unshift(LOCALE_PATH)
33
+ I18n.reload!
34
+ else
35
+ errors = defaults['activerecord']['errors']['messages'].inject({}) {|h,(k,v)| h[k.to_sym] = v.gsub(/\{\{\w*\}\}/, '%s');h }
36
+ ::ActiveRecord::Errors.default_error_messages.update(errors)
37
+ end
38
+ end
39
+
40
+ def setup_for_rails
41
+ self.default_timezone = ::ActiveRecord::Base.default_timezone
42
+ self.use_time_zones = ::ActiveRecord::Base.time_zone_aware_attributes rescue false
43
+ self.enable_active_record_datetime_parser!
44
+ load_error_messages
45
+ end
46
+ end
47
+ end
48
+
49
+ ValidatesTimeliness.setup_for_rails
@@ -0,0 +1,52 @@
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
@@ -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 unless Time === value
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,365 @@
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
+ end
213
+
214
+ # Delete formats of specified type. Error raised if format not found.
215
+ def remove_formats(type, *remove_formats)
216
+ remove_formats.each do |format|
217
+ unless self.send("#{type}_formats").delete(format)
218
+ raise "Format #{format} not found in #{type} formats"
219
+ end
220
+ end
221
+ compile_format_expressions
222
+ end
223
+
224
+ # Adds new formats. Must specify format type and can specify a :before
225
+ # option to nominate which format the new formats should be inserted in
226
+ # front on to take higher precedence.
227
+ # Error is raised if format already exists or if :before format is not found.
228
+ def add_formats(type, *add_formats)
229
+ formats = self.send("#{type}_formats")
230
+ options = {}
231
+ options = add_formats.pop if add_formats.last.is_a?(Hash)
232
+ before = options[:before]
233
+ raise "Format for :before option #{format} was not found." if before && !formats.include?(before)
234
+
235
+ add_formats.each do |format|
236
+ raise "Format #{format} is already included in #{type} formats" if formats.include?(format)
237
+
238
+ index = before ? formats.index(before) : -1
239
+ formats.insert(index, format)
240
+ end
241
+ compile_format_expressions
242
+ end
243
+
244
+ # Removes formats where the 1 or 2 digit month comes first, to eliminate
245
+ # formats which are ambiguous with the European style of day then month.
246
+ # The mmm token is ignored as its not ambigous.
247
+ def remove_us_formats
248
+ us_format_regexp = /\Am{1,2}[^m]/
249
+ date_formats.reject! { |format| us_format_regexp =~ format }
250
+ datetime_formats.reject! { |format| us_format_regexp =~ format }
251
+ compile_format_expressions
252
+ end
253
+
254
+ def full_hour(hour, meridian)
255
+ hour = hour.to_i
256
+ return hour if meridian.nil?
257
+ if meridian.delete('.').downcase == 'am'
258
+ hour == 12 ? 0 : hour
259
+ else
260
+ hour == 12 ? hour : hour + 12
261
+ end
262
+ end
263
+
264
+ def unambiguous_year(year)
265
+ if year.length <= 2
266
+ century = Time.now.year.to_s[0..1].to_i
267
+ century -= 1 if year.to_i >= ambiguous_year_threshold
268
+ year = "#{century}#{year.rjust(2,'0')}"
269
+ end
270
+ year.to_i
271
+ end
272
+
273
+ def month_index(month)
274
+ return month.to_i if month.to_i.nonzero?
275
+ abbr_month_names.index(month.capitalize) || month_names.index(month.capitalize)
276
+ end
277
+
278
+ def month_names
279
+ defined?(I18n) ? I18n.t('date.month_names') : Date::MONTHNAMES
280
+ end
281
+
282
+ def abbr_month_names
283
+ defined?(I18n) ? I18n.t('date.abbr_month_names') : Date::ABBR_MONTHNAMES
284
+ end
285
+
286
+ def microseconds(usec)
287
+ (".#{usec}".to_f * 1_000_000).to_i
288
+ end
289
+
290
+ def offset_in_seconds(offset)
291
+ sign = offset =~ /^-/ ? -1 : 1
292
+ parts = offset.scan(/\d\d/).map {|p| p.to_f }
293
+ parts[1] = parts[1].to_f / 60
294
+ (parts[0] + parts[1]) * sign * 3600
295
+ end
296
+
297
+ private
298
+
299
+ # Generate regular expression and processor from format string
300
+ def generate_format_expression(string_format)
301
+ regexp = string_format.dup
302
+ order = {}
303
+ regexp.gsub!(/([\.\\])/, '\\\\\1') # escapes dots and backslashes
304
+
305
+ format_tokens.each do |token|
306
+ token_name = token.keys.first
307
+ token_regexp, regexp_str, arg_key = *token.values.first
308
+
309
+ # hack for lack of look-behinds. If has a capture group then is
310
+ # considered an anchor to put straight back in the regexp string.
311
+ regexp.gsub!(token_regexp) {|m| "#{$1}" + regexp_str }
312
+ order[arg_key] = $~.begin(0) if $~ && !arg_key.nil?
313
+ end
314
+
315
+ return Regexp.new(regexp), format_proc(order)
316
+ rescue
317
+ raise "The following format regular expression failed to compile: #{regexp}\n from format #{string_format}."
318
+ end
319
+
320
+ # Generates a proc which when executed maps the regexp capture groups to a
321
+ # proc argument based on order captured. A time array is built using the proc
322
+ # argument in the position indicated by the first element of the proc arg
323
+ # array.
324
+ #
325
+ def format_proc(order)
326
+ arg_map = format_proc_args
327
+ args = order.invert.sort.map {|p| arg_map[p[1]][1] }
328
+ arr = [nil] * 7
329
+ order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? }
330
+ proc_string = <<-EOL
331
+ lambda {|#{args.join(',')}|
332
+ md ||= nil
333
+ [#{arr.map {|i| i.nil? ? 'nil' : i }.join(',')}].map {|i| i.is_a?(Float) ? i : i.to_i }
334
+ }
335
+ EOL
336
+ eval proc_string
337
+ end
338
+
339
+ def compile_formats(formats)
340
+ formats.map { |format| [ format, *generate_format_expression(format) ] }
341
+ end
342
+
343
+ # Pick expression set and combine date and datetimes for
344
+ # datetime attributes to allow date string as datetime
345
+ def expression_set(type, string)
346
+ case type
347
+ when :date
348
+ date_expressions
349
+ when :time
350
+ time_expressions
351
+ when :datetime
352
+ # gives a speed-up for date string as datetime attributes
353
+ if string.length < 11
354
+ date_expressions + datetime_expressions
355
+ else
356
+ datetime_expressions + date_expressions
357
+ end
358
+ end
359
+ end
360
+
361
+ end
362
+ end
363
+ end
364
+
365
+ ValidatesTimeliness::Formats.compile_format_expressions