szilm-validates_timeliness 2.3.1

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 (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