validates_timeliness 2.3.2 → 3.0.0.beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +12 -4
- data/LICENSE +1 -1
- data/README.rdoc +138 -280
- data/Rakefile +30 -16
- data/lib/generators/validates_timeliness/install_generator.rb +17 -0
- data/lib/generators/validates_timeliness/templates/en.yml +16 -0
- data/lib/generators/validates_timeliness/templates/validates_timeliness.rb +22 -0
- data/lib/validates_timeliness.rb +46 -52
- data/lib/validates_timeliness/attribute_methods.rb +51 -0
- data/lib/validates_timeliness/conversion.rb +69 -0
- data/lib/validates_timeliness/extensions.rb +14 -0
- data/lib/validates_timeliness/extensions/date_time_select.rb +45 -0
- data/lib/validates_timeliness/extensions/multiparameter_handler.rb +31 -0
- data/lib/validates_timeliness/helper_methods.rb +41 -0
- data/lib/validates_timeliness/orms/active_record.rb +14 -0
- data/lib/validates_timeliness/parser.rb +389 -17
- data/lib/validates_timeliness/validator.rb +37 -200
- data/lib/validates_timeliness/version.rb +1 -1
- data/spec/model_helpers.rb +27 -0
- data/spec/spec_helper.rb +74 -43
- data/spec/test_model.rb +56 -0
- data/spec/validates_timeliness/attribute_methods_spec.rb +36 -0
- data/spec/validates_timeliness/conversion_spec.rb +204 -0
- data/spec/validates_timeliness/extensions/date_time_select_spec.rb +178 -0
- data/spec/validates_timeliness/extensions/multiparameter_handler_spec.rb +21 -0
- data/spec/validates_timeliness/helper_methods_spec.rb +36 -0
- data/spec/{formats_spec.rb → validates_timeliness/parser_spec.rb} +105 -71
- data/spec/validates_timeliness/validator/after_spec.rb +59 -0
- data/spec/validates_timeliness/validator/before_spec.rb +59 -0
- data/spec/validates_timeliness/validator/is_at_spec.rb +63 -0
- data/spec/validates_timeliness/validator/on_or_after_spec.rb +59 -0
- data/spec/validates_timeliness/validator/on_or_before_spec.rb +59 -0
- data/spec/validates_timeliness/validator_spec.rb +172 -0
- data/validates_timeliness.gemspec +30 -0
- metadata +42 -40
- data/TODO +0 -8
- data/lib/validates_timeliness/action_view/instance_tag.rb +0 -52
- data/lib/validates_timeliness/active_record/attribute_methods.rb +0 -77
- data/lib/validates_timeliness/active_record/multiparameter_attributes.rb +0 -69
- data/lib/validates_timeliness/formats.rb +0 -368
- data/lib/validates_timeliness/locale/en.new.yml +0 -18
- data/lib/validates_timeliness/locale/en.old.yml +0 -18
- data/lib/validates_timeliness/matcher.rb +0 -1
- data/lib/validates_timeliness/spec/rails/matchers/validate_timeliness.rb +0 -162
- data/lib/validates_timeliness/validation_methods.rb +0 -46
- data/spec/action_view/instance_tag_spec.rb +0 -194
- data/spec/active_record/attribute_methods_spec.rb +0 -157
- data/spec/active_record/multiparameter_attributes_spec.rb +0 -118
- data/spec/ginger_scenarios.rb +0 -19
- data/spec/parser_spec.rb +0 -65
- data/spec/resources/application.rb +0 -2
- data/spec/resources/person.rb +0 -3
- data/spec/resources/schema.rb +0 -10
- data/spec/resources/sqlite_patch.rb +0 -19
- data/spec/spec/rails/matchers/validate_timeliness_spec.rb +0 -245
- data/spec/time_travel/MIT-LICENSE +0 -20
- data/spec/time_travel/time_extensions.rb +0 -33
- data/spec/time_travel/time_travel.rb +0 -12
- data/spec/validator_spec.rb +0 -723
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
|